From a6a3386638d3a90f70de91372095e8873d502cb2 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Tue, 2 Dec 2025 16:47:11 +0100 Subject: [PATCH 1/4] add vmlens Signed-off-by: christian.lutnik --- providers/flagd/pom.xml | 27 ++++++ .../providers/flagd/FlagdProviderCT.java | 86 ++++++++++++++++++ .../providers/flagd/FlagdProviderTest.java | 76 ++-------------- .../providers/flagd/FlagdTestUtils.java | 88 +++++++++++++++++++ 4 files changed, 208 insertions(+), 69 deletions(-) create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java diff --git a/providers/flagd/pom.xml b/providers/flagd/pom.xml index c26ff014b..1ae99ab3f 100644 --- a/providers/flagd/pom.xml +++ b/providers/flagd/pom.xml @@ -19,6 +19,7 @@ 1.77.0 3.25.6 + 1.2.22 flagd @@ -175,6 +176,13 @@ 2.0.17 test + + + com.vmlens + api + ${com.vmlens.version} + test + @@ -288,6 +296,25 @@ dev.openfeature.contrib.- + + com.vmlens + vmlens-maven-plugin + ${com.vmlens.version} + + + test + + test + + + + **/*CT.java + + true + + + + diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java new file mode 100644 index 000000000..b02d340e3 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java @@ -0,0 +1,86 @@ +package dev.openfeature.contrib.providers.flagd; + +import com.vmlens.api.AllInterleavings; +import com.vmlens.api.Runner; +import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.Value; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FlagdProviderCT { + private FlagdProvider provider; + + @BeforeEach + void setup() throws Exception { + provider = FlagdTestUtils.createInProcessProvider( + Map.of( + "flag", + new FeatureFlag( + "ENABLED", + "default", + Map.of("default", "a", "other", "b"), + "{\n" + + " \"if\": [\n" + + " {\n" + + " \"ends_with\": [\n" + + " {\n" + + " \"var\": \"email\"\n" + + " },\n" + + " \"@ingen.com\"\n" + + " ]\n" + + " },\n" + + " \"default\",\n" + + " \"other\"\n" + + " ]\n" + + " }", + null + ) + ) + ); + provider.initialize(ImmutableContext.EMPTY); + } +/* + @Test + void concurrentFlagEvaluationsWork() { + var invocationContext = ImmutableContext.EMPTY; + + try (var interleavings = new AllInterleavings("Concurrent Flag evaluations")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> assertEquals("a", + provider.getStringEvaluation("flag", "z", invocationContext).getValue()), + () -> assertEquals("a", + provider.getStringEvaluation("flag", "z", invocationContext).getValue()) + ); + } + } + }*/ + + @Test + void flagEvaluationsWhileSettingContextWork() { + var invocationContext = ImmutableContext.EMPTY; + + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + var client = OpenFeatureAPI.getInstance().getClient(); + + var context = new ImmutableContext(Map.of("email", new Value("someone@ingen.com"))); + + try (var interleavings = new AllInterleavings("Concurrently setting client context and evaluating a Flag")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> assertTrue(List.of("a", "b") + .contains(provider.getStringEvaluation("flag", "z", invocationContext).getValue())), + () -> client.setEvaluationContext(context) + ); + } + } + } +} diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java index 115887002..4c39924e4 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java @@ -152,7 +152,7 @@ void resolvers_call_grpc_service_and_return_details() { .thenReturn(objectResponse); ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); assertTrue(booleanDetails.getValue()); @@ -230,7 +230,7 @@ void zero_value() { ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); assertEquals(false, booleanDetails.getValue()); @@ -294,7 +294,7 @@ void test_metadata_from_grpc_response() { .thenReturn(booleanResponse); ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); // when FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); @@ -376,7 +376,7 @@ void context_is_parsed_and_passed_to_grpc_service() { .thenReturn(booleanResponse); ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); final MutableContext context = new MutableContext("MY_TARGETING_KEY"); context.add(BOOLEAN_ATTR_KEY, BOOLEAN_ATTR_VALUE); @@ -415,7 +415,7 @@ void null_context_handling() { .build()); ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); // then final Boolean evaluation = api.getClient().getBooleanValue(flagA, defaultVariant, context); @@ -439,7 +439,7 @@ void reason_mapped_correctly_if_unknown() { .thenReturn(badReasonResponse); ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY, false, new MutableContext()); @@ -498,7 +498,7 @@ private void doResolversCacheResponses(String reason, Boolean eventStreamAlive, .thenReturn(objectResponse); ChannelConnector grpc = mock(ChannelConnector.class); - FlagdProvider provider = createProvider(grpc, serviceBlockingStubMock); + FlagdProvider provider = FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock); // provider.setState(eventStreamAlive); // caching only available when event // stream is alive @@ -676,66 +676,4 @@ void updatesSyncMetadataWithCallback() throws Exception { assertEquals(val, contextFromHook.get().getValue(key).asString()); } } - - // test helper - // create provider with given grpc provider and state supplier - private FlagdProvider createProvider(ChannelConnector connector, ServiceBlockingStub mockBlockingStub) { - final Cache cache = new Cache("lru", 5); - final ServiceStub mockStub = mock(ServiceStub.class); - - return createProvider(connector, cache, mockStub, mockBlockingStub); - } - - // create provider with given grpc provider, cache and state supplier - private FlagdProvider createProvider( - ChannelConnector connector, Cache cache, ServiceStub mockStub, ServiceBlockingStub mockBlockingStub) { - final FlagdOptions flagdOptions = FlagdOptions.builder().build(); - final RpcResolver grpcResolver = new RpcResolver(flagdOptions, cache, (connectionEvent) -> {}); - - try { - Field resolver = RpcResolver.class.getDeclaredField("connector"); - resolver.setAccessible(true); - resolver.set(grpcResolver, connector); - - Field stub = RpcResolver.class.getDeclaredField("stub"); - stub.setAccessible(true); - stub.set(grpcResolver, mockStub); - - Field blockingStub = RpcResolver.class.getDeclaredField("blockingStub"); - blockingStub.setAccessible(true); - blockingStub.set(grpcResolver, mockBlockingStub); - - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - final FlagdProvider provider = new FlagdProvider(grpcResolver, true); - return provider; - } - - // Create an in process provider - private FlagdProvider createInProcessProvider() { - - final FlagdOptions flagdOptions = FlagdOptions.builder() - .resolverType(Config.Resolver.IN_PROCESS) - .deadline(1000) - .build(); - final FlagdProvider provider = new FlagdProvider(flagdOptions); - final MockStorage mockStorage = new MockStorage( - new HashMap(), - new LinkedBlockingQueue(Arrays.asList(new StorageStateChange(StorageState.OK)))); - - try { - final Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver"); - flagResolver.setAccessible(true); - final Resolver resolver = (Resolver) flagResolver.get(provider); - - final Field flagStore = InProcessResolver.class.getDeclaredField("flagStore"); - flagStore.setAccessible(true); - flagStore.set(resolver, mockStorage); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - - return provider; - } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java new file mode 100644 index 000000000..fdcb0adbc --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java @@ -0,0 +1,88 @@ +package dev.openfeature.contrib.providers.flagd; + +import dev.openfeature.contrib.providers.flagd.resolver.Resolver; +import dev.openfeature.contrib.providers.flagd.resolver.common.ChannelConnector; +import dev.openfeature.contrib.providers.flagd.resolver.process.InProcessResolver; +import dev.openfeature.contrib.providers.flagd.resolver.process.MockStorage; +import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageState; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageStateChange; +import dev.openfeature.contrib.providers.flagd.resolver.rpc.RpcResolver; +import dev.openfeature.contrib.providers.flagd.resolver.rpc.cache.Cache; +import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; + +import static org.mockito.Mockito.mock; + +class FlagdTestUtils { + // test helper + // create provider with given grpc provider and state supplier + static FlagdProvider createProvider(ChannelConnector connector, ServiceGrpc.ServiceBlockingStub mockBlockingStub) { + final Cache cache = new Cache("lru", 5); + final ServiceGrpc.ServiceStub mockStub = mock(ServiceGrpc.ServiceStub.class); + + return createProvider(connector, cache, mockStub, mockBlockingStub); + } + + // create provider with given grpc provider, cache and state supplier + static FlagdProvider createProvider( + ChannelConnector connector, Cache cache, ServiceGrpc.ServiceStub mockStub, + ServiceGrpc.ServiceBlockingStub mockBlockingStub) { + final FlagdOptions flagdOptions = FlagdOptions.builder().build(); + final RpcResolver grpcResolver = new RpcResolver(flagdOptions, cache, (connectionEvent) -> {}); + + try { + Field resolver = RpcResolver.class.getDeclaredField("connector"); + resolver.setAccessible(true); + resolver.set(grpcResolver, connector); + + Field stub = RpcResolver.class.getDeclaredField("stub"); + stub.setAccessible(true); + stub.set(grpcResolver, mockStub); + + Field blockingStub = RpcResolver.class.getDeclaredField("blockingStub"); + blockingStub.setAccessible(true); + blockingStub.set(grpcResolver, mockBlockingStub); + + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + final FlagdProvider provider = new FlagdProvider(grpcResolver, true); + return provider; + } + + static FlagdProvider createInProcessProvider(Map mockFlags) { + final FlagdOptions flagdOptions = FlagdOptions.builder() + .resolverType(Config.Resolver.IN_PROCESS) + .offlineFlagSourcePath("") // this is new + .deadline(1000) + .build(); + final FlagdProvider provider = new FlagdProvider(flagdOptions); + final MockStorage mockStorage = new MockStorage( + mockFlags, + new LinkedBlockingQueue<>(Arrays.asList(new StorageStateChange(StorageState.OK)))); + + try { + final Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver"); + flagResolver.setAccessible(true); + final Resolver resolver = (Resolver) flagResolver.get(provider); + + final Field flagStore = InProcessResolver.class.getDeclaredField("flagStore"); + flagStore.setAccessible(true); + flagStore.set(resolver, mockStorage); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + + return provider; + } + + static FlagdProvider createInProcessProvider() { + return createInProcessProvider(Collections.emptyMap()); + } +} From 7a9ccca14ef7c7c7960b225e1355aac6f831827d Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Thu, 4 Dec 2025 10:35:09 +0100 Subject: [PATCH 2/4] add vmlens Signed-off-by: christian.lutnik --- .../contrib/providers/flagd/FlagdProviderCT.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java index b02d340e3..dcf447137 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java @@ -1,19 +1,18 @@ package dev.openfeature.contrib.providers.flagd; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.vmlens.api.AllInterleavings; import com.vmlens.api.Runner; import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.OpenFeatureAPI; import dev.openfeature.sdk.Value; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import java.util.Collections; import java.util.List; import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; class FlagdProviderCT { private FlagdProvider provider; @@ -47,7 +46,7 @@ void setup() throws Exception { ); provider.initialize(ImmutableContext.EMPTY); } -/* + @Test void concurrentFlagEvaluationsWork() { var invocationContext = ImmutableContext.EMPTY; @@ -62,7 +61,7 @@ void concurrentFlagEvaluationsWork() { ); } } - }*/ + } @Test void flagEvaluationsWhileSettingContextWork() { From 13f1c4c575a419e3cb5cdedbd917715abeab26b9 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 10 Dec 2025 11:57:34 +0100 Subject: [PATCH 3/4] minor improvements Signed-off-by: christian.lutnik --- .../resolver/process/InProcessResolver.java | 12 +++---- .../resolver/process/model/FeatureFlag.java | 5 +-- .../providers/flagd/FlagdProviderCT.java | 35 +++++++++++++++---- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java index e54c938cf..86ae34604 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java @@ -166,7 +166,7 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC return ProviderEvaluation.builder() .errorMessage("flag: " + key + " not found") .errorCode(ErrorCode.FLAG_NOT_FOUND) - .flagMetadata(getFlagMetadata(storageQueryResult)) + .flagMetadata(getFlagMetadata(storageQueryResult, scope)) .build(); } @@ -175,7 +175,7 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC return ProviderEvaluation.builder() .errorMessage("flag: " + key + " is disabled") .errorCode(ErrorCode.FLAG_NOT_FOUND) - .flagMetadata(getFlagMetadata(storageQueryResult)) + .flagMetadata(getFlagMetadata(storageQueryResult, scope)) .build(); } @@ -210,7 +210,7 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC .reason(Reason.ERROR.toString()) .errorCode(ErrorCode.FLAG_NOT_FOUND) .errorMessage("Flag '" + key + "' has no default variant defined, will use code default") - .flagMetadata(getFlagMetadata(storageQueryResult)) + .flagMetadata(getFlagMetadata(storageQueryResult, scope)) .build(); } @@ -235,11 +235,11 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC .value((T) value) .variant(resolvedVariant) .reason(reason) - .flagMetadata(getFlagMetadata(storageQueryResult)) + .flagMetadata(getFlagMetadata(storageQueryResult, scope)) .build(); } - private ImmutableMetadata getFlagMetadata(StorageQueryResult storageQueryResult) { + private static ImmutableMetadata getFlagMetadata(StorageQueryResult storageQueryResult, String scope) { ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder = ImmutableMetadata.builder(); for (Map.Entry entry : storageQueryResult.getFlagSetMetadata().entrySet()) { @@ -260,7 +260,7 @@ private ImmutableMetadata getFlagMetadata(StorageQueryResult storageQueryResult) return metadataBuilder.build(); } - private void addEntryToMetadataBuilder( + private static void addEntryToMetadataBuilder( ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder, String key, Object value) { if (value instanceof Number) { if (value instanceof Long) { diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java index 2eaf2ed87..e8aaedafe 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import lombok.EqualsAndHashCode; @@ -39,7 +40,7 @@ public FeatureFlag( this.variants = variants; this.targeting = targeting; if (metadata == null) { - this.metadata = new HashMap<>(); + this.metadata = Collections.emptyMap(); } else { this.metadata = metadata; } @@ -51,7 +52,7 @@ public FeatureFlag(String state, String defaultVariant, Map vari this.defaultVariant = defaultVariant; this.variants = variants; this.targeting = targeting; - this.metadata = new HashMap<>(); + this.metadata = Collections.emptyMap(); } /** Get targeting rule of the flag. */ diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java index dcf447137..e323f79e2 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCT.java @@ -24,8 +24,8 @@ void setup() throws Exception { "flag", new FeatureFlag( "ENABLED", - "default", - Map.of("default", "a", "other", "b"), + "a", + Map.of("a", "a", "b", "b", "c", "c"), "{\n" + " \"if\": [\n" + " {\n" @@ -36,8 +36,8 @@ void setup() throws Exception { + " \"@ingen.com\"\n" + " ]\n" + " },\n" - + " \"default\",\n" - + " \"other\"\n" + + " \"b\",\n" + + " \"c\"\n" + " ]\n" + " }", null @@ -54,9 +54,9 @@ void concurrentFlagEvaluationsWork() { try (var interleavings = new AllInterleavings("Concurrent Flag evaluations")) { while (interleavings.hasNext()) { Runner.runParallel( - () -> assertEquals("a", + () -> assertEquals("c", provider.getStringEvaluation("flag", "z", invocationContext).getValue()), - () -> assertEquals("a", + () -> assertEquals("c", provider.getStringEvaluation("flag", "z", invocationContext).getValue()) ); } @@ -75,11 +75,32 @@ void flagEvaluationsWhileSettingContextWork() { try (var interleavings = new AllInterleavings("Concurrently setting client context and evaluating a Flag")) { while (interleavings.hasNext()) { Runner.runParallel( - () -> assertTrue(List.of("a", "b") + () -> assertTrue(List.of("b", "c") .contains(provider.getStringEvaluation("flag", "z", invocationContext).getValue())), () -> client.setEvaluationContext(context) ); } } } + + @Test + void settingDifferentContextsWorks() { + + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + var client = OpenFeatureAPI.getInstance().getClient(); + + var clientContext = new ImmutableContext(Map.of("email", new Value("someone@ingen.com"))); + var apiContext = new ImmutableContext(Map.of("email", new Value("someone.else@test.com"))); + + try (var interleavings = new AllInterleavings("Concurrently setting client and api context")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> client.setEvaluationContext(clientContext), + () -> OpenFeatureAPI.getInstance().setEvaluationContext(apiContext), + () -> assertTrue(List.of("b", "c") + .contains(provider.getStringEvaluation("flag", "z", ImmutableContext.EMPTY).getValue())) + ); + } + } + } } From 881e5a1fa413beaec57bdd84433f1c8e078bfe27 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 17 Dec 2025 12:54:40 +0100 Subject: [PATCH 4/4] improve tests Signed-off-by: christian.lutnik --- .../flagd/FlagdProviderSyncResources.java | 2 +- .../flagd/FlagdProviderSyncResourcesCT.java | 177 ++++++++++++++++++ .../flagd/FlagdProviderSyncResourcesTest.java | 146 --------------- 3 files changed, 178 insertions(+), 147 deletions(-) create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCT.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java index 03d444528..205b6214c 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java @@ -31,7 +31,7 @@ public void setEnrichedContext(EvaluationContext context) { * @return true iff this was the first call to {@code initialize()} */ public synchronized boolean initialize() { - if (this.initialized) { + if (this.initialized || this.isShutDown) { return false; } this.initialized = true; diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCT.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCT.java new file mode 100644 index 000000000..c1cae0492 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCT.java @@ -0,0 +1,177 @@ +package dev.openfeature.contrib.providers.flagd; + +import com.vmlens.api.AllInterleavings; +import com.vmlens.api.Runner; +import dev.openfeature.sdk.exceptions.GeneralError; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +class FlagdProviderSyncResourcesCT { + private static final long MAX_TIME_TOLERANCE = 20; + + private FlagdProviderSyncResources flagdProviderSyncResources; + + @BeforeEach + void setUp() { + flagdProviderSyncResources = new FlagdProviderSyncResources(); + } + + @Timeout(2) + @Test + void waitForInitialization_failsWhenDeadlineElapses() { + Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(2)); + } + + @Timeout(2) + @Test + void waitForInitialization_waitsApproxForDeadline() { + final AtomicLong start = new AtomicLong(); + final AtomicLong end = new AtomicLong(); + final long deadline = 45; + + start.set(System.currentTimeMillis()); + Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(deadline)); + end.set(System.currentTimeMillis()); + + final long elapsed = end.get() - start.get(); + // should wait at least for the deadline + Assertions.assertTrue(elapsed >= deadline); + // should not wait much longer than the deadline + Assertions.assertTrue(elapsed < deadline + MAX_TIME_TOLERANCE); + } + + @Timeout(2) + @Test + void interruptingWaitingThread_isIgnored() throws InterruptedException { + final AtomicBoolean isWaiting = new AtomicBoolean(); + final long deadline = 500; + Thread waitingThread = new Thread(() -> { + long start = System.currentTimeMillis(); + isWaiting.set(true); + Assertions.assertThrows( + GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(deadline)); + + long end = System.currentTimeMillis(); + long duration = end - start; + // even though thread was interrupted, it still waited for the deadline + Assertions.assertTrue(duration >= deadline); + Assertions.assertTrue(duration < deadline + MAX_TIME_TOLERANCE); + }); + waitingThread.start(); + + while (!isWaiting.get()) { + Thread.yield(); + } + + Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime + + for (int i = 0; i < 50; i++) { + waitingThread.interrupt(); + Thread.sleep(10); + } + + waitingThread.join(); + } + + @Timeout(5) + @Test + void callingInitialize_wakesUpWaitingThread() { + try (var interleavings = new AllInterleavings("calling initialize() wakes up waiting thread")) { + while (interleavings.hasNext()) { + final var startTime = new AtomicLong(); + final var endTime = new AtomicLong(); + Runner.runParallel( + () -> { + flagdProviderSyncResources.waitForInitialization(10000); + endTime.set(System.currentTimeMillis()); + Assertions.assertTrue(flagdProviderSyncResources.isInitialized()); + }, + () -> { + startTime.set(System.currentTimeMillis()); + flagdProviderSyncResources.initialize(); + } + ); + + Assertions.assertTrue(endTime.get() - startTime.get() <= MAX_TIME_TOLERANCE, () -> + "Expected waiting thread to be released shortly after initialization, but waited for " + + (endTime.get() - startTime.get()) + "ms" + ); + } + } + } + + @Timeout(5) + @Test + void callingShutdown_wakesUpWaitingThreadWithException() { + try (var interleavings = new AllInterleavings("calling shutdown() wakes up waiting thread with exception")) { + while (interleavings.hasNext()) { + final var startTime = new AtomicLong(); + final var endTime = new AtomicLong(); + Runner.runParallel( + () -> { + Assertions.assertThrows( + IllegalStateException.class, + () -> flagdProviderSyncResources.waitForInitialization(10000)); + Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); + }, + () -> { + startTime.set(System.currentTimeMillis()); + flagdProviderSyncResources.shutdown(); + } + ); + + Assertions.assertTrue(endTime.get() - startTime.get() <= MAX_TIME_TOLERANCE, () -> + "Expected waiting thread to be released shortly after initialization, but waited for " + + (endTime.get() - startTime.get()) + "ms" + ); + } + } + } + + @Timeout(5) + @Test + void concurrentInitializesWork() { + try (var interleavings = new AllInterleavings("concurrent initialize() calls work")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> flagdProviderSyncResources.initialize(), + () -> flagdProviderSyncResources.initialize() + ); + Assertions.assertTrue(flagdProviderSyncResources.isInitialized()); + } + } + } + + @Timeout(5) + @Test + void concurrentInitializeAndShutdownShutsDownWork() { + try (var interleavings = new AllInterleavings("concurrent initialize() calls work")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> flagdProviderSyncResources.initialize(), + () -> flagdProviderSyncResources.shutdown() + ); + Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); + } + } + } + + @Timeout(2) + @Test + void waitForInitializationAfterCallingInitialize_returnsInstantly() { + flagdProviderSyncResources.initialize(); + long start = System.currentTimeMillis(); + flagdProviderSyncResources.waitForInitialization(10000); + long end = System.currentTimeMillis(); + // do not use MAX_TIME_TOLERANCE here, this should happen faster than that + Assertions.assertTrue(start + 1 >= end); + } +} 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 deleted file mode 100644 index 6258dad37..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java +++ /dev/null @@ -1,146 +0,0 @@ -package dev.openfeature.contrib.providers.flagd; - -import dev.openfeature.sdk.exceptions.GeneralError; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; - -class FlagdProviderSyncResourcesTest { - private static final long MAX_TIME_TOLERANCE = 20; - - private FlagdProviderSyncResources flagdProviderSyncResources; - - @BeforeEach - void setUp() { - flagdProviderSyncResources = new FlagdProviderSyncResources(); - } - - @Timeout(2) - @Test - void waitForInitialization_failsWhenDeadlineElapses() { - Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(2)); - } - - @Timeout(2) - @Test - void waitForInitialization_waitsApproxForDeadline() { - final AtomicLong start = new AtomicLong(); - final AtomicLong end = new AtomicLong(); - final long deadline = 45; - - start.set(System.currentTimeMillis()); - Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(deadline)); - end.set(System.currentTimeMillis()); - - final long elapsed = end.get() - start.get(); - // should wait at least for the deadline - Assertions.assertTrue(elapsed >= deadline); - // should not wait much longer than the deadline - Assertions.assertTrue(elapsed < deadline + MAX_TIME_TOLERANCE); - } - - @Timeout(2) - @Test - void interruptingWaitingThread_isIgnored() throws InterruptedException { - final AtomicBoolean isWaiting = new AtomicBoolean(); - final long deadline = 500; - Thread waitingThread = new Thread(() -> { - long start = System.currentTimeMillis(); - isWaiting.set(true); - Assertions.assertThrows( - GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(deadline)); - - long end = System.currentTimeMillis(); - long duration = end - start; - // even though thread was interrupted, it still waited for the deadline - Assertions.assertTrue(duration >= deadline); - Assertions.assertTrue(duration < deadline + MAX_TIME_TOLERANCE); - }); - waitingThread.start(); - - while (!isWaiting.get()) { - Thread.yield(); - } - - Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime - - for (int i = 0; i < 50; i++) { - waitingThread.interrupt(); - Thread.sleep(10); - } - - waitingThread.join(); - } - - @Timeout(2) - @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; - successfulTest.set(duration < MAX_TIME_TOLERANCE * 2); - }); - waitingThread.start(); - - while (!isWaiting.get()) { - Thread.yield(); - } - - Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime - - 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); - Assertions.assertThrows( - IllegalStateException.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); - - long end = System.currentTimeMillis(); - long duration = end - start; - successfulTest.set(duration < MAX_TIME_TOLERANCE * 2); - }); - waitingThread.start(); - - while (!isWaiting.get()) { - Thread.yield(); - } - - Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime - - flagdProviderSyncResources.shutdown(); - - waitingThread.join(); - - Assertions.assertTrue(successfulTest.get()); - } - - @Timeout(2) - @Test - void waitForInitializationAfterCallingInitialize_returnsInstantly() { - flagdProviderSyncResources.initialize(); - long start = System.currentTimeMillis(); - flagdProviderSyncResources.waitForInitialization(10000); - long end = System.currentTimeMillis(); - // do not use MAX_TIME_TOLERANCE here, this should happen faster than that - Assertions.assertTrue(start + 1 >= end); - } -}