Merge pull request #13 from open-feature/update-with-latest-spec
Update with latest spec
This commit is contained in:
commit
2b3ae81385
|
|
@ -4,3 +4,4 @@
|
|||
# Ignore Gradle build output directory
|
||||
build
|
||||
.idea
|
||||
specification.json
|
||||
|
|
|
|||
|
|
@ -23,5 +23,5 @@ public interface BaseEvaluation<T> {
|
|||
* The error code, if applicable. Should only be set when the Reason is ERROR.
|
||||
* @return {ErrorCode}
|
||||
*/
|
||||
ErrorCode getErrorCode();
|
||||
String getErrorCode();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
public interface Client extends FlagEvaluationLifecycle, Features{
|
||||
public interface Client extends FlagEvaluationLifecycle, Features {
|
||||
Metadata getMetadata();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
public interface FeatureProvider {
|
||||
String getName();
|
||||
Metadata getMetadata();
|
||||
ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
|
||||
ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
|
||||
ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ public class FlagEvaluationDetails<T> implements BaseEvaluation<T> {
|
|||
T value;
|
||||
@Nullable String variant;
|
||||
Reason reason;
|
||||
@Nullable ErrorCode errorCode;
|
||||
@Nullable String errorCode;
|
||||
|
||||
public static <T> FlagEvaluationDetails<T> from(ProviderEvaluation<T> providerEval, String flagKey) {
|
||||
return FlagEvaluationDetails.<T>builder()
|
||||
|
|
|
|||
|
|
@ -11,14 +11,15 @@ public class HookContext<T> {
|
|||
@NonNull FlagValueType type;
|
||||
@NonNull T defaultValue;
|
||||
@NonNull EvaluationContext ctx;
|
||||
Client client;
|
||||
FeatureProvider provider;
|
||||
Metadata clientMetadata;
|
||||
Metadata providerMetadata;
|
||||
|
||||
public static <T> HookContext<T> from(String key, FlagValueType type, Client client, EvaluationContext ctx, T defaultValue) {
|
||||
public static <T> HookContext<T> from(String key, FlagValueType type, Metadata clientMetadata, Metadata providerMetadata, EvaluationContext ctx, T defaultValue) {
|
||||
return HookContext.<T>builder()
|
||||
.flagKey(key)
|
||||
.type(type)
|
||||
.client(client)
|
||||
.clientMetadata(clientMetadata)
|
||||
.providerMetadata(providerMetadata)
|
||||
.ctx(ctx)
|
||||
.defaultValue(defaultValue)
|
||||
.build();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
public interface Metadata {
|
||||
public String getName();
|
||||
}
|
||||
|
|
@ -7,6 +7,16 @@ public class NoOpProvider implements FeatureProvider {
|
|||
@Getter
|
||||
private final String name = "No-op Provider";
|
||||
|
||||
@Override
|
||||
public Metadata getMetadata() {
|
||||
return new Metadata() {
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return ProviderEvaluation.<Boolean>builder()
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ public class OpenFeatureAPI {
|
|||
return getClient(null, null);
|
||||
}
|
||||
|
||||
public Metadata getProviderMetadata() {
|
||||
return provider.getMetadata();
|
||||
}
|
||||
|
||||
public Client getClient(@Nullable String name) {
|
||||
return getClient(name, null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ public class OpenFeatureClient implements Client {
|
|||
// merge of: API.context, client.context, invocation.context
|
||||
|
||||
// TODO: Context transformation?
|
||||
HookContext hookCtx = HookContext.from(key, type, this, ctx, defaultValue);
|
||||
HookContext hookCtx = HookContext.from(key, type, this.getMetadata(), OpenFeatureAPI.getInstance().getProvider().getMetadata(), ctx, defaultValue);
|
||||
|
||||
List<Hook> mergedHooks;
|
||||
if (options != null && options.getHooks() != null) {
|
||||
|
|
@ -84,11 +84,7 @@ public class OpenFeatureClient implements Client {
|
|||
}
|
||||
details.value = defaultValue;
|
||||
details.reason = Reason.ERROR;
|
||||
if (e instanceof OpenFeatureError) { //NOPMD - suppressed AvoidInstanceofChecksInCatchClause - Don't want to duplicate detail creation logic.
|
||||
details.errorCode = ((OpenFeatureError) e).getErrorCode();
|
||||
} else {
|
||||
details.errorCode = ErrorCode.GENERAL;
|
||||
}
|
||||
details.errorCode = e.getMessage();
|
||||
this.errorHooks(hookCtx, e, mergedHooks, hints);
|
||||
} finally {
|
||||
this.afterAllHooks(hookCtx, mergedHooks, hints);
|
||||
|
|
@ -99,13 +95,21 @@ public class OpenFeatureClient implements Client {
|
|||
|
||||
private void errorHooks(HookContext hookCtx, Exception e, List<Hook> hooks, ImmutableMap<String, Object> hints) {
|
||||
for (Hook hook : hooks) {
|
||||
hook.error(hookCtx, e, hints);
|
||||
try {
|
||||
hook.error(hookCtx, e, hints);
|
||||
} catch (Exception inner_exception) {
|
||||
log.error("Exception when running error hooks " + hook.getClass().toString(), inner_exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void afterAllHooks(HookContext hookCtx, List<Hook> hooks, ImmutableMap<String, Object> hints) {
|
||||
for (Hook hook : hooks) {
|
||||
hook.finallyAfter(hookCtx, hints);
|
||||
try {
|
||||
hook.finallyAfter(hookCtx, hints);
|
||||
} catch (Exception inner_exception) {
|
||||
log.error("Exception when running finally hooks " + hook.getClass().toString(), inner_exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -247,4 +251,14 @@ public class OpenFeatureClient implements Client {
|
|||
public <T> FlagEvaluationDetails<T> getObjectDetails(String key, T defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.OBJECT, key, defaultValue, ctx, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Metadata getMetadata() {
|
||||
return new Metadata() {
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,5 +10,5 @@ public class ProviderEvaluation<T> implements BaseEvaluation<T> {
|
|||
T value;
|
||||
@Nullable String variant;
|
||||
Reason reason;
|
||||
@Nullable ErrorCode errorCode;
|
||||
@Nullable String errorCode;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,27 +3,32 @@ package dev.openfeature.javasdk;
|
|||
|
||||
public class AlwaysBrokenProvider implements FeatureProvider {
|
||||
@Override
|
||||
public String getName() {
|
||||
throw new NotImplementedException();
|
||||
public Metadata getMetadata() {
|
||||
return new Metadata() {
|
||||
@Override
|
||||
public String getName() {
|
||||
throw new NotImplementedException("BORK");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
throw new NotImplementedException();
|
||||
throw new NotImplementedException("BORK");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
throw new NotImplementedException();
|
||||
throw new NotImplementedException("BORK");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
throw new NotImplementedException();
|
||||
throw new NotImplementedException("BORK");
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ProviderEvaluation<T> getObjectEvaluation(String key, T defaultValue, EvaluationContext invocationContext, FlagEvaluationOptions options) {
|
||||
throw new NotImplementedException();
|
||||
throw new NotImplementedException("BORK");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class DeveloperExperienceTest {
|
|||
api.setProvider(new AlwaysBrokenProvider());
|
||||
Client client = api.getClient();
|
||||
FlagEvaluationDetails<Boolean> retval = client.getBooleanDetails(flagKey, false);
|
||||
assertEquals(ErrorCode.GENERAL, retval.getErrorCode());
|
||||
assertEquals("BORK", retval.getErrorCode());
|
||||
assertEquals(Reason.ERROR, retval.getReason());
|
||||
assertFalse(retval.getValue());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
public class DoSomethingProvider implements FeatureProvider {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "test";
|
||||
public Metadata getMetadata() {
|
||||
return new Metadata() {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "test";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import java.time.ZonedDateTime;
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class EvalContextTests {
|
||||
@Specification(spec="Evaluation Context", number="3.1",
|
||||
@Specification(number="3.1",
|
||||
text="The `evaluation context` structure **MUST** define an optional `targeting key` field of " +
|
||||
"type string, identifying the subject of the flag evaluation.")
|
||||
@Test void requires_targeting_key() {
|
||||
|
|
@ -16,7 +16,7 @@ public class EvalContextTests {
|
|||
assertEquals("targeting-key", ec.getTargetingKey());
|
||||
}
|
||||
|
||||
@Specification(spec="Evaluation Context", number="3.2", text="The evaluation context MUST support the inclusion of " +
|
||||
@Specification(number="3.2", text="The evaluation context MUST support the inclusion of " +
|
||||
"custom fields, having keys of type `string`, and " +
|
||||
"values of type `boolean | string | number | datetime | structure`.")
|
||||
@Test void eval_context() {
|
||||
|
|
@ -36,7 +36,7 @@ public class EvalContextTests {
|
|||
assertEquals(dt, ec.getDatetimeAttribute("dt"));
|
||||
}
|
||||
|
||||
@Specification(spec="Evaluation Context", number="3.2", text="The evaluation context MUST support the inclusion of " +
|
||||
@Specification(number="3.2", text="The evaluation context MUST support the inclusion of " +
|
||||
"custom fields, having keys of type `string`, and " +
|
||||
"values of type `boolean | string | number | datetime | structure`.")
|
||||
@Test void eval_context__structure() {
|
||||
|
|
|
|||
|
|
@ -16,18 +16,12 @@ public class FlagEvaluationSpecTests {
|
|||
return api.getClient();
|
||||
}
|
||||
|
||||
@Specification(spec="flag evaluation", number="1.1",
|
||||
text="The API, and any state it maintains SHOULD exist as a global singleton, " +
|
||||
"even in cases wherein multiple versions of the API are present at runtime.")
|
||||
@Specification(number="1.1.1", text="The API, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the API are present at runtime.")
|
||||
@Test void global_singleton() {
|
||||
assertSame(OpenFeatureAPI.getInstance(), OpenFeatureAPI.getInstance());
|
||||
}
|
||||
|
||||
@Specification(spec="flag evaluation", number="1.2",
|
||||
text="The API MUST provide a function to set the global provider singleton, " +
|
||||
"which accepts an API-conformant provider implementation.")
|
||||
@Specification(spec="flag evaluation", number="1.4",
|
||||
text="The API MUST provide a function for retrieving the provider implementation.")
|
||||
@Specification(number="1.1.2", text="The API MUST provide a function to set the global provider singleton, which accepts an API-conformant provider implementation.")
|
||||
@Test void provider() {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
FeatureProvider mockProvider = mock(FeatureProvider.class);
|
||||
|
|
@ -35,9 +29,14 @@ public class FlagEvaluationSpecTests {
|
|||
assertEquals(mockProvider, api.getProvider());
|
||||
}
|
||||
|
||||
@Specification(spec="flag evaluation", number="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.")
|
||||
@Specification(number="1.1.4", text="The API MUST provide a function for retrieving the metadata field of the configured provider.")
|
||||
@Test void provider_metadata() {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new DoSomethingProvider());
|
||||
assertEquals("test", api.getProviderMetadata().getName());
|
||||
}
|
||||
|
||||
@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.")
|
||||
@Test void hook_addition() {
|
||||
Hook h1 = mock(Hook.class);
|
||||
Hook h2 = mock(Hook.class);
|
||||
|
|
@ -52,17 +51,14 @@ public class FlagEvaluationSpecTests {
|
|||
assertEquals(h2, api.getApiHooks().get(1));
|
||||
}
|
||||
|
||||
@Specification(spec="flag evaluation", number="1.5", text="The API MUST provide a function for creating a client " +
|
||||
"which accepts the following options: name (optional): A logical string identifier for the client.")
|
||||
@Specification(number="1.1.5", text="The API MUST provide a function for creating a client which accepts the following options: - name (optional): A logical string identifier for the client.")
|
||||
@Test void namedClient() {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
Client c = api.getClient("Sir Calls-a-lot");
|
||||
// TODO: Doesn't say that you can *get* the client name.. which seems useful?
|
||||
}
|
||||
|
||||
@Specification(spec="flag evaluation", number="1.6", text="The client MUST provide a method 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.")
|
||||
@Specification(number="1.2.1", text="The client MUST provide a method 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.")
|
||||
@Test void hookRegistration() {
|
||||
Client c = _client();
|
||||
Hook m1 = mock(Hook.class);
|
||||
|
|
@ -74,12 +70,8 @@ public class FlagEvaluationSpecTests {
|
|||
assertTrue(hooks.contains(m1));
|
||||
assertTrue(hooks.contains(m2));
|
||||
}
|
||||
|
||||
@Specification(spec="flag evaluation", number="1.7", text="The client MUST provide methods for flag evaluation, with" +
|
||||
" parameters flag key (string, required), default value (boolean | number | string | structure, required), " +
|
||||
"evaluation context (optional), and evaluation options (optional), which returns the flag value.")
|
||||
@Specification(spec="flag evaluation", number="1.8.1",text="The client MUST provide methods for typed flag " +
|
||||
"evaluation, including boolean, numeric, string, and structure.")
|
||||
@Specification(number="1.3.1", text="The client MUST provide methods for flag evaluation, with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns the flag value.")
|
||||
@Specification(number="1.3.2.1", text="The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure.")
|
||||
@Test void value_flags() {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new DoSomethingProvider());
|
||||
|
|
@ -104,17 +96,12 @@ public class FlagEvaluationSpecTests {
|
|||
assertEquals(null, c.getObjectValue(key, new Node<Integer>(), new EvaluationContext(), FlagEvaluationOptions.builder().build()));
|
||||
}
|
||||
|
||||
@Specification(spec="flag evaluation", number="1.9", text="The client MUST provide methods for detailed flag value " +
|
||||
"evaluation with parameters flag key (string, required), default value (boolean | number | string | " +
|
||||
"structure, required), evaluation context (optional), and evaluation options (optional), which returns an " +
|
||||
"evaluation details structure.")
|
||||
@Specification(spec="flag evaluation", number="1.10", text="The evaluation details structure's value field MUST " +
|
||||
"contain the evaluated flag value.")
|
||||
@Specification(spec="flag evaluation", number="1.11.1", text="The evaluation details structure SHOULD accept a " +
|
||||
"generic argument (or use an equivalent language feature) which indicates the type of the wrapped value " +
|
||||
"field.")
|
||||
@Specification(spec="flag evaluation", number="1.12", text="The evaluation details structure's flag key field MUST " +
|
||||
"contain the flag key argument passed to the detailed flag evaluation method.")
|
||||
@Specification(number="1.4.1", text="The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns an evaluation details structure.")
|
||||
@Specification(number="1.4.2", text="The evaluation details structure's value field MUST contain the evaluated flag value.")
|
||||
@Specification(number="1.4.3.1", text="The evaluation details structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped value field.")
|
||||
@Specification(number="1.4.4", text="The evaluation details structure's flag key field MUST contain the flag key argument passed to the detailed flag evaluation method.")
|
||||
@Specification(number="1.4.5", text="In cases of normal execution, the evaluation details structure's variant field MUST contain the value of the variant field in the flag resolution structure returned by the configured provider, if the field is set.")
|
||||
@Specification(number="1.4.6", text="In cases of normal execution, the evaluation details structure's reason field MUST contain the value of the reason field in the flag resolution structure returned by the configured provider, if the field is set.")
|
||||
@Test void detail_flags() {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new DoSomethingProvider());
|
||||
|
|
@ -146,11 +133,11 @@ public class FlagEvaluationSpecTests {
|
|||
assertEquals(id, c.getIntegerDetails(key, 4));
|
||||
assertEquals(id, c.getIntegerDetails(key, 4, new EvaluationContext()));
|
||||
assertEquals(id, c.getIntegerDetails(key, 4, new EvaluationContext(), FlagEvaluationOptions.builder().build()));
|
||||
|
||||
// TODO: Structure detail tests.
|
||||
}
|
||||
|
||||
@Specification(spec="flag evaluation", number="1.17", text="The evaluation options structure's hooks field denotes " +
|
||||
"a collection of hooks that the client MUST execute for the respective flag evaluation, in addition to " +
|
||||
"those already configured.")
|
||||
@Specification(number="1.5.1", text="The evaluation options structure's hooks field denotes an ordered collection of hooks that the client MUST execute for the respective flag evaluation, in addition to those already configured.")
|
||||
@Test void hooks() {
|
||||
Client c = _client();
|
||||
Hook clientHook = mock(Hook.class);
|
||||
|
|
@ -163,44 +150,45 @@ public class FlagEvaluationSpecTests {
|
|||
verify(invocationHook, times(1)).before(any(), any());
|
||||
}
|
||||
|
||||
@Specification(spec="flag evaluation", number="1.18", text="Methods, functions, or operations on the client MUST " +
|
||||
"NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the " +
|
||||
"default value in the event of abnormal execution. Exceptions include functions or methods for the " +
|
||||
"purposes for configuration or setup.")
|
||||
@Specification(number="1.4.9", text="Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the default value in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.")
|
||||
@Specification(number="1.4.7", text="In cases of abnormal execution, the evaluation details structure's error code field MUST contain a string identifying an error occurred during flag evaluation and the nature of the error.")
|
||||
@Test void broken_provider() {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new AlwaysBrokenProvider());
|
||||
Client c = api.getClient();
|
||||
assertFalse(c.getBooleanValue("key", false));
|
||||
|
||||
FlagEvaluationDetails<Boolean> details = c.getBooleanDetails("key", false);
|
||||
assertEquals("BORK", details.getErrorCode());
|
||||
}
|
||||
@Specification(spec="flag evaluation", number="1.19", text="In the case of abnormal execution, the client SHOULD " +
|
||||
"log an informative error message.")
|
||||
@Specification(number="1.4.10", text="In the case of abnormal execution, the client SHOULD log an informative error message.")
|
||||
@Disabled("Not actually sure how to mock out the slf4j logger")
|
||||
@Test void log_on_error() throws NotImplementedException {
|
||||
throw new NotImplementedException();
|
||||
throw new NotImplementedException("Dunno.");
|
||||
}
|
||||
|
||||
@Specification(spec="flag evaluation", number="1.21", text="The client MUST transform the evaluation context using " +
|
||||
"the provider's context transformer function, before passing the result of the transformation to the " +
|
||||
"provider's flag resolution functions.")
|
||||
@Specification(spec="flag evaluation", number="1.13", text="In cases of normal execution, the evaluation details " +
|
||||
"structure's variant field MUST contain the value of the variant field in the flag resolution structure " +
|
||||
"returned by the configured provider, if the field is set.")
|
||||
@Specification(spec="flag evaluation", number="1.14", text="In cases of normal execution, the evaluation details " +
|
||||
"structure's reason field MUST contain the value of the reason field in the flag resolution structure " +
|
||||
"returned by the configured provider, if the field is set.")
|
||||
@Specification(spec="flag evaluation", number="1.15", text="In cases of abnormal execution, the evaluation details " +
|
||||
"structure's error code field MUST identify an error occurred during flag evaluation, having possible " +
|
||||
"values PROVIDER_NOT_READY, FLAG_NOT_FOUND, PARSE_ERROR, TYPE_MISMATCH, or GENERAL.")
|
||||
@Specification(spec="flag evaluation", number="1.16", text="In cases of abnormal execution (network failure, " +
|
||||
"unhandled error, etc) the reason field in the evaluation details SHOULD indicate an error.")
|
||||
@Test @Disabled void todo() {}
|
||||
|
||||
@Specification(spec="flag evaluation", number="1.20", text="The client SHOULD provide asynchronous or non-blocking " +
|
||||
"mechanisms for flag evaluation.")
|
||||
@Disabled("We're operating in a one request per thread model")
|
||||
@Test void explicitly_not_doing() {
|
||||
@Specification(number="1.2.2", text="The client interface MUST define a metadata member or accessor, containing an immutable name field or accessor of type string, which corresponds to the name value supplied during client creation.")
|
||||
@Test void clientMetadata() {
|
||||
Client c = _client();
|
||||
assertNull(c.getMetadata().getName());
|
||||
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new AlwaysBrokenProvider());
|
||||
Client c2 = api.getClient("test");
|
||||
assertEquals("test", c2.getMetadata().getName());
|
||||
}
|
||||
|
||||
@Specification(number="1.4.8", text="In cases of abnormal execution (network failure, unhandled error, etc) the reason field in the evaluation details SHOULD indicate an error.")
|
||||
@Test @Disabled void question_pending() {}
|
||||
|
||||
@Specification(number="1.3.3", text="The client SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied default value should be returned.")
|
||||
@Specification(number="1.1.6", text="The client creation function MUST NOT throw, or otherwise abnormally terminate.")
|
||||
@Disabled("Not sure how to test?")
|
||||
@Test void not_sure_how_to_test() {}
|
||||
|
||||
@Specification(number="1.6.1", text="The client SHOULD transform the evaluation context using the provider's context transformer function if one is defined, before passing the result of the transformation to the provider's flag resolution functions.")
|
||||
@Test void not_doing_unless_someone_has_a_good_reason_why() {}
|
||||
|
||||
@Specification(number="1.4.11", text="The client SHOULD provide asynchronous or non-blocking mechanisms for flag evaluation.")
|
||||
@Test void one_thread_per_request_model() {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class HookContextTest {
|
||||
@Specification(number="4.2.2.2", text="Condition: The client metadata field in the hook context MUST be immutable.")
|
||||
@Specification(number="4.2.2.3", text="Condition: The provider metadata field in the hook context MUST be immutable.")
|
||||
@Test void metadata_field_is_type_metadata() {
|
||||
Metadata meta = mock(Metadata.class);
|
||||
HookContext<Object> hc = HookContext.from(
|
||||
"key",
|
||||
FlagValueType.BOOLEAN,
|
||||
meta,
|
||||
meta,
|
||||
new EvaluationContext(),
|
||||
false
|
||||
);
|
||||
|
||||
assertTrue(Metadata.class.isAssignableFrom(hc.getClientMetadata().getClass()));
|
||||
assertTrue(Metadata.class.isAssignableFrom(hc.getProviderMetadata().getClass()));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package dev.openfeature.javasdk;
|
|||
import com.google.common.collect.ImmutableMap;
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InOrder;
|
||||
|
|
@ -24,7 +23,7 @@ public class HookSpecTests {
|
|||
OpenFeatureAPI.getInstance().clearHooks();
|
||||
}
|
||||
|
||||
@Specification(spec="hooks", number="1.3", text="flag key, flag type, default value properties MUST be immutable. If the language does not support immutability, the hook MUST NOT modify these properties.")
|
||||
@Specification(number="4.1.3", text="The flag key, flag type, and default value properties MUST be immutable. If the language does not support immutability, the hook MUST NOT modify these properties.")
|
||||
@Test void immutableValues() {
|
||||
try {
|
||||
HookContext.class.getMethod("setFlagKey");
|
||||
|
|
@ -48,7 +47,7 @@ public class HookSpecTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Specification(spec="hooks", number="1.1", text="Hook context MUST provide: the flag key, flag type, evaluation context, and the default value.")
|
||||
@Specification(number="4.1.1", text="Hook context MUST provide: the flag key, flag value type, evaluation context, and the default value.")
|
||||
@Test void nullish_properties_on_hookcontext() {
|
||||
// missing ctx
|
||||
try {
|
||||
|
|
@ -112,7 +111,7 @@ public class HookSpecTests {
|
|||
|
||||
}
|
||||
|
||||
@Specification(spec="hooks", number="1.2", text="Hook context SHOULD provide: provider (instance) and client (instance)")
|
||||
@Specification(number="4.1.2", text="The hook context SHOULD provide: access to the client metadata and the provider metadata fields.")
|
||||
@Test void optional_properties() {
|
||||
// don't specify
|
||||
HookContext.<Integer>builder()
|
||||
|
|
@ -127,7 +126,7 @@ public class HookSpecTests {
|
|||
.flagKey("key")
|
||||
.type(FlagValueType.INTEGER)
|
||||
.ctx(new EvaluationContext())
|
||||
.provider(new NoOpProvider())
|
||||
.providerMetadata(new NoOpProvider().getMetadata())
|
||||
.defaultValue(1)
|
||||
.build();
|
||||
|
||||
|
|
@ -137,11 +136,11 @@ public class HookSpecTests {
|
|||
.type(FlagValueType.INTEGER)
|
||||
.ctx(new EvaluationContext())
|
||||
.defaultValue(1)
|
||||
.client(OpenFeatureAPI.getInstance().getClient())
|
||||
.clientMetadata(OpenFeatureAPI.getInstance().getClient().getMetadata())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Specification(spec="hooks", number="3.2", text="The before stage MUST run before flag evaluation occurs. It accepts a hook context (required) and HookHints (optional) as parameters and returns either a EvaluationContext or nothing.")
|
||||
@Specification(number="4.3.2", text="The before stage MUST run before flag resolution occurs. It accepts a hook context (required) and hook hints (optional) as parameters and returns either an evaluation context or nothing.")
|
||||
@Test void before_runs_ahead_of_evaluation() {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new AlwaysBrokenProvider());
|
||||
|
|
@ -154,29 +153,12 @@ public class HookSpecTests {
|
|||
verify(evalHook, times(1)).before(any(), any());
|
||||
}
|
||||
|
||||
@Specification(spec="hooks", number="5.1", text="Flag evalution options MUST contain a list of hooks to evaluate.")
|
||||
@Test void feo_has_hook_list() {
|
||||
FlagEvaluationOptions feo = FlagEvaluationOptions.builder()
|
||||
.build();
|
||||
assertNotNull(feo.getHooks());
|
||||
}
|
||||
|
||||
@Specification(spec="hooks", number="4.3", text="If an error occurs in the finally hook, it MUST NOT trigger the error hook.")
|
||||
@Test void errors_in_finally() {
|
||||
Hook<Boolean> h = mock(Hook.class);
|
||||
doThrow(RuntimeException.class).when(h).finallyAfter(any(), any());
|
||||
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new NoOpProvider());
|
||||
Client c= api.getClient();
|
||||
|
||||
assertThrows(RuntimeException.class, () -> c.getBooleanValue("key", false, null, FlagEvaluationOptions.builder().hook(h).build()));
|
||||
|
||||
verify(h, times(1)).finallyAfter(any(), any());
|
||||
verify(h, times(0)).error(any(), any(), any());
|
||||
}
|
||||
|
||||
@Specification(spec="hooks", number="3.6", text="The error hook MUST run when errors are encountered in the before stage, the after stage or during flag evaluation. It accepts hook context (required), exception for what went wrong (required), and HookHints (optional). It has no return value.")
|
||||
@Test void error_hook_run_during_non_finally_stage() {
|
||||
final boolean[] error_called = {false};
|
||||
Hook h = mock(Hook.class);
|
||||
|
|
@ -185,12 +167,12 @@ public class HookSpecTests {
|
|||
verify(h, times(0)).error(any(), any(), any());
|
||||
}
|
||||
|
||||
@Specification(spec="hooks", number="4.1", text="The API, Client and invocation MUST have a method for registering hooks which accepts flag evaluation options")
|
||||
@Specification(spec="hooks", number="4.2", text="Hooks MUST be evaluated in the following order:" +
|
||||
"before: API, Client, Invocation" +
|
||||
"after: Invocation, Client, API" +
|
||||
"error (if applicable): Invocation, Client, API" +
|
||||
"finally: Invocation, Client, API")
|
||||
|
||||
@Specification(number="4.4.1", text="The API, Client and invocation MUST have a method for registering hooks which accepts flag evaluation options")
|
||||
@Specification(number="4.3.5", text="The after stage MUST run after flag resolution occurs. It accepts a hook context (required), flag evaluation details (required) and hook hints (optional). It has no return value.")
|
||||
@Specification(number="4.4.2", text="Hooks MUST be evaluated in the following order: - before: API, Client, Invocation - after: Invocation, Client, API - error (if applicable): Invocation, Client, API - finally: Invocation, Client, API")
|
||||
@Specification(number="4.3.6", text="The error hook MUST run when errors are encountered in the before stage, the after stage or during flag resolution. It accepts hook context (required), exception representing what went wrong (required), and hook hints (optional). It has no return value.")
|
||||
@Specification(number="4.3.7", text="The finally hook MUST run after the before, after, and error stages. It accepts a hook context (required) and hook hints (optional). There is no return value.")
|
||||
@Test void hook_eval_order() {
|
||||
List<String> evalOrder = new ArrayList<String>();
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
|
|
@ -277,28 +259,46 @@ public class HookSpecTests {
|
|||
assertEquals(expectedOrder, evalOrder);
|
||||
}
|
||||
|
||||
@Specification(spec="hooks", number="4.6", text="If an error is encountered in the error stage, it MUST NOT be returned to the user.")
|
||||
@Disabled("Not actually sure what 'returned to the user' means in this context. There is no exception information returned.")
|
||||
@Test void error_in_error_stage() {
|
||||
@Specification(number="4.4.6", text="If an error occurs during the evaluation of before or after hooks, any remaining hooks in the before or after stages MUST NOT be invoked.")
|
||||
@Test void error_stops_before() {
|
||||
Hook<Boolean> h = mock(Hook.class);
|
||||
doThrow(RuntimeException.class).when(h).error(any(), any(), any());
|
||||
doThrow(RuntimeException.class).when(h).before(any(), any());
|
||||
Hook<Boolean> h2 = mock(Hook.class);
|
||||
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new AlwaysBrokenProvider());
|
||||
Client c = api.getClient();
|
||||
|
||||
FlagEvaluationDetails<Boolean> details = c.getBooleanDetails("key", false, null, FlagEvaluationOptions.builder().hook(h).build());
|
||||
c.getBooleanDetails("key", false, null, FlagEvaluationOptions.builder()
|
||||
.hook(h2)
|
||||
.hook(h)
|
||||
.build());
|
||||
verify(h, times(1)).before(any(), any());
|
||||
verify(h2, times(0)).before(any(), any());
|
||||
}
|
||||
|
||||
@Specification(number="4.4.6", text="If an error occurs during the evaluation of before or after hooks, any remaining hooks in the before or after stages MUST NOT be invoked.")
|
||||
@Test void error_stops_after() {
|
||||
Hook<Boolean> h = mock(Hook.class);
|
||||
doThrow(RuntimeException.class).when(h).after(any(), any(), any());
|
||||
Hook<Boolean> h2 = mock(Hook.class);
|
||||
|
||||
@Specification(spec="hooks", number="2.1", text="HookHints MUST be a map of objects.")
|
||||
@Specification(spec="hooks", number="2.2", text="Condition: HookHints MUST be immutable.")
|
||||
@Specification(spec="hooks", number="5.4", text="The hook MUST NOT alter the HookHints object.")
|
||||
@Specification(spec="hooks", number="5.3", text="HookHints MUST be passed to each hook.")
|
||||
Client c = getClient(null);
|
||||
|
||||
c.getBooleanDetails("key", false, null, FlagEvaluationOptions.builder()
|
||||
.hook(h)
|
||||
.hook(h2)
|
||||
.build());
|
||||
verify(h, times(1)).after(any(), any(), any());
|
||||
verify(h2, times(0)).after(any(), any(), any());
|
||||
}
|
||||
|
||||
@Specification(number="4.2.1", text="hook hints MUST be a structure supports definition of arbitrary properties, with keys of type string, and values of type boolean | string | number | datetime | structure..")
|
||||
@Specification(number="4.5.2", text="hook hints MUST be passed to each hook.")
|
||||
@Specification(number="4.2.2.1", text="Condition: Hook hints MUST be immutable.")
|
||||
@Specification(number="4.5.3", text="The hook MUST NOT alter the hook hints structure.")
|
||||
@Test void hook_hints() {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new NoOpProvider());
|
||||
Client client = api.getClient();
|
||||
Client client = getClient(null);
|
||||
Hook<Boolean> mutatingHook = new Hook<Boolean>() {
|
||||
@Override
|
||||
public Optional<EvaluationContext> before(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) {
|
||||
|
|
@ -330,15 +330,13 @@ public class HookSpecTests {
|
|||
.build());
|
||||
}
|
||||
|
||||
@Specification(spec="hooks", number="5.2", text="Flag evaluation options MAY contain HookHints, a map of data to be provided to hook invocations.")
|
||||
@Specification(number="4.5.1", text="Flag evaluation options MAY contain hook hints, a map of data to be provided to hook invocations.")
|
||||
@Test void missing_hook_hints() {
|
||||
FlagEvaluationOptions feo = FlagEvaluationOptions.builder().build();
|
||||
assertNotNull(feo.getHookHints());
|
||||
assertTrue(feo.getHookHints().isEmpty());
|
||||
}
|
||||
|
||||
@Specification(spec="hooks", number="3.5", text="The after stage MUST run after flag evaluation occurs. It accepts a hook context (required), flag evaluation details (required) and HookHints (optional). It has no return value.")
|
||||
@Specification(spec="hooks", number="3.7", text="The finally hook MUST run after the before, after, and error stages. It accepts a hook context (required) and HookHints (optional). There is no return value.")
|
||||
@Test void flag_eval_hook_order() {
|
||||
Hook hook = mock(Hook.class);
|
||||
FeatureProvider provider = mock(FeatureProvider.class);
|
||||
|
|
@ -360,28 +358,34 @@ public class HookSpecTests {
|
|||
order.verify(hook).finallyAfter(any(),any());
|
||||
}
|
||||
|
||||
@Specification(spec="hooks", number="4.4", text="If an error occurs in the before or after hooks, the error hooks MUST be invoked.")
|
||||
@Test void error_hooks() {
|
||||
@Specification(number="4.4.5", text="If an error occurs in the before or after hooks, the error hooks MUST be invoked.")
|
||||
@Test void error_hooks__before() {
|
||||
Hook hook = mock(Hook.class);
|
||||
doThrow(RuntimeException.class).when(hook).before(any(), any());
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new NoOpProvider());
|
||||
Client client = api.getClient();
|
||||
Client client = getClient(null);
|
||||
client.getBooleanValue("key", false, new EvaluationContext(),
|
||||
FlagEvaluationOptions.builder().hook(hook).build());
|
||||
verify(hook, times(1)).before(any(), any());
|
||||
verify(hook, times(1)).error(any(), any(), any());
|
||||
}
|
||||
|
||||
@Specification(spec="hooks", number="4.5", text="If an error occurs during the evaluation of before or after hooks, any remaining hooks in the before or after stages MUST NOT be invoked.")
|
||||
@Specification(number="4.4.5", text="If an error occurs in the before or after hooks, the error hooks MUST be invoked.")
|
||||
@Test void error_hooks__after() {
|
||||
Hook hook = mock(Hook.class);
|
||||
doThrow(RuntimeException.class).when(hook).after(any(), any(), any());
|
||||
Client client = getClient(null);
|
||||
client.getBooleanValue("key", false, new EvaluationContext(),
|
||||
FlagEvaluationOptions.builder().hook(hook).build());
|
||||
verify(hook, times(1)).after(any(), any(), any());
|
||||
verify(hook, times(1)).error(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test void multi_hooks_early_out__before() {
|
||||
Hook hook = mock(Hook.class);
|
||||
Hook hook2 = mock(Hook.class);
|
||||
doThrow(RuntimeException.class).when(hook).before(any(), any());
|
||||
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new NoOpProvider());
|
||||
Client client = api.getClient();
|
||||
Client client = getClient(null);
|
||||
|
||||
client.getBooleanValue("key", false, new EvaluationContext(),
|
||||
FlagEvaluationOptions.builder()
|
||||
|
|
@ -396,7 +400,8 @@ public class HookSpecTests {
|
|||
verify(hook2, times(1)).error(any(), any(), any());
|
||||
}
|
||||
|
||||
@Specification(spec="hooks", number="3.3", text="Any `EvaluationContext` returned from a `before` hook **MUST** be passed to subsequent `before` hooks (via `HookContext`).")
|
||||
@Specification(number="4.1.4", text="The evaluation context MUST be mutable only within the before hook.")
|
||||
@Specification(number="4.3.3", text="Any evaluation context returned from a before hook MUST be passed to subsequent before hooks (via HookContext).")
|
||||
@Test void beforeContextUpdated() {
|
||||
EvaluationContext ctx = new EvaluationContext();
|
||||
Hook hook = mock(Hook.class);
|
||||
|
|
@ -406,10 +411,7 @@ public class HookSpecTests {
|
|||
InOrder order = inOrder(hook, hook2);
|
||||
|
||||
|
||||
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new NoOpProvider());
|
||||
Client client = api.getClient();
|
||||
Client client = getClient(null);
|
||||
client.getBooleanValue("key", false, new EvaluationContext(),
|
||||
FlagEvaluationOptions.builder()
|
||||
.hook(hook2)
|
||||
|
|
@ -424,7 +426,8 @@ public class HookSpecTests {
|
|||
assertEquals(hc.getCtx(), ctx);
|
||||
|
||||
}
|
||||
@Specification(spec="hooks", number="3.4", text="When `before` hooks have finished executing, any resulting `EvaluationContext` **MUST** be merged with the invocation `EvaluationContext` wherein the invocation `EvaluationContext` win any conflicts.")
|
||||
|
||||
@Specification(number="4.3.4", text="When before hooks have finished executing, any resulting evaluation context MUST be merged with the invocation evaluation context with the invocation evaluation context taking precedence in the case of any conflicts.")
|
||||
@Test void mergeHappensCorrectly() {
|
||||
EvaluationContext hookCtx = new EvaluationContext();
|
||||
hookCtx.addStringAttribute("test", "broken");
|
||||
|
|
@ -456,13 +459,63 @@ public class HookSpecTests {
|
|||
assertEquals("exists", ec.getStringAttribute("another"));
|
||||
}
|
||||
|
||||
@Specification(number="4.4.3", text="If a finally hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining finally hooks.")
|
||||
@Test void first_finally_broken() {
|
||||
Hook hook = mock(Hook.class);
|
||||
doThrow(RuntimeException.class).when(hook).before(any(), any());
|
||||
doThrow(RuntimeException.class).when(hook).finallyAfter(any(), any());
|
||||
Hook hook2 = mock(Hook.class);
|
||||
InOrder order = inOrder(hook, hook2);
|
||||
|
||||
@Specification(spec="hooks", number="1.4", text="The evaluation context MUST be mutable only within the before hook.")
|
||||
@Specification(spec="hooks", number="3.1", text="Hooks MUST specify at least one stage.")
|
||||
@Test @Disabled void todo() {}
|
||||
Client client = getClient(null);
|
||||
client.getBooleanValue("key", false, new EvaluationContext(),
|
||||
FlagEvaluationOptions.builder()
|
||||
.hook(hook2)
|
||||
.hook(hook)
|
||||
.build());
|
||||
|
||||
order.verify(hook).before(any(), any());
|
||||
order.verify(hook2).finallyAfter(any(), any());
|
||||
order.verify(hook).finallyAfter(any(), any());
|
||||
}
|
||||
|
||||
@Specification(number="4.4.4", text="If an error hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining error hooks.")
|
||||
@Test void first_error_broken() {
|
||||
|
||||
Hook hook = mock(Hook.class);
|
||||
doThrow(RuntimeException.class).when(hook).before(any(), any());
|
||||
doThrow(RuntimeException.class).when(hook).error(any(), any(), any());
|
||||
Hook hook2 = mock(Hook.class);
|
||||
InOrder order = inOrder(hook, hook2);
|
||||
|
||||
Client client = getClient(null);
|
||||
client.getBooleanValue("key", false, new EvaluationContext(),
|
||||
FlagEvaluationOptions.builder()
|
||||
.hook(hook2)
|
||||
.hook(hook)
|
||||
.build());
|
||||
|
||||
order.verify(hook).before(any(), any());
|
||||
order.verify(hook2).error(any(), any(), any());
|
||||
order.verify(hook).error(any(), any(), any());
|
||||
}
|
||||
|
||||
private Client getClient(FeatureProvider provider) {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
if (provider == null) {
|
||||
api.setProvider(new NoOpProvider());
|
||||
} else {
|
||||
api.setProvider(provider);
|
||||
}
|
||||
Client client = api.getClient();
|
||||
return client;
|
||||
}
|
||||
|
||||
@Specification(number="4.3.1", text="Hooks MUST specify at least one stage.")
|
||||
@Test void default_methods_so_impossible() {}
|
||||
|
||||
@Specification(number="4.3.8.1", text="Instead of finally, finallyAfter SHOULD be used.")
|
||||
@SneakyThrows
|
||||
@Specification(spec="hooks", number="3.8", text="Condition: If finally is a reserved word in the language, finallyAfter SHOULD be used.")
|
||||
@Test void doesnt_use_finally() {
|
||||
try {
|
||||
Hook.class.getMethod("finally", HookContext.class, ImmutableMap.class);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
class MetadataTest {
|
||||
@Specification(number="4.2.2.2", text="Condition: The client metadata field in the hook context MUST be immutable.")
|
||||
@Specification(number="4.2.2.3", text="Condition: The provider metadata field in the hook context MUST be immutable.")
|
||||
@Test
|
||||
void metadata_is_immutable() {
|
||||
try {
|
||||
Metadata.class.getMethod("setName", String.class);
|
||||
fail("Not expected to be mutable.");
|
||||
} catch (NoSuchMethodException e) {
|
||||
// Pass
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,5 +4,7 @@ public class NotImplementedException extends RuntimeException {
|
|||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public NotImplementedException(){}
|
||||
public NotImplementedException(String message){
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,21 +8,20 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||
public class ProviderSpecTests {
|
||||
NoOpProvider p = new NoOpProvider();
|
||||
|
||||
@Specification(spec="provider", number="2.1", text="The provider interface MUST define a name field or accessor, " +
|
||||
"which identifies the provider implementation.")
|
||||
@Specification(number="2.1", text="The provider interface MUST define a metadata member or accessor, containing a name field or accessor of type string, which identifies the provider implementation.")
|
||||
@Test void name_accessor() {
|
||||
assertNotNull(p.getName());
|
||||
}
|
||||
|
||||
@Specification(spec="provider", number="2.3.1", text="The feature provider interface MUST define methods for typed " +
|
||||
@Specification(number="2.3.1", text="The feature provider interface MUST define methods for typed " +
|
||||
"flag resolution, including boolean, numeric, string, and structure.")
|
||||
@Specification(spec="provider", number="2.4", text="In cases of normal execution, the provider MUST populate the " +
|
||||
@Specification(number="2.4", text="In cases of normal execution, the provider MUST populate the " +
|
||||
"flag resolution structure's value field with the resolved flag value.")
|
||||
@Specification(spec="provider", number="2.2", text="The feature provider interface MUST define methods to resolve " +
|
||||
@Specification(number="2.2", text="The feature provider interface MUST define methods to resolve " +
|
||||
"flag values, with parameters flag key (string, required), default value " +
|
||||
"(boolean | number | string | structure, required), evaluation context (optional), and " +
|
||||
"evaluation options (optional), which returns a flag resolution structure.")
|
||||
@Specification(spec="provider", number="2.9.1", text="The flag resolution structure SHOULD accept a generic " +
|
||||
@Specification(number="2.9.1", text="The flag resolution structure SHOULD accept a generic " +
|
||||
"argument (or use an equivalent language feature) which indicates the type of the wrapped value field.")
|
||||
@Test void flag_value_set() {
|
||||
ProviderEvaluation<Integer> int_result = p.getIntegerEvaluation("key", 4, new EvaluationContext(), FlagEvaluationOptions.builder().build());
|
||||
|
|
@ -38,21 +37,21 @@ public class ProviderSpecTests {
|
|||
assertNotNull(boolean_result.getValue());
|
||||
}
|
||||
|
||||
@Specification(spec="provider", number="2.6", text="The provider SHOULD populate the flag resolution structure's " +
|
||||
@Specification(number="2.6", text="The provider SHOULD populate the flag resolution structure's " +
|
||||
"reason field with a string indicating the semantic reason for the returned flag value.")
|
||||
@Test void has_reason() {
|
||||
ProviderEvaluation<Boolean> result = p.getBooleanEvaluation("key", false, new EvaluationContext(), FlagEvaluationOptions.builder().build());
|
||||
assertEquals(Reason.DEFAULT, result.getReason());
|
||||
}
|
||||
|
||||
@Specification(spec="provider", number="2.7", text="In cases of normal execution, the provider MUST NOT populate " +
|
||||
@Specification(number="2.7", text="In cases of normal execution, the provider MUST NOT populate " +
|
||||
"the flag resolution structure's error code field, or otherwise must populate it with a null or falsy value.")
|
||||
@Test void no_error_code_by_default() {
|
||||
ProviderEvaluation<Boolean> result = p.getBooleanEvaluation("key", false, new EvaluationContext(), FlagEvaluationOptions.builder().build());
|
||||
assertNull(result.getErrorCode());
|
||||
}
|
||||
|
||||
@Specification(spec="provider", number="2.8", text="In cases of abnormal execution, the provider MUST indicate an " +
|
||||
@Specification(number="2.8", text="In cases of abnormal execution, the provider MUST indicate an " +
|
||||
"error using the idioms of the implementation language, with an associated error code having possible " +
|
||||
"values PROVIDER_NOT_READY, FLAG_NOT_FOUND, PARSE_ERROR, TYPE_MISMATCH, or GENERAL.")
|
||||
@Disabled("I don't think we expect the provider to do all the exception catching.. right?")
|
||||
|
|
@ -62,7 +61,7 @@ public class ProviderSpecTests {
|
|||
assertEquals(ErrorCode.GENERAL, result.getErrorCode());
|
||||
}
|
||||
|
||||
@Specification(spec="provider", number="2.5", text="In cases of normal execution, the provider SHOULD populate the " +
|
||||
@Specification(number="2.5", text="In cases of normal execution, the provider SHOULD populate the " +
|
||||
"flag resolution structure's variant field with a string identifier corresponding to the returned flag value.")
|
||||
@Test void variant_set() {
|
||||
ProviderEvaluation<Integer> int_result = p.getIntegerEvaluation("key", 4, new EvaluationContext(), FlagEvaluationOptions.builder().build());
|
||||
|
|
@ -75,13 +74,9 @@ public class ProviderSpecTests {
|
|||
assertNotNull(boolean_result.getReason());
|
||||
}
|
||||
|
||||
@Specification(spec="provider", number="2.11.1", text="If the implementation includes a context transformer, the " +
|
||||
"provider SHOULD accept a generic argument (or use an equivalent language feature) indicating the type of " +
|
||||
"the transformed context. If such type information is supplied, more accurate type information can be " +
|
||||
"supplied in the flag resolution methods.")
|
||||
@Specification(spec="provider", number="2.10", text="The provider interface MAY define a context transformer method " +
|
||||
@Specification(number="2.11.1", text="If the implementation includes a context transformer, the provider SHOULD accept a generic argument (or use an equivalent language feature) indicating the type of the transformed context. If such type information is supplied, more accurate type information can be supplied in the flag resolution methods.")
|
||||
@Specification(number="2.10", text="The provider interface MAY define a context transformer method " +
|
||||
"or function, which can be optionally implemented in order to transform the evaluation context prior to " +
|
||||
"flag value resolution.")
|
||||
@Disabled("I don't think we should do that until we figure out the call signature differences")
|
||||
@Test void not_doing() {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import java.lang.annotation.Repeatable;
|
|||
|
||||
@Repeatable(Specifications.class)
|
||||
public @interface Specification {
|
||||
String spec();
|
||||
String number();
|
||||
String text();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
import urllib.request
|
||||
import json
|
||||
import re
|
||||
import difflib
|
||||
import os
|
||||
import sys
|
||||
|
||||
def _demarkdown(t):
|
||||
return t.replace('**', '').replace('`', '').replace('"', '')
|
||||
|
||||
def get_spec(force_refresh=False):
|
||||
spec_path = './specification.json'
|
||||
data = ""
|
||||
if os.path.exists(spec_path):
|
||||
with open(spec_path) as f:
|
||||
data = ''.join(f.readlines())
|
||||
else:
|
||||
# TODO: Status code check
|
||||
spec_response = urllib.request.urlopen('https://raw.githubusercontent.com/open-feature/spec/main/specification.json')
|
||||
raw = []
|
||||
for i in spec_response.readlines():
|
||||
raw.append(i.decode('utf-8'))
|
||||
data = ''.join(raw)
|
||||
with open(spec_path, 'w') as f:
|
||||
f.write(data)
|
||||
return json.loads(data)
|
||||
|
||||
|
||||
def main(refresh_spec=False, diff_output=False, limit_numbers=None):
|
||||
actual_spec = get_spec(refresh_spec)
|
||||
|
||||
spec_map = {}
|
||||
for entry in actual_spec['rules']:
|
||||
number = re.search('[\d.]+', entry['id']).group()
|
||||
if 'requirement' in entry['machine_id']:
|
||||
spec_map[number] = _demarkdown(entry['content'])
|
||||
|
||||
if len(entry['children']) > 0:
|
||||
for ch in entry['children']:
|
||||
number = re.search('[\d.]+', ch['id']).group()
|
||||
if 'requirement' in ch['machine_id']:
|
||||
spec_map[number] = _demarkdown(ch['content'])
|
||||
|
||||
java_specs = {}
|
||||
missing = set(spec_map.keys())
|
||||
|
||||
|
||||
import os
|
||||
for root, dirs, files in os.walk(".", topdown=False):
|
||||
for name in files:
|
||||
F = os.path.join(root, name)
|
||||
if '.java' not in name:
|
||||
continue
|
||||
with open(F) as f:
|
||||
data = ''.join(f.readlines())
|
||||
|
||||
for match in re.findall('@Specification\((?P<innards>.*?)"\)', data.replace('\n', ''), re.MULTILINE | re.DOTALL):
|
||||
number = re.findall('number="(.*?)"', match)[0]
|
||||
if number in missing:
|
||||
missing.remove(number)
|
||||
text_with_concat_chars = re.findall('text=(.*)', match)
|
||||
try:
|
||||
# We have to match for ") to capture text with parens inside, so we add the trailing " back in.
|
||||
text = _demarkdown(eval(''.join(text_with_concat_chars) + '"'))
|
||||
entry = java_specs[number] = {
|
||||
'number': number,
|
||||
'text': text,
|
||||
}
|
||||
except:
|
||||
print(f"Skipping {match} b/c we couldn't parse it")
|
||||
|
||||
bad_num = len(missing)
|
||||
for number, entry in java_specs.items():
|
||||
if limit_numbers is not None and len(limit_numbers) > 0 and number not in limit_numbers:
|
||||
continue
|
||||
if number in spec_map:
|
||||
txt = entry['text']
|
||||
if txt == spec_map[number]:
|
||||
# print(f'{number} is good')
|
||||
continue
|
||||
else:
|
||||
print(f"{number} is bad")
|
||||
bad_num += 1
|
||||
if diff_output:
|
||||
print(number + '\n' + '\n'.join([li for li in difflib.ndiff([txt], [spec_map[number]]) if not li.startswith(' ')]))
|
||||
continue
|
||||
|
||||
print(f"Couldn't find the number {number}")
|
||||
print("\n\n")
|
||||
|
||||
if len(missing) > 0:
|
||||
print('Missing: ', missing)
|
||||
|
||||
sys.exit(bad_num)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Parse the spec to make sure our tests cover it')
|
||||
parser.add_argument('--refresh-spec', action='store_true', help='Re-downloads the spec')
|
||||
parser.add_argument('--diff-output', action='store_true', help='print the text differences')
|
||||
parser.add_argument('specific_numbers', metavar='num', type=str, nargs='*',
|
||||
help='limit this to specific numbers')
|
||||
|
||||
args = parser.parse_args()
|
||||
main(refresh_spec=args.refresh_spec, diff_output=args.diff_output, limit_numbers=args.specific_numbers)
|
||||
Loading…
Reference in New Issue