diff --git a/pom.xml b/pom.xml index a022454b7..a88742a0b 100644 --- a/pom.xml +++ b/pom.xml @@ -190,6 +190,9 @@ cpu-count + + 0.25 + diff --git a/src/main/java/dev/openfeature/sdk/Client.java b/src/main/java/dev/openfeature/sdk/Client.java index a4ccf26f9..c5f3d110e 100644 --- a/src/main/java/dev/openfeature/sdk/Client.java +++ b/src/main/java/dev/openfeature/sdk/Client.java @@ -5,7 +5,7 @@ /** * Interface used to resolve flags of varying types. */ -public interface Client extends Features { +public interface Client extends Features, EventHandling { Metadata getMetadata(); /** diff --git a/src/main/java/dev/openfeature/sdk/EventDetails.java b/src/main/java/dev/openfeature/sdk/EventDetails.java new file mode 100644 index 000000000..4f36afe16 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/EventDetails.java @@ -0,0 +1,12 @@ +package dev.openfeature.sdk; + +import lombok.Data; +import lombok.experimental.SuperBuilder; + +/** + * Interface for attaching event handlers. + */ +@Data @SuperBuilder(toBuilder = true) +public class EventDetails extends ProviderEventDetails { + private String clientName; +} diff --git a/src/main/java/dev/openfeature/sdk/EventEmitter.java b/src/main/java/dev/openfeature/sdk/EventEmitter.java new file mode 100644 index 000000000..693a94c83 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/EventEmitter.java @@ -0,0 +1,108 @@ +package dev.openfeature.sdk; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +import javax.annotation.Nullable; + +import dev.openfeature.sdk.internal.AutoCloseableLock; +import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock; +import lombok.extern.slf4j.Slf4j; + +/** + * Event emitter construct to be used by providers. + */ +@Slf4j +public class EventEmitter { + + private static final ExecutorService taskExecutor = Executors.newCachedThreadPool(); + private final Map>> handlerMap; + private AutoCloseableReentrantReadWriteLock handlersLock = new AutoCloseableReentrantReadWriteLock(); + + /** + * Construct a new EventEmitter. + */ + public EventEmitter() { + handlerMap = new ConcurrentHashMap>>(); + handlerMap.put(ProviderEvent.PROVIDER_READY, new ArrayList<>()); + handlerMap.put(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, new ArrayList<>()); + handlerMap.put(ProviderEvent.PROVIDER_ERROR, new ArrayList<>()); + handlerMap.put(ProviderEvent.PROVIDER_STALE, new ArrayList<>()); + } + + /** + * Emit an event. + * + * @param event Event type to emit. + * @param details Event details. + */ + public void emit(ProviderEvent event, ProviderEventDetails details) { + try (AutoCloseableLock __ = handlersLock.readLockAutoCloseable()) { + EventDetails eventDetails = EventDetails.builder() + .flagMetadata(details.getFlagMetadata()) + .flagsChanged(details.getFlagsChanged()) + .message(details.getMessage()) + .build(); + + // we may be forwarding this event, preserve the name if so. + if (EventDetails.class.isInstance(details)) { + eventDetails.setClientName(((EventDetails) details).getClientName()); + } + + this.handlerMap.get(event).stream().forEach(handler -> { + runHander(handler, eventDetails); + }); + } + } + + void runHander(Consumer handler, EventDetails eventDetails) { + taskExecutor.submit(() -> { + try { + handler.accept(eventDetails); + } catch (Exception e) { + log.error("Exception in event handler {}", handler, e); + } + }); + } + + void addHandler(ProviderEvent event, Consumer handler) { + try (AutoCloseableLock __ = handlersLock.writeLockAutoCloseable()) { + this.handlerMap.get(event).add(handler); + } + } + + void removeHandler(ProviderEvent event, Consumer handler) { + try (AutoCloseableLock __ = handlersLock.writeLockAutoCloseable()) { + this.handlerMap.get(event).remove(handler); + } + } + + void removeAllHandlers() { + try (AutoCloseableLock __ = handlersLock.writeLockAutoCloseable()) { + this.handlerMap.keySet().stream() + .forEach(type -> handlerMap.get(type).clear()); + } + } + + /** + * Propagates all events from the originatingEmitter to this one. + * + * @param originatingEmitter The emitter to forward events from. + * @param clientName The client name that will be added to the events. + */ + void forwardEvents(EventEmitter originatingEmitter, @Nullable String clientName) { + Arrays.asList(ProviderEvent.values()).stream().forEach(eventType -> { + originatingEmitter.addHandler(eventType, details -> { + // set the client name when we proxy the events through. + details.setClientName(clientName); + this.emit(eventType, details); + }); + }); + } +} diff --git a/src/main/java/dev/openfeature/sdk/EventHandling.java b/src/main/java/dev/openfeature/sdk/EventHandling.java new file mode 100644 index 000000000..23b3c15ab --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/EventHandling.java @@ -0,0 +1,17 @@ +package dev.openfeature.sdk; + +import java.util.function.Consumer; + +/** + * Interface for attaching event handlers. + */ +public interface EventHandling { + + T onProviderReady(Consumer handler); + + T onProviderConfigurationChanged(Consumer handler); + + T onProviderError(Consumer handler); + + T onProviderStale(Consumer handler); +} diff --git a/src/main/java/dev/openfeature/sdk/EventProvider.java b/src/main/java/dev/openfeature/sdk/EventProvider.java new file mode 100644 index 000000000..ddade17d9 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/EventProvider.java @@ -0,0 +1,21 @@ +package dev.openfeature.sdk; + +/** + * Interface for attaching event handlers. + * + * @see SimpleEventProvider for a basic implementation. + */ +public interface EventProvider { + + /** + * Return the EventEmitter interface for this provider. + * The same instance should be returned from this method at each invocation. + * + * @return the EventEmitter instance for this EventProvider. + */ + EventEmitter getEventEmitter(); + + static boolean isEventProvider(FeatureProvider provider) { + return EventProvider.class.isInstance(provider); + } +} diff --git a/src/main/java/dev/openfeature/sdk/FeatureProvider.java b/src/main/java/dev/openfeature/sdk/FeatureProvider.java index 7df56a5f0..707dbfec2 100644 --- a/src/main/java/dev/openfeature/sdk/FeatureProvider.java +++ b/src/main/java/dev/openfeature/sdk/FeatureProvider.java @@ -4,7 +4,8 @@ import java.util.List; /** - * The interface implemented by upstream flag providers to resolve flags for their service. + * The interface implemented by upstream flag providers to resolve flags for + * their service. */ public interface FeatureProvider { Metadata getMetadata(); @@ -24,22 +25,28 @@ default List getProviderHooks() { ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx); /** - * This method is called before a provider is used to evaluate flags. Providers can overwrite this method, - * if they have special initialization needed prior being called for flag evaluation. + * This method is called before a provider is used to evaluate flags. Providers + * can overwrite this method, + * if they have special initialization needed prior being called for flag + * evaluation. *

- * It is ok, if the method is expensive as it is executed in the background. All runtime exceptions will be + * It is ok, if the method is expensive as it is executed in the background. All + * runtime exceptions will be * caught and logged. *

*/ - default void initialize() { + default void initialize(EvaluationContext evaluationContext) throws Exception { // Intentionally left blank } /** - * This method is called when a new provider is about to be used to evaluate flags, or the SDK is shut down. - * Providers can overwrite this method, if they have special shutdown actions needed. + * This method is called when a new provider is about to be used to evaluate + * flags, or the SDK is shut down. + * Providers can overwrite this method, if they have special shutdown actions + * needed. *

- * It is ok, if the method is expensive as it is executed in the background. All runtime exceptions will be + * It is ok, if the method is expensive as it is executed in the background. All + * runtime exceptions will be * caught and logged. *

*/ @@ -47,4 +54,14 @@ default void shutdown() { // Intentionally left blank } + /** + * Returns a representation of the current readiness of the provider. + * Providers which do not implement this method are assumed to be ready immediately. + * + * @return ProviderState + */ + default ProviderState getState() { + return ProviderState.READY; + } + } diff --git a/src/main/java/dev/openfeature/sdk/NoOpProvider.java b/src/main/java/dev/openfeature/sdk/NoOpProvider.java index c2e841a53..d3d9ca21b 100644 --- a/src/main/java/dev/openfeature/sdk/NoOpProvider.java +++ b/src/main/java/dev/openfeature/sdk/NoOpProvider.java @@ -10,6 +10,12 @@ public class NoOpProvider implements FeatureProvider { @Getter private final String name = "No-op Provider"; + // The Noop provider is ALWAYS NOT_READY, otherwise READY handlers would run immediately when attached. + @Override + public ProviderState getState() { + return ProviderState.NOT_READY; + } + @Override public Metadata getMetadata() { return new Metadata() { diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java index 2e921a746..2cf94b2c9 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java @@ -1,28 +1,30 @@ package dev.openfeature.sdk; -import dev.openfeature.sdk.internal.AutoCloseableLock; -import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock; -import lombok.extern.slf4j.Slf4j; - -import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.function.Consumer; + +import javax.annotation.Nullable; + +import dev.openfeature.sdk.internal.AutoCloseableLock; +import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock; +import lombok.extern.slf4j.Slf4j; /** - * A global singleton which holds base configuration for the OpenFeature library. + * A global singleton which holds base configuration for the OpenFeature + * library. * Configuration here will be shared across all {@link Client}s. */ @Slf4j -public class OpenFeatureAPI { +public class OpenFeatureAPI implements EventHandling { // package-private multi-read/single-write lock static AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock(); static AutoCloseableReentrantReadWriteLock contextLock = new AutoCloseableReentrantReadWriteLock(); - + private EvaluationContext evaluationContext; private final List apiHooks; - private ProviderRepository providerRepository = new ProviderRepository(); - private EvaluationContext evaluationContext; + final EventEmitter emitter = new EventEmitter(); protected OpenFeatureAPI() { this.apiHooks = new ArrayList<>(); @@ -49,16 +51,29 @@ public Metadata getProviderMetadata(String clientName) { return getProvider(clientName).getMetadata(); } + /** + * {@inheritDoc} + */ public Client getClient() { return getClient(null, null); } + /** + * {@inheritDoc} + */ public Client getClient(@Nullable String name) { return getClient(name, null); } + /** + * {@inheritDoc} + */ public Client getClient(@Nullable String name, @Nullable String version) { - return new OpenFeatureClient(this, name, version); + return new OpenFeatureClient(this, + () -> this.providerRepository.getProvider(name).getState(), + this.providerRepository.getAndCacheEmitter(name), + name, + version); } /** @@ -83,6 +98,7 @@ public EvaluationContext getEvaluationContext() { * Set the default provider. */ public void setProvider(FeatureProvider provider) { + propagateEventsIfSupported(provider, null); providerRepository.setProvider(provider); } @@ -93,6 +109,7 @@ public void setProvider(FeatureProvider provider) { * @param provider The provider to set. */ public void setProvider(String clientName, FeatureProvider provider) { + propagateEventsIfSupported(provider, clientName); providerRepository.setProvider(clientName, provider); } @@ -144,6 +161,37 @@ public void shutdown() { providerRepository.shutdown(); } + @Override + public OpenFeatureAPI onProviderReady(Consumer handler) { + return this.on(ProviderEvent.PROVIDER_READY, handler); + } + + @Override + public OpenFeatureAPI onProviderConfigurationChanged(Consumer handler) { + return this.on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler); + } + + @Override + public OpenFeatureAPI onProviderError(Consumer handler) { + return this.on(ProviderEvent.PROVIDER_ERROR, handler); + } + + @Override + public OpenFeatureAPI onProviderStale(Consumer handler) { + return this.on(ProviderEvent.PROVIDER_STALE, handler); + } + + private OpenFeatureAPI on(ProviderEvent event, Consumer consumer) { + this.emitter.addHandler(event, consumer); + return this; + } + + private void propagateEventsIfSupported(FeatureProvider provider, @Nullable String clientName) { + if (EventProvider.isEventProvider(provider)) { + this.emitter.forwardEvents(((EventProvider) provider).getEventEmitter(), clientName); + } + } + /** * This method is only here for testing as otherwise all tests after the API shutdown test would fail. */ diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java index 44febf77b..ab7ba99a2 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java @@ -5,6 +5,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Supplier; import dev.openfeature.sdk.exceptions.GeneralError; import dev.openfeature.sdk.exceptions.OpenFeatureError; @@ -28,24 +30,42 @@ public class OpenFeatureClient implements Client { private final String version; private final List clientHooks; private final HookSupport hookSupport; + private final EventEmitter emitter = new EventEmitter(); AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock(); AutoCloseableReentrantReadWriteLock contextLock = new AutoCloseableReentrantReadWriteLock(); private EvaluationContext evaluationContext; + private Supplier providerState; /** - * Client for evaluating the flag. There may be multiples of these floating - * around. + * Deprecated constructor. Use OpenFeature.API.getClient() instead. * * @param openFeatureAPI Backing global singleton * @param name Name of the client (used by observability tools). * @param version Version of the client (used by observability tools). + * @deprecated Do not use this constructor it wil be removed. + * Clients created using it will not run event handlers. + * Use the OpenFeatureAPI's getClient factory method instead. */ + @Deprecated() public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String name, String version) { this.openfeatureApi = openFeatureAPI; this.name = name; this.version = version; this.clientHooks = new ArrayList<>(); this.hookSupport = new HookSupport(); + log.info( + "You've directly constructed a OpenFeatureClient. Use OpenFeature.API.getClient() instead."); + } + + protected OpenFeatureClient(OpenFeatureAPI openFeatureAPI, final Supplier providerState, + final EventEmitter providerEventEmitter, String name, String version) { + this.openfeatureApi = openFeatureAPI; + this.providerState = providerState; + this.name = name; + this.version = version; + this.clientHooks = new ArrayList<>(); + this.hookSupport = new HookSupport(); + this.emitter.forwardEvents(providerEventEmitter, name); } /** @@ -95,7 +115,6 @@ private FlagEvaluationDetails evaluateFlag(FlagValueType type, String key Map hints = Collections.unmodifiableMap(flagOptions.getHookHints()); ctx = ObjectUtils.defaultIfNull(ctx, () -> new ImmutableContext()); - FlagEvaluationDetails details = null; List mergedHooks = null; HookContext hookCtx = null; @@ -341,4 +360,33 @@ public FlagEvaluationDetails getObjectDetails(String key, Value defaultVa public Metadata getMetadata() { return () -> name; } + + @Override + public Client onProviderReady(Consumer handler) { + return this.on(ProviderEvent.PROVIDER_READY, handler); + } + + @Override + public Client onProviderConfigurationChanged(Consumer handler) { + return this.on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler); + } + + @Override + public Client onProviderError(Consumer handler) { + return this.on(ProviderEvent.PROVIDER_ERROR, handler); + } + + @Override + public Client onProviderStale(Consumer handler) { + return this.on(ProviderEvent.PROVIDER_STALE, handler); + } + + private Client on(ProviderEvent event, Consumer handler) { + emitter.addHandler(event, handler); + // run handler immediately if our provider is ready + if (ProviderEvent.PROVIDER_READY.equals(event) && ProviderState.READY.equals(this.providerState.get())) { + this.emitter.runHander(handler, EventDetails.builder().clientName(this.name).build()); + } + return this; + } } diff --git a/src/main/java/dev/openfeature/sdk/ProviderEvent.java b/src/main/java/dev/openfeature/sdk/ProviderEvent.java new file mode 100644 index 000000000..dcefd606a --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/ProviderEvent.java @@ -0,0 +1,8 @@ +package dev.openfeature.sdk; + +/** + * Provider event types. + */ +public enum ProviderEvent { + PROVIDER_READY, PROVIDER_CONFIGURATION_CHANGED, PROVIDER_ERROR, PROVIDER_STALE; +} diff --git a/src/main/java/dev/openfeature/sdk/ProviderEventDetails.java b/src/main/java/dev/openfeature/sdk/ProviderEventDetails.java new file mode 100644 index 000000000..aac797624 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/ProviderEventDetails.java @@ -0,0 +1,18 @@ +package dev.openfeature.sdk; + +import java.util.List; + +import javax.annotation.Nullable; + +import lombok.Data; +import lombok.experimental.SuperBuilder; + +/** + * Interface for attaching event handlers. + */ +@Data @SuperBuilder(toBuilder = true) +public class ProviderEventDetails { + @Nullable private List flagsChanged; + @Nullable private String message; + @Nullable private FlagMetadata flagMetadata; // TODO: rename this? +} diff --git a/src/main/java/dev/openfeature/sdk/ProviderRepository.java b/src/main/java/dev/openfeature/sdk/ProviderRepository.java index 5a360eb63..c1e061be2 100644 --- a/src/main/java/dev/openfeature/sdk/ProviderRepository.java +++ b/src/main/java/dev/openfeature/sdk/ProviderRepository.java @@ -1,24 +1,25 @@ package dev.openfeature.sdk; -import lombok.extern.slf4j.Slf4j; - import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; import java.util.stream.Stream; +import javax.annotation.Nullable; + +import lombok.extern.slf4j.Slf4j; + @Slf4j class ProviderRepository { private final Map providers = new ConcurrentHashMap<>(); - private final ExecutorService taskExecutor = Executors.newCachedThreadPool(); - private final Map initializingNamedProviders = new ConcurrentHashMap<>(); + private final Map clientEmitters = new ConcurrentHashMap<>(); private final AtomicReference defaultProvider = new AtomicReference<>(new NoOpProvider()); - private FeatureProvider initializingDefaultProvider; + private final EventEmitter defaultEmitter = new EventEmitter(); + private final ExecutorService taskExecutor = Executors.newCachedThreadPool(); /** * Return the default provider. @@ -44,7 +45,7 @@ public void setProvider(FeatureProvider provider) { if (provider == null) { throw new IllegalArgumentException("Provider cannot be null"); } - initializeProvider(provider); + initializeProvider(null, provider); } /** @@ -63,66 +64,70 @@ public void setProvider(String clientName, FeatureProvider provider) { initializeProvider(clientName, provider); } - private void initializeProvider(FeatureProvider provider) { - initializingDefaultProvider = provider; - initializeProvider(provider, this::updateDefaultProviderAfterInitialization); - } - - private void initializeProvider(String clientName, FeatureProvider provider) { - initializingNamedProviders.put(clientName, provider); - initializeProvider(provider, newProvider -> updateProviderAfterInit(clientName, newProvider)); - } - - private void initializeProvider(FeatureProvider provider, Consumer afterInitialization) { + /** + * Get the emitter for the referenced clientName, or the default. + * The emitter is created if it doesn't exist. + * + * @param clientName name for the client, or null for default. + * @return existing or new emitter for this clientName (or the default). + */ + EventEmitter getAndCacheEmitter(@Nullable String clientName) { + return Optional + .ofNullable(clientName) + .map(name -> { + return Optional + .ofNullable(this.clientEmitters.get(name)) + .filter(emitter -> Optional.ofNullable(emitter).isPresent()) + .orElseGet(() -> { + EventEmitter newEmitter = new EventEmitter(); + this.clientEmitters.put(name, newEmitter); + return newEmitter; + }); + }) + .orElse(defaultEmitter); + } + + private void initializeProvider(@Nullable String clientName, FeatureProvider newProvider) { + forwardProviderEvents(clientName, newProvider); taskExecutor.submit(() -> { try { - if (!isProviderRegistered(provider)) { - provider.initialize(); + if (!isProviderRegistered(newProvider)) { + newProvider.initialize(OpenFeatureAPI.getInstance().getEvaluationContext()); } - afterInitialization.accept(provider); + + FeatureProvider oldProvider = clientName != null + ? this.providers.put(clientName, newProvider) + : this.defaultProvider.getAndSet(newProvider); + emitReadyAndShutdownOld(clientName, oldProvider); } catch (Exception e) { - log.error("Exception when initializing feature provider {}", provider.getClass().getName(), e); + log.error("Exception when initializing feature provider {}", newProvider.getClass().getName(), e); + EventEmitter eventEmitter = this.getAndCacheEmitter(clientName); + EventDetails errorEvent = EventDetails.builder().clientName(clientName).message(e.getMessage()).build(); + eventEmitter.emit(ProviderEvent.PROVIDER_ERROR, errorEvent); + OpenFeatureAPI.getInstance().emitter.emit(ProviderEvent.PROVIDER_ERROR, errorEvent); } }); } - private void updateProviderAfterInit(String clientName, FeatureProvider newProvider) { - Optional - .ofNullable(initializingNamedProviders.get(clientName)) - .filter(initializingProvider -> initializingProvider.equals(newProvider)) - .ifPresent(provider -> updateNamedProviderAfterInitialization(clientName, provider)); - } - - private void updateDefaultProviderAfterInitialization(FeatureProvider initializedProvider) { - Optional - .ofNullable(this.initializingDefaultProvider) - .filter(initializingProvider -> initializingProvider.equals(initializedProvider)) - .ifPresent(this::replaceDefaultProvider); - } - - private void replaceDefaultProvider(FeatureProvider provider) { - FeatureProvider oldProvider = this.defaultProvider.getAndSet(provider); - if (isOldProviderNotBoundByName(oldProvider)) { + private void emitReadyAndShutdownOld(@Nullable String clientName, FeatureProvider oldProvider) { + EventDetails readyEvent = EventDetails.builder().clientName(clientName).build(); + this.getAndCacheEmitter(clientName).emit(ProviderEvent.PROVIDER_READY, readyEvent); + OpenFeatureAPI.getInstance().emitter.emit(ProviderEvent.PROVIDER_READY, readyEvent); + if (!isProviderRegistered(oldProvider)) { shutdownProvider(oldProvider); } } - private boolean isOldProviderNotBoundByName(FeatureProvider oldProvider) { - return !this.providers.containsValue(oldProvider); - } - - private void updateNamedProviderAfterInitialization(String clientName, FeatureProvider initializedProvider) { - Optional - .ofNullable(this.initializingNamedProviders.get(clientName)) - .filter(initializingProvider -> initializingProvider.equals(initializedProvider)) - .ifPresent(provider -> replaceNamedProviderAndShutdownOldOne(clientName, provider)); + private void forwardProviderEvents(@Nullable String clientName, FeatureProvider newProvider) { + if (isEventProvider(newProvider)) { + this.getAndCacheEmitter(clientName) + .forwardEvents(((EventProvider) newProvider).getEventEmitter(), clientName); + } } - private void replaceNamedProviderAndShutdownOldOne(String clientName, FeatureProvider provider) { - FeatureProvider oldProvider = this.providers.put(clientName, provider); - this.initializingNamedProviders.remove(clientName, provider); - if (!isProviderRegistered(oldProvider)) { - shutdownProvider(oldProvider); + private void detachProviderEvents(FeatureProvider oldProvider) { + if (isEventProvider(oldProvider)) { + ((EventProvider) oldProvider).getEventEmitter().removeAllHandlers(); } } @@ -133,6 +138,7 @@ private boolean isProviderRegistered(FeatureProvider oldProvider) { private void shutdownProvider(FeatureProvider provider) { taskExecutor.submit(() -> { try { + detachProviderEvents(provider); provider.shutdown(); } catch (Exception e) { log.error("Exception when shutting down feature provider {}", provider.getClass().getName(), e); @@ -140,8 +146,12 @@ private void shutdownProvider(FeatureProvider provider) { }); } + private boolean isEventProvider(FeatureProvider provider) { + return EventProvider.isEventProvider(provider); + } + /** - * Shutdowns this repository which includes shutting down all FeatureProviders that are registered, + * Shuts down this repository which includes shutting down all FeatureProviders that are registered, * including the default feature provider. */ public void shutdown() { diff --git a/src/main/java/dev/openfeature/sdk/ProviderState.java b/src/main/java/dev/openfeature/sdk/ProviderState.java new file mode 100644 index 000000000..6685f8fe9 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/ProviderState.java @@ -0,0 +1,8 @@ +package dev.openfeature.sdk; + +/** + * Indicates the state of the provider. + */ +public enum ProviderState { + READY, NOT_READY, ERROR; +} diff --git a/src/main/java/dev/openfeature/sdk/SimpleEventProvider.java b/src/main/java/dev/openfeature/sdk/SimpleEventProvider.java new file mode 100644 index 000000000..f9bdda673 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/SimpleEventProvider.java @@ -0,0 +1,18 @@ +package dev.openfeature.sdk; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * Abstract EventProvider encapsulating an EventEmitter. + * + * @see EventProvider + */ +public abstract class SimpleEventProvider implements EventProvider { + protected final EventEmitter eventEmitter = new EventEmitter(); + + @Override + @SuppressFBWarnings(value = {"EI_EXPOSE_REP", "EI_EXPOSE_REP2"}) + public EventEmitter getEventEmitter() { + return this.eventEmitter; + } +} diff --git a/src/test/java/dev/openfeature/sdk/EventsTest.java b/src/test/java/dev/openfeature/sdk/EventsTest.java new file mode 100644 index 000000000..f76a060b5 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/EventsTest.java @@ -0,0 +1,530 @@ +package dev.openfeature.sdk; + +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.after; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class EventsTest { + + private static final int TIMEOUT = 1000; + + class TestEventsProvider implements FeatureProvider, EventProvider { + + private boolean initError = false; + private String initErrorMessage; + private ProviderState state = ProviderState.NOT_READY; + + @Override + public ProviderState getState() { + return this.state; + } + + TestEventsProvider() { + } + + TestEventsProvider(boolean initError, String initErrorMessage) { + this.initError = initError; + this.initErrorMessage = initErrorMessage; + } + + private EventEmitter eventEmitter = new EventEmitter(); + + @Override + public void initialize(EvaluationContext evaluationContext) throws Exception { + if (this.initError) { + this.state = ProviderState.ERROR; + throw new Exception(initErrorMessage); + } + this.state = ProviderState.READY; + } + + public void mockEvent(ProviderEvent event, ProviderEventDetails details) { + this.eventEmitter.emit(event, details); + } + + @Override + public Metadata getMetadata() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getMetadata'"); + } + + @Override + public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, + EvaluationContext ctx) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getBooleanEvaluation'"); + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String defaultValue, + EvaluationContext ctx) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getStringEvaluation'"); + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, + EvaluationContext ctx) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getIntegerEvaluation'"); + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, + EvaluationContext ctx) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getDoubleEvaluation'"); + } + + @Override + public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, + EvaluationContext ctx) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getObjectEvaluation'"); + } + + @Override + public EventEmitter getEventEmitter() { + return eventEmitter; + } + + }; + + @Nested + class ApiEvents { + + @Nested + class NamedProvider { + + @Nested + class Initialization { + + @Test + @DisplayName("should fire initial READY event when provider init succeeds") + @Specification(number = "5.3.1", text = "If the provider's initialize function terminates normally," + + " PROVIDER_READY handlers MUST run.") + void apiInitReady() { + final Consumer handler = mock(Consumer.class); + final String name = "apiInitReady"; + + TestEventsProvider provider = new TestEventsProvider(); + OpenFeatureAPI.getInstance().onProviderReady(handler); + OpenFeatureAPI.getInstance().setProvider(name, provider); + verify(handler, timeout(TIMEOUT)) + .accept(argThat(details -> details.getClientName().equals(name))); + } + + @Test + @DisplayName("should fire initial ERROR event when provider init errors") + @Specification(number = "5.3.2", text = "If the provider's initialize function terminates abnormally," + + " PROVIDER_ERROR handlers MUST run.") + void apiInitError() { + final Consumer handler = mock(Consumer.class); + final String name = "apiInitError"; + final String errMessage = "oh no!"; + + TestEventsProvider provider = new TestEventsProvider(true, errMessage); + OpenFeatureAPI.getInstance().onProviderError(handler); + OpenFeatureAPI.getInstance().setProvider(name, provider); + verify(handler, timeout(3000)).accept(argThat(details -> { + return details.getClientName().equals(name) + && details.getMessage().equals(errMessage); + })); + } + } + + @Nested + class ProviderEvents { + + @Test + @DisplayName("should propagate events") + @Specification(number = "5.1.2", text = "When a provider signals the occurrence of a particular event, " + + "the associated client and API event handlers MUST run.") + void apiShouldPropagateEvents() { + final Consumer handler = mock(Consumer.class); + final String name = "apiProviderBefore"; + + TestEventsProvider provider = new TestEventsProvider(); + OpenFeatureAPI.getInstance().setProvider(name, provider); + OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler); + provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build()); + verify(handler, timeout(TIMEOUT)).accept(argThat(details -> details.getClientName().equals(name))); + } + + @Test + @DisplayName("should support all event types") + @Specification(number = "5.1.1", text = "The provider MAY define a mechanism for signaling the occurrence " + + "of one of a set of events, including PROVIDER_READY, PROVIDER_ERROR, " + + "PROVIDER_CONFIGURATION_CHANGED and PROVIDER_STALE, with a provider event details payload.") + @Specification(number = "5.2.2", text = "The API MUST provide a function for associating handler functions" + + " with a particular provider event type.") + void apiShouldSupportAllEventTypes() throws Exception { + final String name = "apiShouldSupportAllEventTypes"; + final Consumer handler1 = mock(Consumer.class); + final Consumer handler2 = mock(Consumer.class); + final Consumer handler3 = mock(Consumer.class); + final Consumer handler4 = mock(Consumer.class); + + TestEventsProvider provider = new TestEventsProvider(); + OpenFeatureAPI.getInstance().setProvider(name, provider); + OpenFeatureAPI.getInstance().onProviderReady(handler1); + OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler2); + OpenFeatureAPI.getInstance().onProviderStale(handler3); + OpenFeatureAPI.getInstance().onProviderError(handler4); + + Arrays.asList(ProviderEvent.values()).stream().forEach(eventType -> { + provider.mockEvent(eventType, ProviderEventDetails.builder().build()); + }); + + verify(handler1, timeout(TIMEOUT).atLeastOnce()).accept(any()); + verify(handler2, timeout(TIMEOUT).atLeastOnce()).accept(any()); + verify(handler3, timeout(TIMEOUT).atLeastOnce()).accept(any()); + verify(handler4, timeout(TIMEOUT).atLeastOnce()).accept(any()); + } + } + } + } + + @Nested + class ClientEvents { + + @Nested + class NamedProvider { + + @Nested + class Initialization { + @Test + @DisplayName("should fire initial READY event when provider init succeeds after client retrieved") + @Specification(number = "5.3.1", text = "If the provider's initialize function terminates normally, PROVIDER_READY handlers MUST run.") + void initReadyProviderBefore() throws InterruptedException { + final Consumer handler = mock(Consumer.class); + final String name = "initReadyProviderBefore"; + + TestEventsProvider provider = new TestEventsProvider(); + Client client = OpenFeatureAPI.getInstance().getClient(name); + client.onProviderReady(handler); + // set provider after getting a client + OpenFeatureAPI.getInstance().setProvider(name, provider); + verify(handler, timeout(TIMEOUT).atLeastOnce()) + .accept(argThat(details -> details.getClientName().equals(name))); + } + + @Test + @DisplayName("should fire initial READY event when provider init succeeds before client retrieved") + @Specification(number = "5.3.1", text = "If the provider's initialize function terminates normally, PROVIDER_READY handlers MUST run.") + void initReadyProviderAfter() { + final Consumer handler = mock(Consumer.class); + final String name = "initReadyProviderAfter"; + + TestEventsProvider provider = new TestEventsProvider(); + // set provider before getting a client + OpenFeatureAPI.getInstance().setProvider(name, provider); + Client client = OpenFeatureAPI.getInstance().getClient(name); + client.onProviderReady(handler); + verify(handler, timeout(TIMEOUT).atLeastOnce()) + .accept(argThat(details -> details.getClientName().equals(name))); + } + + @Test + @DisplayName("should fire initial ERROR event when provider init errors after client retrieved") + @Specification(number = "5.3.2", text = "If the provider's initialize function terminates abnormally, PROVIDER_ERROR handlers MUST run.") + void initErrorProviderAfter() { + final Consumer handler = mock(Consumer.class); + final String name = "initErrorProviderAfter"; + final String errMessage = "oh no!"; + + TestEventsProvider provider = new TestEventsProvider(true, errMessage); + Client client = OpenFeatureAPI.getInstance().getClient(name); + client.onProviderError(handler); + // set provider after getting a client + OpenFeatureAPI.getInstance().setProvider(name, provider); + verify(handler, timeout(TIMEOUT)).accept(argThat(details -> { + return details.getClientName().equals(name) + && details.getMessage().equals(errMessage); + })); + } + + @Test + @DisplayName("should fire initial ERROR event when provider init errors before client retrieved") + @Specification(number = "5.3.2", text = "If the provider's initialize function terminates abnormally, PROVIDER_ERROR handlers MUST run.") + void initErrorProviderBefore() { + final Consumer handler = mock(Consumer.class); + final String name = "initErrorProviderBefore"; + final String errMessage = "oh no!"; + + TestEventsProvider provider = new TestEventsProvider(true, errMessage); + OpenFeatureAPI.getInstance().onProviderError(handler); + OpenFeatureAPI.getInstance().setProvider(name, provider); + verify(handler, timeout(TIMEOUT)).accept(argThat(details -> { + return details.getClientName().equals(name) + && details.getMessage().equals(errMessage); + })); + } + } + + @Nested + class ProviderEvents { + + @Test + @DisplayName("should propagate events when provider set before client retrieved") + @Specification(number = "5.1.2", text = "When a provider signals the occurrence of a particular event, the associated client and API event handlers MUST run.") + void shouldPropagateBefore() { + final Consumer handler = mock(Consumer.class); + final String name = "shouldPropagateBefore"; + + TestEventsProvider provider = new TestEventsProvider(); + // set provider before getting a client + OpenFeatureAPI.getInstance().setProvider(name, provider); + Client client = OpenFeatureAPI.getInstance().getClient(name); + client.onProviderConfigurationChanged(handler); + provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build()); + verify(handler, timeout(TIMEOUT)).accept(argThat(details -> details.getClientName().equals(name))); + + } + + @Test + @DisplayName("should propagate events when provider set after client retrieved") + @Specification(number = "5.1.2", text = "When a provider signals the occurrence of a particular event, the associated client and API event handlers MUST run.") + void shouldPropagateAfter() { + + final Consumer handler = mock(Consumer.class); + final String name = "shouldPropagateAfter"; + + TestEventsProvider provider = new TestEventsProvider(); + Client client = OpenFeatureAPI.getInstance().getClient(name); + client.onProviderConfigurationChanged(handler); + // set provider after getting a client + OpenFeatureAPI.getInstance().setProvider(name, provider); + provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build()); + verify(handler, timeout(TIMEOUT)).accept(any()); + } + + @Test + @DisplayName("should support all event types") + @Specification(number = "5.1.1", text = "The provider MAY define a mechanism for signaling the occurrence " + + "of one of a set of events, including PROVIDER_READY, PROVIDER_ERROR, " + + "PROVIDER_CONFIGURATION_CHANGED and PROVIDER_STALE, with a provider event details payload.") + @Specification(number = "5.2.1", text = "The client MUST provide a function for associating handler functions" + + " with a particular provider event type.") + void shouldSupportAllEventTypes() throws Exception { + final String name = "shouldSupportAllEventTypes"; + final Consumer handler1 = mock(Consumer.class); + final Consumer handler2 = mock(Consumer.class); + final Consumer handler3 = mock(Consumer.class); + final Consumer handler4 = mock(Consumer.class); + + TestEventsProvider provider = new TestEventsProvider(); + OpenFeatureAPI.getInstance().setProvider(name, provider); + Client client = OpenFeatureAPI.getInstance().getClient(name); + + client.onProviderReady(handler1); + client.onProviderConfigurationChanged(handler2); + client.onProviderStale(handler3); + client.onProviderError(handler4); + + Arrays.asList(ProviderEvent.values()).stream().forEach(eventType -> { + provider.mockEvent(eventType, ProviderEventDetails.builder().build()); + }); + + verify(handler1, timeout(TIMEOUT).atLeastOnce()).accept(any()); + verify(handler2, timeout(TIMEOUT).atLeastOnce()).accept(any()); + verify(handler3, timeout(TIMEOUT).atLeastOnce()).accept(any()); + verify(handler4, timeout(TIMEOUT).atLeastOnce()).accept(any()); + } + } + } + } + + @Test + @DisplayName("shutdown should clean up handlers") + void shouldCleanUpHandlers() throws Exception { + final Consumer handler1 = mock(Consumer.class); + final Consumer handler2 = mock(Consumer.class); + final String name = "shouldCleanUpHandlers"; + + TestEventsProvider provider1 = new TestEventsProvider(); + TestEventsProvider provider2 = new TestEventsProvider(); + OpenFeatureAPI.getInstance().setProvider(name, provider1); + Client client = OpenFeatureAPI.getInstance().getClient(name); + + // attached handlers + OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler1); + client.onProviderConfigurationChanged(handler2); + + OpenFeatureAPI.getInstance().setProvider(name, provider2); + + // wait for the new provider to be ready and make sure things are cleaned up. + await().until(() -> provider2.getState().equals(ProviderState.READY)); + + // fire old event + provider1.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build()); + + // a bit of waiting here, but we want to make sure these are indeed never + // called. + verify(handler1, after(TIMEOUT).never()).accept(any()); + verify(handler2, never()).accept(any()); + } + + @Test + @DisplayName("other client handlers should not run") + @Specification(number = "5.1.3", text = "When a provider signals the occurrence of a particular event, " + + "event handlers on clients which are not associated with that provider MUST NOT run.") + void otherClientHandlersShouldNotRun() throws Exception { + final String name1 = "otherClientHandlersShouldNotRun1"; + final String name2 = "otherClientHandlersShouldNotRun2"; + final Consumer handlerToRun = mock(Consumer.class); + final Consumer handlerNotToRun = mock(Consumer.class); + + TestEventsProvider provider1 = new TestEventsProvider(); + TestEventsProvider provider2 = new TestEventsProvider(); + OpenFeatureAPI.getInstance().setProvider(name1, provider1); + OpenFeatureAPI.getInstance().setProvider(name2, provider2); + + Client client1 = OpenFeatureAPI.getInstance().getClient(name1); + Client client2 = OpenFeatureAPI.getInstance().getClient(name2); + + client1.onProviderConfigurationChanged(handlerToRun); + client2.onProviderConfigurationChanged(handlerNotToRun); + + provider1.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build()); + + verify(handlerToRun, timeout(TIMEOUT)).accept(any()); + verify(handlerNotToRun, after(TIMEOUT).never()).accept(any()); + } + + @Test + @DisplayName("subsequent handlers run if earlier throws") + @Specification(number = "5.2.5", text = "If a handler function terminates abnormally, other handler functions MUST run.") + void handlersRunIfOneThrows() throws Exception { + final String name = "handlersRunIfOneThrows"; + final Consumer errorHandler = mock(Consumer.class); + doThrow(new NullPointerException()).when(errorHandler).accept(any()); + final Consumer nextHandler = mock(Consumer.class); + final Consumer lastHandler = mock(Consumer.class); + + TestEventsProvider provider = new TestEventsProvider(); + OpenFeatureAPI.getInstance().setProvider(name, provider); + + Client client1 = OpenFeatureAPI.getInstance().getClient(name); + + client1.onProviderConfigurationChanged(errorHandler); + client1.onProviderConfigurationChanged(nextHandler); + client1.onProviderConfigurationChanged(lastHandler); + + provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build()); + verify(errorHandler, timeout(TIMEOUT)).accept(any()); + verify(nextHandler).accept(any()); + verify(lastHandler).accept(any()); + } + + @Test + @DisplayName("should have all properties") + @Specification(number = "5.2.4", text = "The handler function MUST accept a event details parameter.") + @Specification(number = "5.2.3", text = "The event details MUST contain the client name associated with the event.") + void shouldHaveAllProperties() throws Exception { + final Consumer handler1 = mock(Consumer.class); + final Consumer handler2 = mock(Consumer.class); + final String name = "shouldCleanUpHandlers"; + + TestEventsProvider provider = new TestEventsProvider(); + OpenFeatureAPI.getInstance().setProvider(name, provider); + Client client = OpenFeatureAPI.getInstance().getClient(name); + + // attached handlers + OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler1); + client.onProviderConfigurationChanged(handler2); + + List flagsChanged = Arrays.asList("flag"); + FlagMetadata metadata = FlagMetadata.builder().addInteger("int", 1).build(); + String message = "a message"; + ProviderEventDetails details = ProviderEventDetails.builder() + .flagMetadata(metadata) + .flagsChanged(flagsChanged) + .message(message) + .build(); + + provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details); + + // both global and client handler should have all the fields. + verify(handler1, timeout(TIMEOUT)) + .accept(argThat((EventDetails ed) -> { + return details.getFlagMetadata().equals(metadata) + && details.getFlagsChanged().equals(flagsChanged) + && details.getMessage().equals(message) + && ed.getClientName().equals(name); + })); + + verify(handler2, timeout(TIMEOUT)) + .accept(argThat((EventDetails ed) -> { + return details.getFlagMetadata().equals(metadata) + && details.getFlagsChanged().equals(flagsChanged) + && details.getMessage().equals(message) + && ed.getClientName().equals(name); + })); + + } + + + @Test + @DisplayName("must persist across changes") + @Specification(number = "5.2.6", text = "Event handlers MUST persist across provider changes.") + void mustPersistAcrossChanges() throws Exception { + final String name = "mustPersistAcrossChanges1"; + final Consumer handler = mock(Consumer.class); + + TestEventsProvider provider1 = new TestEventsProvider(); + TestEventsProvider provider2 = new TestEventsProvider(); + + OpenFeatureAPI.getInstance().setProvider(name, provider1); + Client client = OpenFeatureAPI.getInstance().getClient(name); + client.onProviderConfigurationChanged(handler); + provider1.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build()); + verify(handler, timeout(TIMEOUT).times(1)).accept(any()); + + // wait for the new provider to be ready. + OpenFeatureAPI.getInstance().setProvider(name, provider2); + await().until(() -> provider2.getState().equals(ProviderState.READY)); + + // verify that with the new provider under the same name, the handler is called again. + provider2.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build()); + verify(handler, timeout(TIMEOUT).times(2)).accept(any()); + } + + @Test + @DisplayName("if the provider is ready handlers must run immediately") + @Specification(number = "5.3.3", text = "PROVIDER_READY handlers attached after the provider is already in a ready state MUST run immediately.") + void readyMustRunImmediately() throws Exception { + final String name = "mustPersistAcrossChanges1"; + final Consumer handler = mock(Consumer.class); + + TestEventsProvider provider = new TestEventsProvider(); + + OpenFeatureAPI.getInstance().setProvider(name, provider); + // wait for readiness. + await().until(() -> provider.getState().equals(ProviderState.READY)); + + // should run + Client client = OpenFeatureAPI.getInstance().getClient(name); + client.onProviderReady(handler); + verify(handler, timeout(TIMEOUT)).accept(any()); + } + + @Specification(number = "5.1.4", text = "PROVIDER_ERROR events SHOULD populate the provider event details's error message field.") + @Test + void thisIsAProviderRequirement() { + } +} diff --git a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java index 57f0c0454..eb41fd950 100644 --- a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java @@ -62,20 +62,20 @@ void getApiInstance() { assertSame(OpenFeatureAPI.getInstance(), OpenFeatureAPI.getInstance()); } - @Specification(number="1.1.2", text="The API MUST provide a function to set the global provider singleton, which accepts an API-conformant provider implementation.") + @Specification(number="1.1.2.1", text="The API MUST define a provider mutator, a function to set the default provider, which accepts an API-conformant provider implementation.") @Test void provider() { FeatureProvider mockProvider = mock(FeatureProvider.class); FeatureProviderTestUtils.setFeatureProvider(mockProvider); assertThat(api.getProvider()).isEqualTo(mockProvider); } - @Specification(number="1.1.4", text="The API MUST provide a function for retrieving the metadata field of the configured provider.") + @Specification(number="1.1.5", text="The API MUST provide a function for retrieving the metadata field of the configured provider.") @Test void provider_metadata() { FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider()); assertThat(api.getProviderMetadata().getName()).isEqualTo(DoSomethingProvider.name); } - @Specification(number="1.1.3", text="The API MUST provide a function to add hooks which accepts one or more API-conformant hooks, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.") + @Specification(number="1.1.4", text="The API MUST provide a function to add hooks which accepts one or more API-conformant hooks, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.") @Test void hook_addition() { Hook h1 = mock(Hook.class); Hook h2 = mock(Hook.class); @@ -89,7 +89,7 @@ void getApiInstance() { assertEquals(h2, api.getHooks().get(1)); } - @Specification(number="1.1.5", text="The API MUST provide a function for creating a client which accepts the following options: - name (optional): A logical string identifier for the client.") + @Specification(number="1.1.6", text="The API MUST provide a function for creating a client which accepts the following options: - name (optional): A logical string identifier for the client.") @Test void namedClient() { assertThatCode(() -> api.getClient("Sir Calls-a-lot")).doesNotThrowAnyException(); // TODO: Doesn't say that you can *get* the client name.. which seems useful? @@ -286,7 +286,7 @@ void getApiInstance() { @Specification(number="1.3.3", text="The client SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied default value should be returned.") @Test void type_system_prevents_this() {} - @Specification(number="1.1.6", text="The client creation function MUST NOT throw, or otherwise abnormally terminate.") + @Specification(number="1.1.7", text="The client creation function MUST NOT throw, or otherwise abnormally terminate.") @Test void constructor_does_not_throw() {} @Specification(number="1.4.11", text="The client SHOULD provide asynchronous or non-blocking mechanisms for flag evaluation.") diff --git a/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java b/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java index 7061719fa..19c3093ba 100644 --- a/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java @@ -21,12 +21,12 @@ class DefaultProvider { @Test @DisplayName("must call initialize function of the newly registered provider before using it for " + "flag evaluation") - void mustCallInitializeFunctionOfTheNewlyRegisteredProviderBeforeUsingItForFlagEvaluation() { + void mustCallInitializeFunctionOfTheNewlyRegisteredProviderBeforeUsingItForFlagEvaluation() throws Exception { FeatureProvider featureProvider = mock(FeatureProvider.class); OpenFeatureAPI.getInstance().setProvider(featureProvider); - verify(featureProvider, timeout(1000)).initialize(); + verify(featureProvider, timeout(1000)).initialize(any()); } @Specification(number = "1.4.9", text = "Methods, functions, or operations on the client MUST NOT throw " @@ -35,14 +35,14 @@ void mustCallInitializeFunctionOfTheNewlyRegisteredProviderBeforeUsingItForFlagE + "the purposes for configuration or setup.") @Test @DisplayName("should catch exception thrown by the provider on initialization") - void shouldCatchExceptionThrownByTheProviderOnInitialization() { + void shouldCatchExceptionThrownByTheProviderOnInitialization() throws Exception { FeatureProvider featureProvider = mock(FeatureProvider.class); - doThrow(TestException.class).when(featureProvider).initialize(); + doThrow(TestException.class).when(featureProvider).initialize(any()); assertThatCode(() -> OpenFeatureAPI.getInstance().setProvider(featureProvider)) .doesNotThrowAnyException(); - verify(featureProvider, timeout(1000)).initialize(); + verify(featureProvider, timeout(1000)).initialize(any()); } } @@ -54,12 +54,12 @@ class ProviderForNamedClient { @Test @DisplayName("must call initialize function of the newly registered named provider before using it " + "for flag evaluation") - void mustCallInitializeFunctionOfTheNewlyRegisteredNamedProviderBeforeUsingItForFlagEvaluation() { + void mustCallInitializeFunctionOfTheNewlyRegisteredNamedProviderBeforeUsingItForFlagEvaluation() throws Exception { FeatureProvider featureProvider = mock(FeatureProvider.class); OpenFeatureAPI.getInstance().setProvider("clientName", featureProvider); - verify(featureProvider, timeout(1000)).initialize(); + verify(featureProvider, timeout(1000)).initialize(any()); } @Specification(number = "1.4.9", text = "Methods, functions, or operations on the client MUST NOT throw " @@ -68,14 +68,14 @@ void mustCallInitializeFunctionOfTheNewlyRegisteredNamedProviderBeforeUsingItFor + "the purposes for configuration or setup.") @Test @DisplayName("should catch exception thrown by the named client provider on initialization") - void shouldCatchExceptionThrownByTheNamedClientProviderOnInitialization() { + void shouldCatchExceptionThrownByTheNamedClientProviderOnInitialization() throws Exception { FeatureProvider featureProvider = mock(FeatureProvider.class); - doThrow(TestException.class).when(featureProvider).initialize(); + doThrow(TestException.class).when(featureProvider).initialize(any()); assertThatCode(() -> OpenFeatureAPI.getInstance().setProvider("clientName", featureProvider)) .doesNotThrowAnyException(); - verify(featureProvider, timeout(1000)).initialize(); + verify(featureProvider, timeout(1000)).initialize(any()); } } } diff --git a/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java b/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java index 00c7949e6..6a54171f8 100644 --- a/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java +++ b/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java @@ -1,11 +1,17 @@ package dev.openfeature.sdk; -import dev.openfeature.sdk.testutils.exception.TestException; -import lombok.SneakyThrows; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +import static dev.openfeature.sdk.fixtures.ProviderFixture.createMockedProvider; +import static dev.openfeature.sdk.testutils.stubbing.ConditionStubber.doBlock; +import static dev.openfeature.sdk.testutils.stubbing.ConditionStubber.doDelayResponse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; import java.time.Duration; import java.util.concurrent.CountDownLatch; @@ -14,22 +20,19 @@ import java.util.concurrent.Future; import java.util.function.Function; -import static dev.openfeature.sdk.fixtures.ProviderFixture.*; -import static dev.openfeature.sdk.testutils.stubbing.ConditionStubber.doBlock; -import static dev.openfeature.sdk.testutils.stubbing.ConditionStubber.doDelayResponse; -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import dev.openfeature.sdk.testutils.exception.TestException; class ProviderRepositoryTest { private static final String CLIENT_NAME = "client name"; private static final String ANOTHER_CLIENT_NAME = "another client name"; private static final String FEATURE_KEY = "some key"; + private static final int TIMEOUT = 5000; private final ExecutorService executorService = Executors.newCachedThreadPool(); @@ -60,9 +63,9 @@ void shouldHaveNoOpProviderSetAsDefaultOnInitialization() { @Test @DisplayName("should immediately return when calling the provider mutator") - void shouldImmediatelyReturnWhenCallingTheProviderMutator() { + void shouldImmediatelyReturnWhenCallingTheProviderMutator() throws Exception { FeatureProvider featureProvider = createMockedProvider(); - doDelayResponse(Duration.ofSeconds(10)).when(featureProvider).initialize(); + doDelayResponse(Duration.ofSeconds(10)).when(featureProvider).initialize(new ImmutableContext()); await() .alias("wait for provider mutator to return") @@ -70,19 +73,19 @@ void shouldImmediatelyReturnWhenCallingTheProviderMutator() { .atMost(Duration.ofSeconds(1)) .until(() -> { providerRepository.setProvider(featureProvider); - verify(featureProvider, timeout(100)).initialize(); + verify(featureProvider, timeout(TIMEOUT)).initialize(any()); return true; }); - verify(featureProvider).initialize(); + verify(featureProvider).initialize(any()); } @Test @DisplayName("should not return set provider if initialize has not yet been finished executing") - void shouldNotReturnSetProviderIfItsInitializeMethodHasNotYetBeenFinishedExecuting() { + void shouldNotReturnSetProviderIfItsInitializeMethodHasNotYetBeenFinishedExecuting() throws Exception { CountDownLatch latch = new CountDownLatch(1); FeatureProvider newProvider = createMockedProvider(); - doBlock(latch).when(newProvider).initialize(); + doBlock(latch).when(newProvider).initialize(any()); FeatureProvider oldProvider = providerRepository.getProvider(); providerRepository.setProvider(newProvider); @@ -95,43 +98,18 @@ void shouldNotReturnSetProviderIfItsInitializeMethodHasNotYetBeenFinishedExecuti .pollDelay(Duration.ofMillis(1)) .atMost(Duration.ofSeconds(1)) .untilAsserted(() -> assertThat(providerRepository.getProvider()).isEqualTo(newProvider)); - verify(newProvider, timeout(100)).initialize(); - } - - @SneakyThrows - @Test - @DisplayName("should discard provider still initializing if a newer has finished before") - void shouldDiscardProviderStillInitializingIfANewerHasFinishedBefore() { - CountDownLatch latch = new CountDownLatch(1); - CountDownLatch testBlockingLatch = new CountDownLatch(1); - FeatureProvider blockedProvider = createBlockedProvider(latch, testBlockingLatch::countDown); - FeatureProvider fastProvider = createUnblockingProvider(latch); - - providerRepository.setProvider(blockedProvider); - providerRepository.setProvider(fastProvider); - - assertThat(testBlockingLatch.await(2, SECONDS)) - .as("blocking provider initialization not completed within 2 seconds") - .isTrue(); - - await() - .pollDelay(Duration.ofMillis(1)) - .atMost(Duration.ofSeconds(1)) - .untilAsserted(() -> assertThat(providerRepository.getProvider()).isEqualTo(fastProvider)); - - verify(blockedProvider, timeout(100)).initialize(); - verify(fastProvider, timeout(100)).initialize(); + verify(newProvider, timeout(TIMEOUT)).initialize(any()); } @Test @DisplayName("should avoid additional initialization call if provider has been initialized already") - void shouldAvoidAdditionalInitializationCallIfProviderHasBeenInitializedAlready() { + void shouldAvoidAdditionalInitializationCallIfProviderHasBeenInitializedAlready() throws Exception { FeatureProvider provider = createMockedProvider(); setFeatureProvider(CLIENT_NAME, provider); setFeatureProvider(provider); - verify(provider).initialize(); + verify(provider).initialize(any()); } } @@ -141,21 +119,23 @@ class NamedProvider { @Test @DisplayName("should reject null as named provider") void shouldRejectNullAsNamedProvider() { - assertThatCode(() -> providerRepository.setProvider(CLIENT_NAME, null)).isInstanceOf(IllegalArgumentException.class); + assertThatCode(() -> providerRepository.setProvider(CLIENT_NAME, null)) + .isInstanceOf(IllegalArgumentException.class); } @Test @DisplayName("should reject null as client name") void shouldRejectNullAsDefaultProvider() { NoOpProvider provider = new NoOpProvider(); - assertThatCode(() -> providerRepository.setProvider(null, provider)).isInstanceOf(IllegalArgumentException.class); + assertThatCode(() -> providerRepository.setProvider(null, provider)) + .isInstanceOf(IllegalArgumentException.class); } @Test @DisplayName("should immediately return when calling the named client provider mutator") - void shouldImmediatelyReturnWhenCallingTheNamedClientProviderMutator() { + void shouldImmediatelyReturnWhenCallingTheNamedClientProviderMutator() throws Exception { FeatureProvider featureProvider = createMockedProvider(); - doDelayResponse(Duration.ofSeconds(10)).when(featureProvider).initialize(); + doDelayResponse(Duration.ofSeconds(10)).when(featureProvider).initialize(any()); await() .alias("wait for provider mutator to return") @@ -163,17 +143,17 @@ void shouldImmediatelyReturnWhenCallingTheNamedClientProviderMutator() { .atMost(Duration.ofSeconds(1)) .until(() -> { providerRepository.setProvider("named client", featureProvider); - verify(featureProvider, timeout(1000)).initialize(); + verify(featureProvider, timeout(TIMEOUT)).initialize(any()); return true; }); } @Test @DisplayName("should not return set provider if it's initialization has not yet been finished executing") - void shouldNotReturnSetProviderIfItsInitializeMethodHasNotYetBeenFinishedExecuting() { + void shouldNotReturnSetProviderIfItsInitializeMethodHasNotYetBeenFinishedExecuting() throws Exception { CountDownLatch latch = new CountDownLatch(1); FeatureProvider newProvider = createMockedProvider(); - doBlock(latch).when(newProvider).initialize(); + doBlock(latch).when(newProvider).initialize(any()); FeatureProvider oldProvider = createMockedProvider(); setFeatureProvider(CLIENT_NAME, oldProvider); @@ -186,45 +166,18 @@ void shouldNotReturnSetProviderIfItsInitializeMethodHasNotYetBeenFinishedExecuti .pollDelay(Duration.ofMillis(1)) .atMost(Duration.ofSeconds(1)) .untilAsserted(() -> assertThat(getNamedProvider()).isEqualTo(newProvider)); - verify(newProvider, timeout(100)).initialize(); + verify(newProvider, timeout(TIMEOUT)).initialize(any()); } - - @SneakyThrows - @Test - @DisplayName("should discard provider still initializing if a newer has finished before") - void shouldDiscardProviderStillInitializingIfANewerHasFinishedBefore() { - String clientName = "clientName"; - CountDownLatch latch = new CountDownLatch(1); - CountDownLatch testBlockingLatch = new CountDownLatch(1); - FeatureProvider blockedProvider = createBlockedProvider(latch, testBlockingLatch::countDown); - FeatureProvider unblockingProvider = createUnblockingProvider(latch); - - providerRepository.setProvider(clientName, blockedProvider); - providerRepository.setProvider(clientName, unblockingProvider); - - assertThat(testBlockingLatch.await(2, SECONDS)) - .as("blocking provider initialization not completed within 2 seconds") - .isTrue(); - - await() - .pollDelay(Duration.ofMillis(1)) - .atMost(Duration.ofSeconds(1)) - .untilAsserted(() -> assertThat(providerRepository.getProvider(clientName)) - .isEqualTo(unblockingProvider)); - - verify(blockedProvider, timeout(100)).initialize(); - verify(unblockingProvider, timeout(100)).initialize(); - } - + @Test @DisplayName("should avoid additional initialization call if provider has been initialized already") - void shouldAvoidAdditionalInitializationCallIfProviderHasBeenInitializedAlready() { + void shouldAvoidAdditionalInitializationCallIfProviderHasBeenInitializedAlready() throws Exception { FeatureProvider provider = createMockedProvider(); setFeatureProvider(provider); setFeatureProvider(CLIENT_NAME, provider); - verify(provider).initialize(); + verify(provider).initialize(any()); } } } @@ -237,9 +190,9 @@ class DefaultProvider { @Test @DisplayName("should immediately return when calling the provider mutator") - void shouldImmediatelyReturnWhenCallingTheProviderMutator() { + void shouldImmediatelyReturnWhenCallingTheProviderMutator() throws Exception { FeatureProvider newProvider = createMockedProvider(); - doDelayResponse(Duration.ofSeconds(10)).when(newProvider).initialize(); + doDelayResponse(Duration.ofSeconds(10)).when(newProvider).initialize(any()); await() .alias("wait for provider mutator to return") @@ -247,19 +200,19 @@ void shouldImmediatelyReturnWhenCallingTheProviderMutator() { .atMost(Duration.ofSeconds(1)) .until(() -> { providerRepository.setProvider(newProvider); - verify(newProvider, timeout(100)).initialize(); + verify(newProvider, timeout(TIMEOUT)).initialize(any()); return true; }); - verify(newProvider).initialize(); + verify(newProvider).initialize(any()); } @Test @DisplayName("should use old provider if replacing one has not yet been finished initializing") - void shouldUseOldProviderIfReplacingOneHasNotYetBeenFinishedInitializing() { + void shouldUseOldProviderIfReplacingOneHasNotYetBeenFinishedInitializing() throws Exception { CountDownLatch latch = new CountDownLatch(1); FeatureProvider newProvider = createMockedProvider(); - doBlock(latch).when(newProvider).initialize(); + doBlock(latch).when(newProvider).initialize(any()); FeatureProvider oldProvider = createMockedProvider(); setFeatureProvider(oldProvider); @@ -272,7 +225,7 @@ void shouldUseOldProviderIfReplacingOneHasNotYetBeenFinishedInitializing() { .atMost(Duration.ofSeconds(1)) .pollDelay(Duration.ofMillis(1)) .untilAsserted(() -> assertThat(getProvider()).isEqualTo(newProvider)); - verify(oldProvider, timeout(100)).getBooleanEvaluation(any(), any(), any()); + verify(oldProvider, timeout(TIMEOUT)).getBooleanEvaluation(any(), any(), any()); verify(newProvider, never()).getBooleanEvaluation(any(), any(), any()); } @@ -295,9 +248,9 @@ class NamedProvider { @Test @DisplayName("should immediately return when calling the provider mutator") - void shouldImmediatelyReturnWhenCallingTheProviderMutator() { + void shouldImmediatelyReturnWhenCallingTheProviderMutator() throws Exception { FeatureProvider newProvider = createMockedProvider(); - doDelayResponse(Duration.ofSeconds(10)).when(newProvider).initialize(); + doDelayResponse(Duration.ofSeconds(10)).when(newProvider).initialize(any()); Future providerMutation = executorService .submit(() -> providerRepository.setProvider(CLIENT_NAME, newProvider)); @@ -311,10 +264,10 @@ void shouldImmediatelyReturnWhenCallingTheProviderMutator() { @Test @DisplayName("should use old provider if replacement one has not yet been finished initializing") - void shouldUseOldProviderIfReplacementHasNotYetBeenFinishedInitializing() { + void shouldUseOldProviderIfReplacementHasNotYetBeenFinishedInitializing() throws Exception { CountDownLatch latch = new CountDownLatch(1); FeatureProvider newProvider = createMockedProvider(); - doBlock(latch).when(newProvider).initialize(); + doBlock(latch).when(newProvider).initialize(any()); FeatureProvider oldProvider = createMockedProvider(); setFeatureProvider(CLIENT_NAME, oldProvider); @@ -327,7 +280,7 @@ void shouldUseOldProviderIfReplacementHasNotYetBeenFinishedInitializing() { .pollDelay(Duration.ofMillis(1)) .atMost(Duration.ofSeconds(1)) .untilAsserted(() -> assertThat(getNamedProvider()).isEqualTo(newProvider)); - verify(oldProvider, timeout(100)).getBooleanEvaluation(eq(FEATURE_KEY), any(), any()); + verify(oldProvider, timeout(TIMEOUT)).getBooleanEvaluation(eq(FEATURE_KEY), any(), any()); verify(newProvider, never()).getBooleanEvaluation(any(), any(), any()); } @@ -385,7 +338,7 @@ void shouldShutdownAllFeatureProvidersOnShutdown() { await() .pollDelay(Duration.ofMillis(1)) - .atMost(Duration.ofSeconds(1)) + .atMost(Duration.ofSeconds(TIMEOUT)) .untilAsserted(() -> { assertThat(providerRepository.getProvider()).isInstanceOf(NoOpProvider.class); assertThat(providerRepository.getProvider(CLIENT_NAME)).isInstanceOf(NoOpProvider.class); @@ -418,7 +371,7 @@ private void waitForSettingProviderHasBeenCompleted( FeatureProvider provider) { await() .pollDelay(Duration.ofMillis(1)) - .atMost(Duration.ofSeconds(1)) + .atMost(Duration.ofSeconds(5)) .until(() -> extractor.apply(providerRepository) == provider); } diff --git a/src/test/java/dev/openfeature/sdk/ProviderSpecTest.java b/src/test/java/dev/openfeature/sdk/ProviderSpecTest.java index 31a6a5e8d..f5e5e6a42 100644 --- a/src/test/java/dev/openfeature/sdk/ProviderSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/ProviderSpecTest.java @@ -18,7 +18,7 @@ void name_accessor() { @Specification(number = "2.2.2.1", text = "The feature provider interface MUST define methods for typed " + "flag resolution, including boolean, numeric, string, and structure.") @Specification(number = "2.2.3", text = "In cases of normal execution, the `provider` MUST populate the `resolution details` structure's `value` field with the resolved flag value.") - @Specification(number = "2.2.1", text = "The `feature provider` interface MUST define methods to resolve flag values, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required) + and `evaluation context` (optional), which returns a `resolution details` structure.") + @Specification(number = "2.2.1", text = "The `feature provider` interface MUST define methods to resolve flag values, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required) and `evaluation context` (optional), which returns a `resolution details` structure.") @Specification(number = "2.2.8.1", text = "The `resolution details` structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.") @Test void flag_value_set() { diff --git a/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java b/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java index d191c8c42..e62c82e31 100644 --- a/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java @@ -89,7 +89,7 @@ void shouldCatchExceptionThrownByTheNamedClientProviderOnShutdown() { @Nested class General { - @Specification(number = "1.6.1", text = "The API MUST define a shutdown function which, when called, must call the respective shutdown function on the active provider.") + @Specification(number = "1.6.1", text = "The API MUST define a mechanism to propagate a shutdown request to active providers.") @Test @DisplayName("must shutdown all providers on shutting down api") void mustShutdownAllProvidersOnShuttingDownApi() { diff --git a/src/test/java/dev/openfeature/sdk/fixtures/ProviderFixture.java b/src/test/java/dev/openfeature/sdk/fixtures/ProviderFixture.java index f0b786422..4c5b8a477 100644 --- a/src/test/java/dev/openfeature/sdk/fixtures/ProviderFixture.java +++ b/src/test/java/dev/openfeature/sdk/fixtures/ProviderFixture.java @@ -1,6 +1,7 @@ package dev.openfeature.sdk.fixtures; import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.ImmutableContext; import lombok.experimental.UtilityClass; import org.mockito.stubbing.Answer; @@ -16,9 +17,9 @@ public static FeatureProvider createMockedProvider() { return mock(FeatureProvider.class); } - public static FeatureProvider createBlockedProvider(CountDownLatch latch, Runnable onAnswer) { + public static FeatureProvider createBlockedProvider(CountDownLatch latch, Runnable onAnswer) throws Exception { FeatureProvider provider = createMockedProvider(); - doBlock(latch, createAnswerExecutingCode(onAnswer)).when(provider).initialize(); + doBlock(latch, createAnswerExecutingCode(onAnswer)).when(provider).initialize(new ImmutableContext()); doReturn("blockedProvider").when(provider).toString(); return provider; } @@ -30,12 +31,12 @@ private static Answer createAnswerExecutingCode(Runnable onAnswer) { }; } - public static FeatureProvider createUnblockingProvider(CountDownLatch latch) { + public static FeatureProvider createUnblockingProvider(CountDownLatch latch) throws Exception { FeatureProvider provider = createMockedProvider(); doAnswer(invocation -> { latch.countDown(); return null; - }).when(provider).initialize(); + }).when(provider).initialize(new ImmutableContext()); doReturn("unblockingProvider").when(provider).toString(); return provider; }