Support for spec validation
This commit is contained in:
parent
01bba2d28b
commit
30c5ac4834
|
|
@ -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,10 @@ 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.")
|
||||
@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.")
|
||||
@Test void provider() {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
FeatureProvider mockProvider = mock(FeatureProvider.class);
|
||||
|
|
@ -35,9 +27,6 @@ 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.")
|
||||
@Test void hook_addition() {
|
||||
Hook h1 = mock(Hook.class);
|
||||
Hook h2 = mock(Hook.class);
|
||||
|
|
@ -52,17 +41,12 @@ 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.")
|
||||
@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.")
|
||||
@Test void hookRegistration() {
|
||||
Client c = _client();
|
||||
Hook m1 = mock(Hook.class);
|
||||
|
|
@ -75,11 +59,6 @@ public class FlagEvaluationSpecTests {
|
|||
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.")
|
||||
@Test void value_flags() {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new DoSomethingProvider());
|
||||
|
|
@ -104,17 +83,6 @@ 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.")
|
||||
@Test void detail_flags() {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new DoSomethingProvider());
|
||||
|
|
@ -148,9 +116,6 @@ public class FlagEvaluationSpecTests {
|
|||
assertEquals(id, c.getIntegerDetails(key, 4, new EvaluationContext(), FlagEvaluationOptions.builder().build()));
|
||||
}
|
||||
|
||||
@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.")
|
||||
@Test void hooks() {
|
||||
Client c = _client();
|
||||
Hook clientHook = mock(Hook.class);
|
||||
|
|
@ -163,10 +128,6 @@ 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.")
|
||||
@Test void broken_provider() {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new AlwaysBrokenProvider());
|
||||
|
|
@ -174,31 +135,38 @@ public class FlagEvaluationSpecTests {
|
|||
assertFalse(c.getBooleanValue("key", false));
|
||||
|
||||
}
|
||||
@Specification(spec="flag evaluation", number="1.19", 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();
|
||||
}
|
||||
|
||||
@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.")
|
||||
@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.")
|
||||
@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.")
|
||||
@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.")
|
||||
@Specification(number="1.1.4", text="The API MUST provide a function for retrieving the metadata field of the configured provider.")
|
||||
@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.")
|
||||
@Specification(number="1.1.6", text="The client creation function MUST NOT throw, or otherwise abnormally terminate.")
|
||||
@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.")
|
||||
@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.")
|
||||
@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.")
|
||||
@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.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.")
|
||||
@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.")
|
||||
@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.")
|
||||
@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.10", text="In the case of abnormal execution, the client SHOULD log an informative error message.")
|
||||
@Specification(number="1.4.11", text="The client SHOULD provide asynchronous or non-blocking mechanisms for flag evaluation.")
|
||||
@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.")
|
||||
@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 @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() {
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,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 +48,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 +112,6 @@ public class HookSpecTests {
|
|||
|
||||
}
|
||||
|
||||
@Specification(spec="hooks", number="1.2", text="Hook context SHOULD provide: provider (instance) and client (instance)")
|
||||
@Test void optional_properties() {
|
||||
// don't specify
|
||||
HookContext.<Integer>builder()
|
||||
|
|
@ -141,7 +140,6 @@ public class HookSpecTests {
|
|||
.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.")
|
||||
@Test void before_runs_ahead_of_evaluation() {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new AlwaysBrokenProvider());
|
||||
|
|
@ -154,14 +152,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());
|
||||
|
|
@ -176,7 +172,6 @@ public class HookSpecTests {
|
|||
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 +180,7 @@ 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")
|
||||
@Test void hook_eval_order() {
|
||||
List<String> evalOrder = new ArrayList<String>();
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
|
|
@ -277,7 +267,7 @@ 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.")
|
||||
@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.")
|
||||
@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() {
|
||||
Hook<Boolean> h = mock(Hook.class);
|
||||
|
|
@ -291,10 +281,6 @@ public class HookSpecTests {
|
|||
}
|
||||
|
||||
|
||||
@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.")
|
||||
@Test void hook_hints() {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new NoOpProvider());
|
||||
|
|
@ -330,15 +316,12 @@ 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.")
|
||||
@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,7 +343,6 @@ 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() {
|
||||
Hook hook = mock(Hook.class);
|
||||
doThrow(RuntimeException.class).when(hook).before(any(), any());
|
||||
|
|
@ -373,7 +355,6 @@ public class HookSpecTests {
|
|||
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.")
|
||||
@Test void multi_hooks_early_out__before() {
|
||||
Hook hook = mock(Hook.class);
|
||||
Hook hook2 = mock(Hook.class);
|
||||
|
|
@ -396,7 +377,6 @@ 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`).")
|
||||
@Test void beforeContextUpdated() {
|
||||
EvaluationContext ctx = new EvaluationContext();
|
||||
Hook hook = mock(Hook.class);
|
||||
|
|
@ -424,7 +404,6 @@ 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.")
|
||||
@Test void mergeHappensCorrectly() {
|
||||
EvaluationContext hookCtx = new EvaluationContext();
|
||||
hookCtx.addStringAttribute("test", "broken");
|
||||
|
|
@ -457,12 +436,30 @@ public class HookSpecTests {
|
|||
}
|
||||
|
||||
|
||||
@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.")
|
||||
@Specification(number="4.1.2", text="The hook context SHOULD provide: access to the client metadata and the provider metadata fields.")
|
||||
@Specification(number="4.1.4", text="The evaluation context MUST be mutable only within the before hook.")
|
||||
@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.2.2.1", text="Condition: Hook hints MUST be immutable.")
|
||||
@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.")
|
||||
@Specification(number="4.3.1", text="Hooks MUST specify at least one stage.")
|
||||
@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.")
|
||||
@Specification(number="4.3.3", text="Any evaluation context returned from a before hook MUST be passed to subsequent before hooks (via HookContext).")
|
||||
@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.")
|
||||
@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.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.")
|
||||
@Specification(number="4.3.8.1", text="Instead of finally, finallyAfter SHOULD be used.")
|
||||
@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.4.3", text="If a finally hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining finally hooks.")
|
||||
@Specification(number="4.4.4", text="If an error hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining error hooks.")
|
||||
@Specification(number="4.4.5", text="If an error occurs in the before or after hooks, the error hooks MUST be invoked.")
|
||||
@Specification(number="4.5.1", text="Flag evaluation options MAY contain hook hints, a map of data to be provided to hook invocations.")
|
||||
@Specification(number="4.5.2", text="hook hints MUST be passed to each hook.")
|
||||
@Specification(number="4.5.3", text="The hook MUST NOT alter the hook hints structure.")
|
||||
@Test @Disabled void todo() {}
|
||||
|
||||
@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);
|
||||
|
|
|
|||
|
|
@ -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,11 +74,8 @@ 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")
|
||||
|
|
|
|||
|
|
@ -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