Skip to content

Commit 3181066

Browse files
committed
add tests
Signed-off-by: Todd Baert <[email protected]>
1 parent e6e536b commit 3181066

File tree

8 files changed

+202
-10
lines changed

8 files changed

+202
-10
lines changed

pom.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,21 @@
164164
<build>
165165
<plugins>
166166

167+
<plugin>
168+
<groupId>org.codehaus.mojo</groupId>
169+
<artifactId>build-helper-maven-plugin</artifactId>
170+
<version>3.3.0</version>
171+
<executions>
172+
<execution>
173+
<phase>validate</phase>
174+
<id>get-cpu-count</id>
175+
<goals>
176+
<goal>cpu-count</goal>
177+
</goals>
178+
</execution>
179+
</executions>
180+
</plugin>
181+
167182
<plugin>
168183
<groupId>org.cyclonedx</groupId>
169184
<artifactId>cyclonedx-maven-plugin</artifactId>
@@ -231,6 +246,9 @@
231246
<argLine>
232247
${surefireArgLine}
233248
</argLine>
249+
<!-- fork a new JVM to isolate test suites, especially import with singletons -->
250+
<forkCount>${cpu.count}</forkCount>
251+
<reuseForks>false</reuseForks>
234252
<excludes>
235253
<!-- tests to exclude -->
236254
<exclude>${testExclusions}</exclude>

spotbugs-exclusions.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@
99
<Class name="dev.openfeature.sdk.OpenFeatureAPI"/>
1010
<Bug pattern="MS_EXPOSE_REP"/>
1111
</And>
12-
<!-- similarly, client using the singleton doesn't seem bad -->
12+
<!-- evaluation context is mutable if mutable impl is used -->
1313
<And>
1414
<Class name="dev.openfeature.sdk.OpenFeatureClient"/>
1515
<Bug pattern="EI_EXPOSE_REP2"/>
1616
</And>
17+
<And>
18+
<Class name="dev.openfeature.sdk.OpenFeatureAPI"/>
19+
<Bug pattern="EI_EXPOSE_REP2"/>
20+
</And>
21+
1722

1823
<!-- Test class that should be excluded -->
1924
<Match>

src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import dev.openfeature.sdk.internal.AutoCloseableLock;
1010
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
1111
import lombok.Getter;
12-
import lombok.Setter;
1312

1413
/**
1514
* A global singleton which holds base configuration for the OpenFeature library.
@@ -21,7 +20,6 @@ public class OpenFeatureAPI {
2120
@Getter
2221
private FeatureProvider provider;
2322
@Getter
24-
@Setter
2523
private EvaluationContext evaluationContext;
2624
@Getter
2725
private List<Hook> apiHooks;
@@ -58,6 +56,15 @@ public Client getClient(@Nullable String name, @Nullable String version) {
5856
return new OpenFeatureClient(this, name, version);
5957
}
6058

59+
/**
60+
* {@inheritDoc}
61+
*/
62+
public void setEvaluationContext(EvaluationContext evaluationContext) {
63+
try (AutoCloseableLock __ = rwLock.writeLockAutoCloseable()) {
64+
this.evaluationContext = evaluationContext;
65+
}
66+
}
67+
6168
/**
6269
* {@inheritDoc}
6370
*/

src/main/java/dev/openfeature/sdk/OpenFeatureClient.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
1313
import dev.openfeature.sdk.internal.ObjectUtils;
1414
import lombok.Getter;
15-
import lombok.Setter;
1615
import lombok.extern.slf4j.Slf4j;
1716

