Skip to content

Commit 4b3c267

Browse files
committed
test: improve gherkin test suite only relying on the newest version, with data loading
Signed-off-by: Simon Schrottner <[email protected]>
1 parent 532ad2f commit 4b3c267

File tree

13 files changed

+341
-86
lines changed

13 files changed

+341
-86
lines changed

pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,20 @@
168168
<scope>test</scope>
169169
</dependency>
170170

171+
<dependency>
172+
<groupId>com.fasterxml.jackson.core</groupId>
173+
<artifactId>jackson-databind</artifactId>
174+
<version>2.19.2</version>
175+
<scope>test</scope>
176+
</dependency>
177+
178+
<dependency>
179+
<groupId>dev.cel</groupId>
180+
<artifactId>cel</artifactId>
181+
<version>0.10.1</version>
182+
<scope>test</scope>
183+
</dependency>
184+
171185
</dependencies>
172186

173187
<dependencyManagement>

src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,21 @@ private <T> ProviderEvaluation<T> getEvaluation(
138138
}
139139
Flag<?> flag = flags.get(key);
140140
if (flag == null) {
141-
throw new FlagNotFoundError("flag " + key + "not found");
141+
throw new FlagNotFoundError("flag " + key + " not found");
142142
}
143143
T value;
144+
Reason reason = Reason.STATIC;
144145
if (flag.getContextEvaluator() != null) {
145-
value = (T) flag.getContextEvaluator().evaluate(flag, evaluationContext);
146+
try {
147+
value = (T) flag.getContextEvaluator().evaluate(flag, evaluationContext);
148+
reason = Reason.TARGETING_MATCH;
149+
} catch (Exception e) {
150+
value = null;
151+
}
152+
if (value == null) {
153+
value = (T) flag.getVariants().get(flag.getDefaultVariant());
154+
reason = Reason.DEFAULT;
155+
}
146156
} else if (!expectedType.isInstance(flag.getVariants().get(flag.getDefaultVariant()))) {
147157
throw new TypeMismatchError("flag " + key + "is not of expected type");
148158
} else {
@@ -151,7 +161,7 @@ private <T> ProviderEvaluation<T> getEvaluation(
151161
return ProviderEvaluation.<T>builder()
152162
.value(value)
153163
.variant(flag.getDefaultVariant())
154-
.reason(Reason.STATIC.toString())
164+
.reason(reason.toString())
155165
.flagMetadata(flag.getFlagMetadata())
156166
.build();
157167
}

src/test/java/dev/openfeature/sdk/e2e/EvaluationTest.java renamed to src/test/java/dev/openfeature/sdk/e2e/GherkinSpecTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;
66

77
import org.junit.platform.suite.api.ConfigurationParameter;
8+
import org.junit.platform.suite.api.ExcludeTags;
89
import org.junit.platform.suite.api.IncludeEngines;
910
import org.junit.platform.suite.api.SelectDirectories;
1011
import org.junit.platform.suite.api.Suite;
@@ -15,4 +16,5 @@
1516
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
1617
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.sdk.e2e.steps")
1718
@ConfigurationParameter(key = OBJECT_FACTORY_PROPERTY_NAME, value = "io.cucumber.picocontainer.PicoFactory")
18-
public class EvaluationTest {}
19+
@ExcludeTags({"deprecated"})
20+
public class GherkinSpecTest {}

src/test/java/dev/openfeature/sdk/e2e/steps/ContextSteps.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import dev.openfeature.sdk.Hook;
99
import dev.openfeature.sdk.HookContext;
1010
import dev.openfeature.sdk.ImmutableContext;
11+
import dev.openfeature.sdk.ImmutableStructure;
12+
import dev.openfeature.sdk.MutableContext;
1113
import dev.openfeature.sdk.OpenFeatureAPI;
1214
import dev.openfeature.sdk.ThreadLocalTransactionContextPropagator;
1315
import dev.openfeature.sdk.Value;
@@ -101,4 +103,31 @@ public void contextEntriesForEachLevelFromAPILevelDownToTheLevelWithKeyAndValue(
101103
}
102104
}
103105
}
106+
107+
@Given("a context containing a key {string} with null value")
108+
public void a_context_containing_a_key_with_null_value(String key)
109+
throws ClassNotFoundException, InstantiationException {
110+
a_context_containing_a_key_with_type_and_with_value(key, "String", null);
111+
}
112+
113+
@Given("a context containing a key {string}, with type {string} and with value {string}")
114+
public void a_context_containing_a_key_with_type_and_with_value(String key, String type, String value)
115+
throws ClassNotFoundException, InstantiationException {
116+
Map<String, Value> map = state.context.asMap();
117+
map.put(key, new Value(value));
118+
state.context = new MutableContext(state.context.getTargetingKey(), map);
119+
}
120+
121+
@Given("a context containing a targeting key with value {string}")
122+
public void a_context_containing_a_targeting_key_with_value(String string) {
123+
state.context.setTargetingKey(string);
124+
}
125+
126+
@Given("a context containing a nested property with outer key {string} and inner key {string}, with value {string}")
127+
public void a_context_containing_a_nested_property_with_outer_key_and_inner_key_with_value(
128+
String outer, String inner, String value) {
129+
Map<String, Value> innerMap = new HashMap<>();
130+
innerMap.put(inner, new Value(value));
131+
state.context.add(outer, new ImmutableStructure(innerMap));
132+
}
104133
}

