diff --git a/.gitmodules b/.gitmodules index 1903ae720..cd4407edf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,7 +4,7 @@ [submodule "providers/flagd/test-harness"] path = providers/flagd/test-harness url = https://github.com/open-feature/test-harness.git - branch = v2.5.0 + branch = v2.7.1 [submodule "providers/flagd/spec"] path = providers/flagd/spec url = https://github.com/open-feature/spec.git diff --git a/providers/flagd/schemas b/providers/flagd/schemas index 9b0ee43ec..e840a037d 160000 --- a/providers/flagd/schemas +++ b/providers/flagd/schemas @@ -1 +1 @@ -Subproject commit 9b0ee43ecc477e277d41770034fa495ec78838fe +Subproject commit e840a037dc49801185217f45cc404fc8ff2cd0d4 diff --git a/providers/flagd/spec b/providers/flagd/spec index 130df3eb6..aad6193d7 160000 --- a/providers/flagd/spec +++ b/providers/flagd/spec @@ -1 +1 @@ -Subproject commit 130df3eb61f1b8d1a121d54f33563ce0ec27c1b6 +Subproject commit aad6193d77eca269bc5a8dc0a0b626dbf98d924b diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/rpc/RpcResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/rpc/RpcResolver.java index 580281d4d..466c26359 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/rpc/RpcResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/rpc/RpcResolver.java @@ -263,14 +263,14 @@ private ProviderEvaluation Boolean isEvaluationCacheable(ProviderEvaluation evaluation) { + private boolean isEvaluationCacheable(ProviderEvaluation evaluation) { String reason = evaluation.getReason(); return reason != null && reason.equals(Config.STATIC_REASON) && this.cacheAvailable(); } - private Boolean cacheAvailable() { - return this.cache.getEnabled(); + private boolean cacheAvailable() { + return this.cache.isEnabled(); } private static ImmutableMetadata metadataFromResponse(Message response) { diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/rpc/cache/Cache.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/rpc/cache/Cache.java index 45d27fac1..068ce65de 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/rpc/cache/Cache.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/rpc/cache/Cache.java @@ -16,12 +16,12 @@ public class Cache { private Map> store; @Getter - private final Boolean enabled; + private final boolean enabled; /** * Initialize the cache. * - * @param forType type of the cache. + * @param forType type of the cache. * @param maxCacheSize max amount of element to keep. */ public Cache(final String forType, int maxCacheSize) { @@ -36,19 +36,50 @@ public Cache(final String forType, int maxCacheSize) { } } + /** + * Adds a provider evaluation to the cache. + * + * @param key the key of the flag + * @param value the provider evaluation + */ public void put(String key, ProviderEvaluation value) { + if (!enabled) { + return; + } this.store.put(key, value); } + /** + * Retrieves a provider evaluation from the cache, or null if the key has not been cached before. + * + * @param key the key of the flag + */ public ProviderEvaluation get(String key) { + if (!enabled) { + return null; + } return this.store.get(key); } + /** + * Removes a provider evaluation from the cache. + * + * @param key the key of the flag + */ public void remove(String key) { + if (!enabled) { + return; + } this.store.remove(key); } + /** + * Clears the cache. + */ public void clear() { + if (!enabled) { + return; + } this.store.clear(); } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java index 6ff6e1765..6258dad37 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java @@ -79,13 +79,14 @@ void interruptingWaitingThread_isIgnored() throws InterruptedException { @Test void callingInitialize_wakesUpWaitingThread() throws InterruptedException { final AtomicBoolean isWaiting = new AtomicBoolean(); + final AtomicBoolean successfulTest = new AtomicBoolean(); Thread waitingThread = new Thread(() -> { long start = System.currentTimeMillis(); isWaiting.set(true); flagdProviderSyncResources.waitForInitialization(10000); long end = System.currentTimeMillis(); long duration = end - start; - Assertions.assertTrue(duration < MAX_TIME_TOLERANCE); + successfulTest.set(duration < MAX_TIME_TOLERANCE * 2); }); waitingThread.start(); @@ -98,12 +99,15 @@ void callingInitialize_wakesUpWaitingThread() throws InterruptedException { flagdProviderSyncResources.initialize(); waitingThread.join(); + + Assertions.assertTrue(successfulTest.get()); } @Timeout(2) @Test void callingShutdown_wakesUpWaitingThreadWithException() throws InterruptedException { final AtomicBoolean isWaiting = new AtomicBoolean(); + final AtomicBoolean successfulTest = new AtomicBoolean(); Thread waitingThread = new Thread(() -> { long start = System.currentTimeMillis(); isWaiting.set(true); @@ -112,7 +116,7 @@ void callingShutdown_wakesUpWaitingThreadWithException() throws InterruptedExcep long end = System.currentTimeMillis(); long duration = end - start; - Assertions.assertTrue(duration < MAX_TIME_TOLERANCE); + successfulTest.set(duration < MAX_TIME_TOLERANCE * 2); }); waitingThread.start(); @@ -125,6 +129,8 @@ void callingShutdown_wakesUpWaitingThreadWithException() throws InterruptedExcep flagdProviderSyncResources.shutdown(); waitingThread.join(); + + Assertions.assertTrue(successfulTest.get()); } @Timeout(2) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EventSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EventSteps.java index 96130c048..32eed5b9d 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EventSteps.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EventSteps.java @@ -27,7 +27,7 @@ public EventSteps(State state) { @Given("a {} event handler") public void a_stale_event_handler(String eventType) { state.client.on(mapEventType(eventType), eventDetails -> { - log.info("event tracked for {} at {} ms ", eventType, System.currentTimeMillis() % 100_000); + log.info("{} event tracked", eventType); state.events.add(new Event(eventType, eventDetails)); }); } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/FlagSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/FlagSteps.java index cd0d65afa..24f7b0e08 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/FlagSteps.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/FlagSteps.java @@ -4,12 +4,19 @@ import dev.openfeature.contrib.providers.flagd.e2e.State; import dev.openfeature.sdk.FlagEvaluationDetails; +import dev.openfeature.sdk.ImmutableMetadata; import dev.openfeature.sdk.Value; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.parallel.Isolated; +@Slf4j @Isolated() public class FlagSteps extends AbstractSteps { @@ -54,6 +61,9 @@ public void the_flag_was_evaluated_with_details() throws InterruptedException { @Then("the resolved details value should be \"{}\"") public void the_resolved_details_value_should_be(String value) throws Throwable { + if (state.evaluation.getErrorCode() != null) { + log.warn(state.evaluation.getErrorMessage()); + } assertThat(state.evaluation.getValue()).isEqualTo(Utils.convert(value, state.flag.type)); } @@ -84,4 +94,47 @@ public Flag(String type, String name, Object defaultValue) { this.type = type; } } + + @Then("the resolved metadata is empty") + @SuppressWarnings("unchecked") + public void the_resolved_metadata_is_empty() throws NoSuchFieldException, IllegalAccessException { + ImmutableMetadata flagMetadata = state.evaluation.getFlagMetadata(); + + Field metadataField = flagMetadata.getClass().getDeclaredField("metadata"); + metadataField.setAccessible(true); + Map metadataMap = (Map) metadataField.get(flagMetadata); + assertThat(metadataMap).isEmpty(); + } + + @Then("the resolved metadata should contain") + @SuppressWarnings("unchecked") + public void the_resolved_metadata_should_contain(io.cucumber.datatable.DataTable dataTable) + throws IOException, ClassNotFoundException { + + ImmutableMetadata flagMetadata = state.evaluation.getFlagMetadata(); + + List> rows = dataTable.asMaps(String.class, String.class); + for (Map row : rows) { + switch (row.get("metadata_type")) { + case "String": + assertThat(flagMetadata.getString(row.get("key"))) + .isEqualTo(Utils.convert(row.get("value"), row.get("metadata_type"))); + break; + case "Boolean": + assertThat(flagMetadata.getBoolean(row.get("key"))) + .isEqualTo(Utils.convert(row.get("value"), row.get("metadata_type"))); + break; + case "Float": + assertThat(flagMetadata.getDouble(row.get("key"))) + .isEqualTo(Utils.convert(row.get("value"), row.get("metadata_type"))); + break; + case "Integer": + assertThat(flagMetadata.getInteger(row.get("key"))) + .isEqualTo(Utils.convert(row.get("value"), row.get("metadata_type"))); + break; + default: + throw new AssertionError("type not supported"); + } + } + } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderSteps.java index eb837afe1..ae16290d5 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderSteps.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderSteps.java @@ -3,6 +3,7 @@ import static io.restassured.RestAssured.when; import dev.openfeature.contrib.providers.flagd.Config; +import dev.openfeature.contrib.providers.flagd.FlagdOptions; import dev.openfeature.contrib.providers.flagd.FlagdProvider; import dev.openfeature.contrib.providers.flagd.e2e.FlagdContainer; import dev.openfeature.contrib.providers.flagd.e2e.State; @@ -104,7 +105,21 @@ public void setupProvider(String providerType) throws InterruptedException { .certPath(absolutePath); flagdConfig = "ssl"; break; + case "metadata": + flagdConfig = "metadata"; + if (State.resolverType == Config.Resolver.FILE) { + FlagdOptions build = state.builder.build(); + String selector = build.getSelector(); + String replace = selector.replace("rawflags/", ""); + + state.builder + .port(UNAVAILABLE_PORT) + .offlineFlagSourcePath(new File("test-harness/flags/" + replace).getAbsolutePath()); + } else { + state.builder.port(container.getPort(State.resolverType)); + } + break; default: this.state.providerType = ProviderType.DEFAULT; if (State.resolverType == Config.Resolver.FILE) { @@ -124,12 +139,13 @@ public void setupProvider(String providerType) throws InterruptedException { .then() .statusCode(200); - // giving flagd a little time to start - Thread.sleep(300); + Thread.sleep(50); + FeatureProvider provider = new FlagdProvider(state.builder.resolverType(State.resolverType).build()); String providerName = "Provider " + Math.random(); OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + if (wait) { api.setProviderAndWait(providerName, provider); } else { @@ -151,10 +167,7 @@ public void the_connection_is_lost_for(int seconds) { } @When("the flag was modified") - public void the_flag_was_modded() throws InterruptedException { + public void the_flag_was_modded() { when().post("http://" + container.getLaunchpadUrl() + "/change").then().statusCode(200); - - // we might be too fast in the execution - Thread.sleep(1000); } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/rpc/RpcResolverTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/rpc/RpcResolverTest.java index 19a61c95d..eb9a55c13 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/rpc/RpcResolverTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/rpc/RpcResolverTest.java @@ -50,11 +50,11 @@ public void init() throws Exception { when(stub.withDeadlineAfter(anyLong(), any())).thenReturn(stub); doAnswer(new Answer() { public Void answer(InvocationOnMock invocation) { - latch.countDown(); Object[] args = invocation.getArguments(); if (args[1] != null) { observer = (QueueingStreamObserver) args[1]; } + latch.countDown(); return null; } }) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/rpc/cache/CacheTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/rpc/cache/CacheTest.java index ede1542b7..476fae408 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/rpc/cache/CacheTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/rpc/cache/CacheTest.java @@ -20,9 +20,9 @@ void cacheTypeTest() { final Cache undefined = new Cache("invalid", 10); // then - assertTrue(lru.getEnabled()); - assertFalse(disabled.getEnabled()); - assertFalse(undefined.getEnabled()); + assertTrue(lru.isEnabled()); + assertFalse(disabled.isEnabled()); + assertFalse(undefined.isEnabled()); } @Test diff --git a/providers/flagd/test-harness b/providers/flagd/test-harness index 9d35a07f4..5f3f68931 160000 --- a/providers/flagd/test-harness +++ b/providers/flagd/test-harness @@ -1 +1 @@ -Subproject commit 9d35a07f43c6b5e1810a5e83029aae62a5dbd494 +Subproject commit 5f3f6893108daed3b510952e97c5429ad1897d8e