1817
@Slf4j
@@ -27,10 +26,9 @@ public class OpenFeatureClient implements Client {
2726
@Getter
2827
private final List<Hook> clientHooks;
2928
private final HookSupport hookSupport;
30-
private AutoCloseableReentrantReadWriteLock rwLock = new AutoCloseableReentrantReadWriteLock();
29+
AutoCloseableReentrantReadWriteLock rwLock = new AutoCloseableReentrantReadWriteLock();
3130

3231
@Getter
33-
@Setter
3432
private EvaluationContext evaluationContext;
3533

3634
/**
@@ -49,13 +47,26 @@ public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String name, String vers
4947
this.hookSupport = new HookSupport();
5048
}
5149

50+
/**
51+
* {@inheritDoc}
52+
*/
5253
@Override
5354
public void addHooks(Hook... hooks) {
5455
try (AutoCloseableLock __ = this.rwLock.writeLockAutoCloseable()) {
5556
this.clientHooks.addAll(Arrays.asList(hooks));
5657
}
5758
}
5859

60+
/**
61+
* {@inheritDoc}
62+
*/
63+
@Override
64+
public void setEvaluationContext(EvaluationContext evaluationContext) {
65+
try (AutoCloseableLock __ = rwLock.writeLockAutoCloseable()) {
66+
this.evaluationContext = evaluationContext;
67+
}
68+
}
69+
5970
private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key, T defaultValue,
6071
EvaluationContext ctx, FlagEvaluationOptions options) {
6172
FlagEvaluationOptions flagOptions = ObjectUtils.defaultIfNull(options,
@@ -74,6 +85,7 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
7485
EvaluationContext clientContext;
7586

7687
// lock while getting the provider and hooks
88+
// the retrieval any mutable state on client/API MUST be done in this block.
7789
try (AutoCloseableLock __ = OpenFeatureAPI.rwLock.readLockAutoCloseable();
7890
AutoCloseableLock ___ = this.rwLock.readLockAutoCloseable()) {
7991
provider = ObjectUtils.defaultIfNull(openfeatureApi.getProvider(), () -> {

src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
import static org.mockito.Mockito.times;
77
import static org.mockito.Mockito.verify;
88

9+
import java.util.Arrays;
10+
import java.util.Map;
11+
import java.util.Optional;
12+
913
import org.junit.jupiter.api.Test;
1014

1115
import dev.openfeature.sdk.fixtures.HookFixtures;
1216

13-
import java.util.Arrays;
14-
1517
class DeveloperExperienceTest implements HookFixtures {
1618
transient String flagKey = "mykey";
1719

@@ -90,4 +92,34 @@ class DeveloperExperienceTest implements HookFixtures {
9092
assertEquals(Reason.ERROR.toString(), retval.getReason());
9193
assertFalse(retval.getValue());
9294
}
95+
96+
@Test
97+
void providerLockedPerTransaction() throws InterruptedException {
98+
99+
class MutatingHook implements Hook {
100+
101+
@Override
102+
// change the provider during a before hook - this should not impact the evaluation in progress
103+
public Optional before(HookContext ctx, Map hints) {
104+
OpenFeatureAPI.getInstance().setProvider(new NoOpProvider());
105+
return Optional.empty();
106+
}
107+
}
108+
109+
final String defaultValue = "string-value";
110+
final OpenFeatureAPI api = OpenFeatureAPI.getInstance();
111+
final Client client = api.getClient();
112+
api.setProvider(new DoSomethingProvider());
113+
api.addHooks(new MutatingHook());
114+
115+
// if provider is changed during an evaluation transaction it should proceed with the original provider
116+
String doSomethingValue = client.getStringValue("val", defaultValue);
117+
assertEquals(new StringBuilder(defaultValue).reverse().toString(), doSomethingValue);
118+
119+
api.clearHooks();
120+
121+
// subsequent evaluations should now use new provider set by hook
122+
String noOpValue = client.getStringValue("val", defaultValue);
123+
assertEquals(noOpValue, defaultValue);
124+
}
93125
}

src/test/java/dev/openfeature/sdk/DoSomethingProvider.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
public class DoSomethingProvider implements FeatureProvider {
44

5+
public static final String name = "Something";
56
private EvaluationContext savedContext;
67

78
public EvaluationContext getMergedContext() {
@@ -10,7 +11,7 @@ public EvaluationContext getMergedContext() {
1011

1112
@Override
1213
public Metadata getMetadata() {
13-
return () -> "test";
14+
return () -> name;
1415
}
1516

1617
@Override

src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ private Client _client() {
5353
@Test void provider_metadata() {
5454
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
5555
api.setProvider(new DoSomethingProvider());
56-
assertEquals("test", api.getProviderMetadata().getName());
56+
assertEquals(DoSomethingProvider.name, api.getProviderMetadata().getName());
5757
}
5858

5959
@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.")
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package dev.openfeature.sdk;
2+
3+
import static org.mockito.Mockito.doNothing;
4+
import static org.mockito.Mockito.mock;
5+
import static org.mockito.Mockito.verify;
6+
import static org.mockito.Mockito.when;
7+
8+
import java.util.concurrent.locks.ReentrantReadWriteLock;
9+
10+
import org.junit.jupiter.api.BeforeAll;
11+
import org.junit.jupiter.api.BeforeEach;
12+
import org.junit.jupiter.api.Test;
13+
14+
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
15+
16+
class LockingTest {
17+
18+
private static OpenFeatureAPI api;
19+
private OpenFeatureClient client;
20+
private AutoCloseableReentrantReadWriteLock apiRwLock;
21+
private AutoCloseableReentrantReadWriteLock clientRwLock;
22+
private ReentrantReadWriteLock.ReadLock mockClientReadLock;
23+
private ReentrantReadWriteLock.WriteLock mockClientWriteLock;
24+
private ReentrantReadWriteLock.ReadLock mockApiReadLock;
25+
private ReentrantReadWriteLock.WriteLock mockApiWriteLock;
26+
27+
@BeforeAll
28+
static void beforeAll() {
29+
api = OpenFeatureAPI.getInstance();
30+
}
31+
32+
@BeforeEach
33+
void beforeEach() {
34+
client = (OpenFeatureClient)api.getClient();
35+
36+
// mock the inner read and write locks
37+
mockClientReadLock = mockInnerReadLock();
38+
mockClientWriteLock = mockInnerWriteLock();
39+
mockApiReadLock = mockInnerReadLock();
40+
mockApiWriteLock = mockInnerWriteLock();
41+
42+
// mock the client rwLock
43+
clientRwLock = mock(AutoCloseableReentrantReadWriteLock.class);
44+
when(clientRwLock.readLockAutoCloseable()).thenCallRealMethod();
45+
when(clientRwLock.readLock()).thenReturn(mockClientReadLock);
46+
when(clientRwLock.writeLockAutoCloseable()).thenCallRealMethod();
47+
when(clientRwLock.writeLock()).thenReturn(mockClientWriteLock);
48+
client.rwLock = clientRwLock;
49+
50+
// mock the API rwLock
51+
apiRwLock = mock(AutoCloseableReentrantReadWriteLock.class);
52+
when(apiRwLock.readLockAutoCloseable()).thenCallRealMethod();
53+
when(apiRwLock.readLock()).thenReturn(mockApiReadLock);
54+
when(apiRwLock.writeLockAutoCloseable()).thenCallRealMethod();
55+
when(apiRwLock.writeLock()).thenReturn(mockApiWriteLock);
56+
OpenFeatureAPI.rwLock = apiRwLock;
57+
}
58+
59+
@Test
60+
void evaluationShouldReadLockandReadUnlockClientAndApi() {
61+
client.getBooleanValue("a-key", false);
62+
verify(mockApiReadLock).lock();
63+
verify(mockApiReadLock).unlock();
64+
verify(mockClientReadLock).lock();
65+
verify(mockClientReadLock).unlock();
66+
}
67+
68+
@Test
69+
void addHooksShouldWriteLockAndUnlock() {
70+
client.addHooks(new Hook(){});
71+
verify(mockClientWriteLock).lock();
72+
verify(mockClientWriteLock).unlock();
73+
74+
api.addHooks(new Hook(){});
75+
verify(mockApiWriteLock).lock();
76+
verify(mockApiWriteLock).unlock();
77+
}
78+
79+
@Test
80+
void setContextShouldWriteLockAndUnlock() {
81+
client.setEvaluationContext(new MutableContext());
82+
verify(mockClientWriteLock).lock();
83+
verify(mockClientWriteLock).unlock();
84+
85+
api.setEvaluationContext(new MutableContext());
86+
verify(mockApiWriteLock).lock();
87+
verify(mockApiWriteLock).unlock();
88+
}
89+
90+
@Test
91+
void setProviderShouldWriteLockAndUnlock() {
92+
api.setProvider(new DoSomethingProvider());
93+
verify(mockApiWriteLock).lock();
94+
verify(mockApiWriteLock).unlock();
95+
}
96+
97+
@Test
98+
void clearHooksShouldWriteLockAndUnlock() {
99+
api.clearHooks();
100+
verify(mockApiWriteLock).lock();
101+
verify(mockApiWriteLock).unlock();
102+
}
103+
104+
private static ReentrantReadWriteLock.ReadLock mockInnerReadLock() {
105+
ReentrantReadWriteLock.ReadLock readLockMock = mock(ReentrantReadWriteLock.ReadLock.class);
106+
doNothing().when(readLockMock).lock();
107+
doNothing().when(readLockMock).unlock();
108+
return readLockMock;
109+
}
110+
111+
private static ReentrantReadWriteLock.WriteLock mockInnerWriteLock() {
112+
ReentrantReadWriteLock.WriteLock writeLockMock = mock(ReentrantReadWriteLock.WriteLock.class);
113+
doNothing().when(writeLockMock).lock();
114+
doNothing().when(writeLockMock).unlock();
115+
return writeLockMock;
116+
}
117+
}

0 commit comments

Comments
 (0)