src/test/java/dev/openfeature/sdk/e2e/steps/FlagStepDefinitions.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static org.assertj.core.api.Assertions.assertThat;
44

5+
import dev.openfeature.sdk.ErrorCode;
56
import dev.openfeature.sdk.FlagEvaluationDetails;
67
import dev.openfeature.sdk.ImmutableMetadata;
78
import dev.openfeature.sdk.Value;
@@ -63,6 +64,11 @@ public void the_resolved_details_value_should_be(String value) {
6364
assertThat(state.evaluation.getValue()).isEqualTo(Utils.convert(value, state.flag.type));
6465
}
6566

67+
@Then("the flag key should be {string}")
68+
public void the_flag_key_should_be(String key) {
69+
assertThat(state.evaluation.getFlagKey()).isEqualTo(key);
70+
}
71+
6672
@Then("the reason should be {string}")
6773
public void the_reason_should_be(String reason) {
6874
assertThat(state.evaluation.getReason()).isEqualTo(reason);
@@ -73,6 +79,20 @@ public void the_variant_should_be(String variant) {
7379
assertThat(state.evaluation.getVariant()).isEqualTo(variant);
7480
}
7581

82+
@Then("the error-code should be {string}")
83+
public void the_error_code_should_be(String errorCode) {
84+
if (errorCode.isEmpty()) {
85+
assertThat(state.evaluation.getErrorCode()).isNull();
86+
} else {
87+
assertThat(state.evaluation.getErrorCode()).isEqualTo(ErrorCode.valueOf(errorCode));
88+
}
89+
}
90+
91+
@Then("the error message should contain {string}")
92+
public void the_error_error_message_should_contain(String errorCode) {
93+
assertThat(state.evaluation.getErrorMessage()).contains(errorCode);
94+
}
95+
7696
@Then("the resolved metadata value \"{}\" with type \"{}\" should be \"{}\"")
7797
public void theResolvedMetadataValueShouldBe(String key, String type, String value)
7898
throws NoSuchFieldException, IllegalAccessException {

src/test/java/dev/openfeature/sdk/e2e/steps/StepDefinitions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.Map;
2222
import lombok.SneakyThrows;
2323

24+
@Deprecated
2425
public class StepDefinitions {
2526

2627
private static Client client;
Lines changed: 34 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
package dev.openfeature.sdk.testutils;
22

3-
import static dev.openfeature.sdk.Structure.mapToStructure;
4-
5-
import com.google.common.collect.ImmutableMap;
3+
import com.fasterxml.jackson.core.StreamReadFeature;
4+
import com.fasterxml.jackson.core.type.TypeReference;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import com.fasterxml.jackson.databind.module.SimpleModule;
67
import dev.openfeature.sdk.ImmutableMetadata;
7-
import dev.openfeature.sdk.Value;
8+
import dev.openfeature.sdk.providers.memory.ContextEvaluator;
89
import dev.openfeature.sdk.providers.memory.Flag;
9-
import java.util.HashMap;
10+
import dev.openfeature.sdk.testutils.jackson.ContextEvaluatorDeserializer;
11+
import dev.openfeature.sdk.testutils.jackson.ImmutableMetadataDeserializer;
12+
import dev.openfeature.sdk.testutils.jackson.InMemoryFlagMixin;
13+
import java.io.IOException;
14+
import java.nio.file.Paths;
1015
import java.util.Map;
1116
import lombok.experimental.UtilityClass;
17+
import lombok.extern.slf4j.Slf4j;
1218

1319
/**
1420
* Test flags utils.
1521
*/
22+
@Slf4j
1623
@UtilityClass
1724
public class TestFlagsUtils {
1825

@@ -31,81 +38,27 @@ public class TestFlagsUtils {
3138
* @return map of flags
3239
*/
3340
public static Map<String, Flag<?>> buildFlags() {
34-
Map<String, Flag<?>> flags = new HashMap<>();
35-
flags.put(
36-
BOOLEAN_FLAG_KEY,
37-
Flag.builder()
38-
.variant("on", true)
39-
.variant("off", false)
40-
.defaultVariant("on")
41-
.build());
42-
flags.put(
43-
STRING_FLAG_KEY,
44-
Flag.builder()
45-
.variant("greeting", "hi")
46-
.variant("parting", "bye")
47-
.defaultVariant("greeting")
48-
.build());
49-
flags.put(
50-
INT_FLAG_KEY,
51-
Flag.builder()
52-
.variant("one", 1)
53-
.variant("ten", 10)
54-
.defaultVariant("ten")
55-
.build());
56-
flags.put(
57-
FLOAT_FLAG_KEY,
58-
Flag.builder()
59-
.variant("tenth", 0.1)
60-
.variant("half", 0.5)
61-
.defaultVariant("half")
62-
.build());
63-
flags.put(
64-
OBJECT_FLAG_KEY,
65-
Flag.builder()
66-
.variant("empty", new HashMap<>())
67-
.variant(
68-
"template",
69-
new Value(mapToStructure(ImmutableMap.of(
70-
"showImages", new Value(true),
71-
"title", new Value("Check out these pics!"),
72-
"imagesPerPage", new Value(100)))))
73-
.defaultVariant("template")
74-
.build());
75-
flags.put(
76-
CONTEXT_AWARE_FLAG_KEY,
77-
Flag.<String>builder()
78-
.variant("internal", "INTERNAL")
79-
.variant("external", "EXTERNAL")
80-
.defaultVariant("external")
81-
.contextEvaluator((flag, evaluationContext) -> {
82-
if (new Value(false).equals(evaluationContext.getValue("customer"))) {
83-
return (String) flag.getVariants().get("internal");
84-
} else {
85-
return (String) flag.getVariants().get(flag.getDefaultVariant());
86-
}
87-
})
88-
.build());
89-
flags.put(
90-
WRONG_FLAG_KEY,
91-
Flag.builder()
92-
.variant("one", "uno")
93-
.variant("two", "dos")
94-
.defaultVariant("one")
95-
.build());
96-
flags.put(
97-
METADATA_FLAG_KEY,
98-
Flag.builder()
99-
.variant("on", true)
100-
.variant("off", false)
101-
.defaultVariant("on")
102-
.flagMetadata(ImmutableMetadata.builder()
103-
.addString("string", "1.0.2")
104-
.addInteger("integer", 2)
105-
.addBoolean("boolean", true)
106-
.addDouble("float", 0.1d)
107-
.build())
108-
.build());
109-
return flags;
41+
ObjectMapper objectMapper = new ObjectMapper();
42+
objectMapper.configure(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION.mappedFeature(), true);
43+
objectMapper.addMixIn(Flag.class, InMemoryFlagMixin.class);
44+
objectMapper.addMixIn(Flag.FlagBuilder.class, InMemoryFlagMixin.FlagBuilderMixin.class);
45+
46+
SimpleModule module = new SimpleModule();
47+
module.addDeserializer(ImmutableMetadata.class, new ImmutableMetadataDeserializer());
48+
module.addDeserializer(ContextEvaluator.class, new ContextEvaluatorDeserializer());
49+
objectMapper.registerModule(module);
50+
51+
Map<String, Flag<?>> flagsJson;
52+
try {
53+
flagsJson = objectMapper.readValue(
54+
Paths.get("spec/specification/assets/gherkin/test-flags.json")
55+
.toFile(),
56+
new TypeReference<>() {});
57+
58+
} catch (IOException e) {
59+
throw new RuntimeException(e);
60+
}
61+
62+
return flagsJson;
11063
}
11164
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package dev.openfeature.sdk.testutils.jackson;
2+
3+
import dev.cel.common.types.SimpleType;
4+
import dev.cel.compiler.CelCompiler;
5+
import dev.cel.compiler.CelCompilerFactory;
6+
import dev.cel.runtime.CelRuntime;
7+
import dev.cel.runtime.CelRuntimeFactory;
8+
import dev.openfeature.sdk.EvaluationContext;
9+
import dev.openfeature.sdk.providers.memory.ContextEvaluator;
10+
import dev.openfeature.sdk.providers.memory.Flag;
11+
import java.util.HashMap;
12+
import java.util.Map;
13+
14+
public class CelContextEvaluator<T> implements ContextEvaluator<T> {
15+
private final CelRuntime.Program program;
16+
17+
public CelContextEvaluator(String expression) {
18+
try {
19+
CelRuntime celRuntime =
20+
CelRuntimeFactory.standardCelRuntimeBuilder().build();
21+
CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder()
22+
.addVar("customer", SimpleType.STRING)
23+
.addVar("email", SimpleType.STRING)
24+
.addVar("dummy", SimpleType.STRING)
25+
.setResultType(SimpleType.STRING)
26+
// Add other variables you expect
27+
.build();
28+
29+
var ast = celCompiler.compile(expression).getAst();
30+
this.program = celRuntime.createProgram(ast);
31+
} catch (Exception e) {
32+
throw new RuntimeException("Failed to compile CEL expression: " + expression, e);
33+
}
34+
}
35+
36+
@Override
37+
@SuppressWarnings("unchecked")
38+
public T evaluate(Flag flag, EvaluationContext evaluationContext) {
39+
try {
40+
Map<String, Object> objectMap = new HashMap<>(Map.of("email", ""));
41+
if (evaluationContext != null) {
42+
// Evaluate with context
43+
objectMap.putAll(evaluationContext.asObjectMap());
44+
}
45+
46+
Object result = program.eval(objectMap);
47+
48+
String stringResult = (String) result;
49+
return (T) flag.getVariants().get(stringResult);
50+
} catch (Exception e) {
51+
throw new RuntimeException(e);
52+
}
53+
}
54+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package dev.openfeature.sdk.testutils.jackson;
2+
3+
import com.fasterxml.jackson.core.JsonParser;
4+
import com.fasterxml.jackson.databind.DeserializationContext;
5+
import com.fasterxml.jackson.databind.JsonDeserializer;
6+
import com.fasterxml.jackson.databind.JsonNode;
7+
import dev.openfeature.sdk.providers.memory.ContextEvaluator;
8+
import java.io.IOException;
9+
10+
public class ContextEvaluatorDeserializer extends JsonDeserializer<ContextEvaluator<?>> {
11+
@Override
12+
public ContextEvaluator<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
13+
JsonNode node = p.getCodec().readTree(p);
14+
15+
if (node.isTextual()) {
16+
return new CelContextEvaluator<>(node.asText());
17+
}
18+
19+
if (node.isObject() && node.has("expression")) {
20+
return new CelContextEvaluator<>(node.get("expression").asText());
21+
}
22+
23+
return null;
24+
}
25+
}

0 commit comments

Comments
 (0)