From 3eebb73ff084233e678fdddcb771979c4f4cb0ff Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Mon, 24 Aug 2026 21:19:08 +0000 Subject: [PATCH 1/2] Stabilize OpenFeature provider event tests Wait for provider events instead of polling asynchronous SDK state with a one-second deadline. Initialize recovery coverage synchronously and remove the unused Awaitility test dependency. Environment: Datadog workspace --- .../feature-flagging-api/build.gradle.kts | 1 - .../trace/api/openfeature/ProviderTest.java | 88 +++++++------------ 2 files changed, 33 insertions(+), 56 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/build.gradle.kts b/products/feature-flagging/feature-flagging-api/build.gradle.kts index af7012b8cc1..b20efb17bd7 100644 --- a/products/feature-flagging/feature-flagging-api/build.gradle.kts +++ b/products/feature-flagging/feature-flagging-api/build.gradle.kts @@ -52,7 +52,6 @@ dependencies { testImplementation(libs.bundles.junit5) testImplementation(libs.bundles.mockito) testImplementation(libs.moshi) - testImplementation("org.awaitility:awaitility:4.3.0") // The main source set gets the bootstrap/config types as compileOnly, so the JMH source set // needs them on its own compile and runtime classpath to drive the hook end to end. diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java index 9623a4a479a..689a5b8cf86 100644 --- a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java @@ -1,12 +1,11 @@ package datadog.trace.api.openfeature; import static datadog.trace.api.openfeature.Provider.METADATA; -import static java.time.Duration.ofSeconds; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; -import static org.awaitility.Awaitility.await; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -39,6 +38,7 @@ import dev.openfeature.sdk.exceptions.ProviderNotReadyError; import java.lang.reflect.Field; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -47,18 +47,13 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.junit.jupiter.MockitoExtension; -@ExtendWith(MockitoExtension.class) public class ProviderTest { - @Captor private ArgumentCaptor eventDetailsCaptor; + private static final long EVENT_TIMEOUT_SECONDS = 10; private ExecutorService executor; @@ -77,15 +72,18 @@ public void tearDown() { } @Test - public void testSetProvider() { + public void testSetProvider() throws Exception { final OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + final CompletableFuture readyEvent = new CompletableFuture<>(); + api.onProviderReady(readyEvent::complete); api.setProvider(new Provider()); final Client client = api.getClient(); assertThat(client.getProviderState(), equalTo(ProviderState.NOT_READY)); FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); - await().atMost(ofSeconds(1)).until(() -> client.getProviderState() == ProviderState.READY); + readyEvent.get(EVENT_TIMEOUT_SECONDS, SECONDS); + assertThat(client.getProviderState(), equalTo(ProviderState.READY)); } @Test @@ -97,35 +95,29 @@ public void testSetProviderAndWait() throws Exception { assertThat(client.getProviderState(), equalTo(ProviderState.NOT_READY)); FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); - await().atMost(ofSeconds(1)).until(() -> client.getProviderState() == ProviderState.READY); - provider.get(1, SECONDS); + provider.get(EVENT_TIMEOUT_SECONDS, SECONDS); + assertThat(client.getProviderState(), equalTo(ProviderState.READY)); } @Test - public void testSetProviderAndWaitTimeoutRecoversWhenConfigurationArrives() { - final Consumer readyEvent = mock(Consumer.class); + public void testSetProviderAndWaitTimeoutRecoversWhenConfigurationArrives() throws Exception { + final CompletableFuture readyEvent = new CompletableFuture<>(); final OpenFeatureAPI api = OpenFeatureAPI.getInstance(); final Client client = api.getClient(); - client.on(ProviderEvent.PROVIDER_READY, readyEvent); + client.on(ProviderEvent.PROVIDER_READY, readyEvent::complete); assertThrows( ProviderNotReadyError.class, () -> api.setProviderAndWait(new Provider(new Options().initTimeout(10, MILLISECONDS)))); assertThat(client.getProviderState(), equalTo(ProviderState.ERROR)); - verify(readyEvent, times(0)).accept(any()); + assertFalse(readyEvent.isDone()); FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); - await() - .atMost(ofSeconds(1)) - .untilAsserted( - () -> { - assertThat(client.getProviderState(), equalTo(ProviderState.READY)); - verify(readyEvent, times(1)).accept(eventDetailsCaptor.capture()); - final EventDetails eventDetails = eventDetailsCaptor.getValue(); - assertThat(eventDetails.getProviderName(), equalTo(METADATA)); - }); + final EventDetails eventDetails = readyEvent.get(EVENT_TIMEOUT_SECONDS, SECONDS); + assertThat(client.getProviderState(), equalTo(ProviderState.READY)); + assertThat(eventDetails.getProviderName(), equalTo(METADATA)); } @Test @@ -266,49 +258,35 @@ public ProviderEvaluation evaluate( } @Test - public void testNullConfigurationAfterReadyTransitionsToErrorAndRecovers() { + public void testNullConfigurationAfterReadyTransitionsToErrorAndRecovers() throws Exception { final OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProvider(new Provider()); - final Client client = api.getClient(); - FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); - await().atMost(ofSeconds(1)).until(() -> client.getProviderState() == ProviderState.READY); + api.setProviderAndWait(new Provider()); + final Client client = api.getClient(); + assertThat(client.getProviderState(), equalTo(ProviderState.READY)); - final Consumer errorEvent = mock(Consumer.class); - final Consumer readyEvent = mock(Consumer.class); - final Consumer configChangedEvent = mock(Consumer.class); - client.on(ProviderEvent.PROVIDER_ERROR, errorEvent); - client.on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, configChangedEvent); + final CompletableFuture errorEvent = new CompletableFuture<>(); + final CompletableFuture readyEvent = new CompletableFuture<>(); + final CompletableFuture configChangedEvent = new CompletableFuture<>(); + client.on(ProviderEvent.PROVIDER_ERROR, errorEvent::complete); + client.on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, configChangedEvent::complete); FeatureFlaggingGateway.dispatch((ServerConfiguration) null); - await() - .atMost(ofSeconds(1)) - .untilAsserted( - () -> { - assertThat(client.getProviderState(), equalTo(ProviderState.ERROR)); - verify(errorEvent, times(1)).accept(eventDetailsCaptor.capture()); - final EventDetails eventDetails = eventDetailsCaptor.getValue(); - assertThat(eventDetails.getProviderName(), equalTo(METADATA)); - }); + final EventDetails eventDetails = errorEvent.get(EVENT_TIMEOUT_SECONDS, SECONDS); + assertThat(client.getProviderState(), equalTo(ProviderState.ERROR)); + assertThat(eventDetails.getProviderName(), equalTo(METADATA)); final FlagEvaluationDetails evalDetails = client.getStringDetails("missing", "default"); assertThat(evalDetails.getValue(), equalTo("default")); assertThat(evalDetails.getErrorCode(), equalTo(ErrorCode.PROVIDER_NOT_READY)); - client.on(ProviderEvent.PROVIDER_READY, readyEvent); + client.on(ProviderEvent.PROVIDER_READY, readyEvent::complete); FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); - await() - .atMost(ofSeconds(1)) - .untilAsserted( - () -> { - assertThat(client.getProviderState(), equalTo(ProviderState.READY)); - verify(readyEvent, times(1)).accept(any()); - }); + readyEvent.get(EVENT_TIMEOUT_SECONDS, SECONDS); + assertThat(client.getProviderState(), equalTo(ProviderState.READY)); FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); - await() - .atMost(ofSeconds(1)) - .untilAsserted(() -> verify(configChangedEvent, times(1)).accept(any())); + configChangedEvent.get(EVENT_TIMEOUT_SECONDS, SECONDS); } @Test From 41cdfdfbf03aa37a0dc3601c2d6053aa4bd9e542 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Mon, 24 Aug 2026 23:21:25 +0000 Subject: [PATCH 2/2] Preserve OpenFeature event count assertions Wrap the provider event futures with verifiable consumers so deterministic waiting continues to reject duplicate transition events. Environment: Datadog workspace --- .../trace/api/openfeature/ProviderTest.java | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java index 689a5b8cf86..d7adf645e74 100644 --- a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java @@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -102,9 +103,10 @@ public void testSetProviderAndWait() throws Exception { @Test public void testSetProviderAndWaitTimeoutRecoversWhenConfigurationArrives() throws Exception { final CompletableFuture readyEvent = new CompletableFuture<>(); + final Consumer readyEventHandler = completingHandler(readyEvent); final OpenFeatureAPI api = OpenFeatureAPI.getInstance(); final Client client = api.getClient(); - client.on(ProviderEvent.PROVIDER_READY, readyEvent::complete); + client.on(ProviderEvent.PROVIDER_READY, readyEventHandler); assertThrows( ProviderNotReadyError.class, @@ -118,6 +120,7 @@ public void testSetProviderAndWaitTimeoutRecoversWhenConfigurationArrives() thro final EventDetails eventDetails = readyEvent.get(EVENT_TIMEOUT_SECONDS, SECONDS); assertThat(client.getProviderState(), equalTo(ProviderState.READY)); assertThat(eventDetails.getProviderName(), equalTo(METADATA)); + verify(readyEventHandler, times(1)).accept(any()); } @Test @@ -268,8 +271,11 @@ public void testNullConfigurationAfterReadyTransitionsToErrorAndRecovers() throw final CompletableFuture errorEvent = new CompletableFuture<>(); final CompletableFuture readyEvent = new CompletableFuture<>(); final CompletableFuture configChangedEvent = new CompletableFuture<>(); - client.on(ProviderEvent.PROVIDER_ERROR, errorEvent::complete); - client.on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, configChangedEvent::complete); + final Consumer errorEventHandler = completingHandler(errorEvent); + final Consumer readyEventHandler = completingHandler(readyEvent); + final Consumer configChangedEventHandler = completingHandler(configChangedEvent); + client.on(ProviderEvent.PROVIDER_ERROR, errorEventHandler); + client.on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, configChangedEventHandler); FeatureFlaggingGateway.dispatch((ServerConfiguration) null); final EventDetails eventDetails = errorEvent.get(EVENT_TIMEOUT_SECONDS, SECONDS); @@ -280,13 +286,16 @@ public void testNullConfigurationAfterReadyTransitionsToErrorAndRecovers() throw assertThat(evalDetails.getValue(), equalTo("default")); assertThat(evalDetails.getErrorCode(), equalTo(ErrorCode.PROVIDER_NOT_READY)); - client.on(ProviderEvent.PROVIDER_READY, readyEvent::complete); + client.on(ProviderEvent.PROVIDER_READY, readyEventHandler); FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); readyEvent.get(EVENT_TIMEOUT_SECONDS, SECONDS); assertThat(client.getProviderState(), equalTo(ProviderState.READY)); FeatureFlaggingGateway.dispatch(mock(ServerConfiguration.class)); configChangedEvent.get(EVENT_TIMEOUT_SECONDS, SECONDS); + verify(errorEventHandler, times(1)).accept(any()); + verify(readyEventHandler, times(1)).accept(any()); + verify(configChangedEventHandler, times(1)).accept(any()); } @Test @@ -457,6 +466,20 @@ private static String initializationState(final Provider provider) throws Except return state.get().toString(); } + @SuppressWarnings("unchecked") + private static Consumer completingHandler( + final CompletableFuture event) { + final Consumer handler = mock(Consumer.class); + doAnswer( + invocation -> { + event.complete(invocation.getArgument(0)); + return null; + }) + .when(handler) + .accept(any()); + return handler; + } + private static FlagEvaluationWriter capturingWriter(final AtomicReference ref) { return new FlagEvaluationWriter() { @Override