Class ExpressionLanguageContractTest
class MyLanguageContractTest extends ExpressionLanguageContractTest {
protected ExpressionLanguage language() { return new MyLanguage(); }
protected String alwaysTrue() { return "true"; }
protected String factEquals(String fact, int value) { return fact + " == " + value; }
// ... one method for each expression the checks need
}
Every check but evaluateAgreesWithDetail runs rules through an engine, so a language passes only if it
works with the engine as users will run it. On the module path, a test module of its own, one that requires the kit,
must open the package of the extending test to org.junit.platform.commons. Tests that Surefire patches into
the language's own named module need no opens clause: Surefire gives JUnit access to them.
The checks compare numbers in the output by value, so a language whose whole numbers are Longs, as CEL's
are, or Doubles, as JsonLogic's and JavaScript's are, needs no conversion to pass on that count. What a
language reads is a separate promise, and every language here makes it, whatever its own numbers are: a rule
written for 1 is also run against a Long, a Short and a BigDecimal fact, and must
fire for all three and not for a 2L one. A language that compares whole numbers by type opts out of that
one check with comparesWholeNumbersByValue(). A language that converts the numbers its actions return for
the output's setters, so that a whole double sets an int or a long property, and a fraction, an
infinity, NaN or a whole number the setter's type doesn't hold exactly, as a Double or a BigDecimal,
fails the rule, opts into two more checks with convertsOutputNumbers(), one of which converts on several
threads at once.
- See Also:
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic final classAn account as a JavaBean with a setter taking along, the outputoutputNumbersConvertedwrites a wholeDoublenointholds to.static final recordAn applicant, so a language's tests can run a rule against a record fact.static final classAn applicant as a JavaBean, so a language's tests can run a rule against a fact whose property is a getter.static final classstatic final classThe fact the nested-run checks run their rules against: itsvalueproperty starts a run nested inside the run that reads it, on the same thread, as a getter a condition or an action reads, or a function it calls, does when it runs rules.static final classAn applicant as a JavaBean with a setter, so a language's tests can run a rule against a fact whose property a condition could write. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionprotected StringReturns an action with a syntax error, one this language's own compiler rejects.protected booleanWhether a variable an action declares lasts until the end of the run, so that a later rule's action in the same run reads it, as in a language that keeps each run's variables withEvaluationContext.runScoped(java.lang.Object, java.util.function.Supplier<? extends T>).protected abstract StringReturns a condition that is always true.protected abstract @Nullable Stringassignment(String fact, int value) Returns a condition that assigns a value to a fact, which the language must reject when it loads or runs the rule, rather than evaluate to a boolean.protected @Nullable StringbothConditions(String condition, String other) Returns a condition that is true when both conditions are, and evaluatesconditionbeforeother, ascondition && otherdoes in MVEL.protected @Nullable StringchangeSharedState(String name, int value) Returns an action that changes state the language keeps outside a run's variables and shares between runs, such as a property of a built-in object, asMath.discount = 50is in JavaScript, or a global, setting whatnamerefers to tovalue.protected booleanWhether this language compares whole numbers by value, so that a rule written for1fires for aLong, aShortand aBigDecimalfact that holds one, and doesn't fire for a2Lfact.protected CompileContextReturns the contextevaluateAgreesWithDetailcompiles its condition with, since that check compiles it with the language's compiler, outside an engine, and the context of the compileroutputUnconvertibleValueFailsTheRuleasks to convert a value when the language's actions change the output in place.protected @Nullable StringconditionDeclaration(String name, int value) Returns a condition that declares a variable and is then true, which the language must reject when it loads or runs the rule.protected voidconfigure(RulesEngineBuilder<Map<String, Object>> builder) Configures each engine the checks build, for a language that needs what the builder carries to compile its expressions: declared facts, imports or options of its own.protected booleanWhether this language converts the numbers its actions return for the output's setters, inconvertOutput, so that a wholeDoublereaches a setter taking anintor along, and a fraction, an infinity, NaN or a whole number the setter's type doesn't hold exactly fails the rule rather than being rounded or narrowed.protected @Nullable StringcopyThroughVariable(String key, String fact) Returns an action that declares a variable holding a fact's value, and then puts the variable's value into the output map under a key.protected abstract @Nullable StringdeclareVariable(String name, int value) Returns an action that declares a variable.protected @Nullable StringdeclareVariableThenFail(String name, int value) Returns an action that declares a variable and then fails the run, asy = 2; Integer.parseInt('not a number')does in MVEL.protected @Nullable StringReturns an action that runs until its run passes its deadline, and stops then, such aswhile (true) {}in a language that registers an action withEvaluationContext.onCancel(Runnable)which stops its runtime, or that pollsEvaluationContext.isCancelled().protected abstract StringfactEquals(String fact, int value) Returns a condition that is true when a whole-number fact equals a value.protected abstract StringfactProperty(String fact, String property, int value) Returns a condition that compares one property of a fact with a value, asapplicant.creditScore == 750does in MVEL.protected abstract StringReturns a condition whose result is a fact's value, whatever its type.protected abstract ExpressionLanguagelanguage()Returns the language under test.protected abstract @Nullable StringmissingFactProperty(String fact, String property, int value) Returns a condition that reads a property the fact doesn't have, which the language must reject when it loads or runs the rule, rather than evaluate tofalse.protected @Nullable StringpropertyAssignment(String fact, String property, int value) Returns a condition that writes a property of a fact, which the language must reject when it loads or runs the rule.protected abstract StringReturns an action that puts a fact's value into the output map, by changing the output or by returning the property in anActionResult.protected @Nullable StringputFactProperty(String key, String fact, String property) Returns an action that puts one property of a fact, read through its getter, into the output map under a key.protected StringputVariable(String key, String variable) Returns an action that puts a variable's value into the output map under a key.protected abstract @Nullable StringReturns an action that assigns a new object to the output.protected @Nullable StringsharedStateEquals(String name, int value) Returns a condition that is true when the shared statechangeSharedState(java.lang.String, int)changes holdsvalue, and false, without failing, while it doesn't.protected abstract StringReturns a condition with a syntax error.protected abstract @Nullable StringReturns a fact name that rules in this language can't refer to.protected Collection<String> Returns fact names that rules in this language can refer to, such ascredit_score2.protected StringvariableEquals(String variable, int value) Returns a condition that is true when a whole-number variable equals a value.
-
Constructor Details
-
ExpressionLanguageContractTest
protected ExpressionLanguageContractTest()Creates the test. JUnit creates an instance of the extending class for each check.
-
-
Method Details
-
language
Returns the language under test.- Returns:
- A new instance of the language
-
alwaysTrue
Returns a condition that is always true.- Returns:
- The condition
-
factEquals
Returns a condition that is true when a whole-number fact equals a value.- Parameters:
fact- The fact's namevalue- The value to compare it with- Returns:
- The condition
-
comparesWholeNumbersByValue
protected boolean comparesWholeNumbersByValue()Whether this language compares whole numbers by value, so that a rule written for1fires for aLong, aShortand aBigDecimalfact that holds one, and doesn't fire for a2Lfact. By default,true. A strongly typed language that deliberately compares them by type returnsfalse, which skips the whole check: there's no opting out of one of the three types.- Returns:
- Whether whole numbers of different types compare equal
-
factValue
Returns a condition whose result is a fact's value, whatever its type.- Parameters:
fact- The fact's name- Returns:
- The condition
-
assignment
Returns a condition that assigns a value to a fact, which the language must reject when it loads or runs the rule, rather than evaluate to a boolean.- Parameters:
fact- The fact's namevalue- The value to assign- Returns:
- The condition, or
nullif the language's conditions can't express an assignment, which skips the check
-
propertyAssignment
Returns a condition that writes a property of a fact, which the language must reject when it loads or runs the rule.conditionWritesRejectedruns the same condition against aMapfact and aExpressionLanguageContractTest.WritableApplicantbean fact. The condition must also write a bean's property, as MVEL'sapplicant.creditScore = 1does. Each fact must be unchanged afterwards: a language that fails the rule only after the write has still changed the caller's fact. By default,null.- Parameters:
fact- The fact's nameproperty- The property to writevalue- The value to write- Returns:
- The condition, or
nullif the language's conditions can't write a property, which skips that part of the check
-
conditionDeclaration
Returns a condition that declares a variable and is then true, which the language must reject when it loads or runs the rule.conditionWritesRejectednames a variable that is none of the checks' facts, so a language that declares its facts can't take the declaration for an assignment to one. By default,null.- Parameters:
name- The variable's namevalue- The value it holds- Returns:
- The condition, or
nullif the language's conditions can't declare a variable, which skips that part of the check
-
putFact
Returns an action that puts a fact's value into the output map, by changing the output or by returning the property in anActionResult.- Parameters:
key- The key to put the value underfact- The fact's name- Returns:
- The action
-
declareVariable
Returns an action that declares a variable.- Parameters:
name- The variable's namevalue- The value it holds- Returns:
- The action, or
nullif the language's actions have no variables, which skips the check
-
putVariable
Returns an action that puts a variable's value into the output map under a key. The variable checks read the variable an earlier action declared with it. By default,putFact(key, variable), for a language that reads a variable as it reads a fact. A language whose variables have a namespace of their own, such as SpEL's#y, overrides it, andvariableEquals(java.lang.String, int)too: a variable read as a fact is never found, and the checks pass whether it leaked or not.- Parameters:
key- The key to put the value undervariable- The variable's name- Returns:
- The action
-
variableEquals
Returns a condition that is true when a whole-number variable equals a value.actionVariablesStayLocalreads with it the variable an earlier run's action declared. By default,factEquals(variable, value), for a language that reads a variable as it reads a fact; seeputVariable(java.lang.String, java.lang.String).- Parameters:
variable- The variable's namevalue- The value to compare it with- Returns:
- The condition
-
declareVariableThenFail
Returns an action that declares a variable and then fails the run, asy = 2; Integer.parseInt('not a number')does in MVEL.failedActionVariablesStayLocalchecks that a later run doesn't read the variable, as a language that clears its variables only when an action succeeds would let it. The action must declare the variable before it fails: one that fails first declares nothing, and the check proves nothing. By default,null.- Parameters:
name- The variable's namevalue- The value it holds- Returns:
- The action, or
nullif the language's actions have no variables, which skips the check
-
copyThroughVariable
Returns an action that declares a variable holding a fact's value, and then puts the variable's value into the output map under a key.concurrentRunsruns it in many runs at once, each with its own value, so it is likely to catch an action whose variables every run shares, such as a map compiled into the action or a static: such an action puts another run's value, or fails, when another run changes the variable between its declaring the variable and putting it. By default,null.- Parameters:
key- The key to put the value underfact- The fact's name- Returns:
- The action, or
nullif the language's actions have no variables, which leaves the variable out ofconcurrentRunsrather than skipping it
-
actionVariablesLastTheRun
protected boolean actionVariablesLastTheRun()Whether a variable an action declares lasts until the end of the run, so that a later rule's action in the same run reads it, as in a language that keeps each run's variables withEvaluationContext.runScoped(java.lang.Object, java.util.function.Supplier<? extends T>). By default,false: a variable stays local to the action that declares it. Returningtrueturns one part ofactionVariablesStayLocalaround: a later rule in the same run must read the variable, so the check fails whenload()refuses that rule, when it fails the run, or when it reads anything but the variable's value. The rest of the check is unchanged: the variable still mustn't change the facts later actions see, and no later run may read it.- Returns:
- Whether an action's variables last the run
-
convertsOutputNumbers
protected boolean convertsOutputNumbers()Whether this language converts the numbers its actions return for the output's setters, inconvertOutput, so that a wholeDoublereaches a setter taking anintor along, and a fraction, an infinity, NaN or a whole number the setter's type doesn't hold exactly fails the rule rather than being rounded or narrowed. By default,false, which skipsoutputNumbersConverted: a language whose whole numbers are Java's,Integers orLongs, needs no conversion to pass the other checks, and the default writer widens anIntegerfor alongsetter by itself. A language whose numbers are all doubles, as JavaScript's and JsonLogic's are, opts in once it converts them; so may any language that converts a whole double its action returns, as one that reads aDoublefact and returns it does.The check runs
putFact("creditScore", "x")against a run whose factxis theDouble1.0, which must set the credit score to 1, and then theDouble1.5, which must fail the rule and leave the score, with a writer that sets each property on aExpressionLanguageContractTest.WritableApplicantthrough the default writer, as an application whose output is a bean has it set. Then 3.0E9,Infinity,-Infinity,NaNand -3.0E9, which must each fail the rule and leave the score too: a language that narrows a double it has only checked is whole setsInteger.MAX_VALUEfor the first two, and one that casts a whole double it has bounded above only setsInteger.MIN_VALUEfor the last. Then theBigDecimals 1.5 and 3E9, which must each fail the rule and leave the score as well: a language whose decimals areBigDecimals, as Groovy's are, and narrows one withintValue()sets 1 and the low 32 bits. ThenputFact("balance", "x")on anExpressionLanguageContractTest.Account, whose setter takes along: 3.0E9 must set the balance to 3000000000, andInfinity, 253 + 2, the first whole double past where a double counts by ones, 1.5,NaN,-Infinityand -(253 + 2) must each fail the rule and leave it: a language that rounds withMath.round()sets 2 for 1.5, 0 forNaNandLong.MIN_VALUEfor-Infinity. Then theBigDecimals 1.5 and 1E19, which must each fail the rule and leave the balance: a language that narrows one withlongValue()sets 1 and the low 64 bits. Its engines are built as every other check's is, soconfigure(io.github.brantunger.unruly.api.RulesEngineBuilder<java.util.Map<java.lang.String, java.lang.Object>>)applies.It opts into
outputNumbersConvertedConcurrentlytoo, which converts whole doubles for anintsetter on eight threads at once, asconvertOutputmust be thread-safe.- Returns:
- Whether the language converts the numbers its actions return for a setter
-
reassignOutput
Returns an action that assigns a new object to the output.- Returns:
- The action, or
nullif the language's actions can't assign anything, as for a language that returns its results as properties, which skips the check
-
syntaxError
Returns a condition with a syntax error.- Returns:
- The condition
-
actionSyntaxError
Returns an action with a syntax error, one this language's own compiler rejects. By default, whateversyntaxError()returns, which is a broken action in most languages. Override it when that text is a valid action in this one: the language then compiles it without complaint, and the check fails with nothing but JUnit's "expectedRuleCompilationExceptionto be thrown", naming neither the expression nor the hook that supplied it.The action must not be blank, and the check fails when it is: the engine rejects a blank action itself, naming the rule, before the language is asked to compile anything, so a blank one would pass the check without the language's compiler ever running. Unlike the
@Nullablehooks around it, this one can't be skipped by returningnull.- Returns:
- The action, never blank
-
unusableFactName
Returns a fact name that rules in this language can't refer to. It must not be blank or one of the names the language reserves, asExpressionLanguage.reservedFactNames()returns them ("output"by default), and the check fails when it is: the engine rejects those names itself, before the language is asked, so they would pass the check without the language'scheckFactNameever running. Nor may it be"x", the fact the check's rule reads, and the check fails when it is too.- Returns:
- The name, or
nullif every name is accepted
-
usableFactNames
Returns fact names that rules in this language can refer to, such ascredit_score2. Each is read by a condition and put into the output by an action, and the check fails if the language rejects the name or the rule doesn't fire. By default, none, which skips the check. List the names a language's users will write, so that acheckFactNamestricter than the language itself fails here rather than in every run with that fact.- Returns:
- The names, or an empty collection to skip the check
-
factProperty
Returns a condition that compares one property of a fact with a value, asapplicant.creditScore == 750does in MVEL. The same condition is run against a record fact, a JavaBean fact and aMapfact.- Parameters:
fact- The fact's nameproperty- The property to readvalue- The value it must equal- Returns:
- The condition
-
missingFactProperty
Returns a condition that reads a property the fact doesn't have, which the language must reject when it loads or runs the rule, rather than evaluate tofalse. It's usuallyfactProperty(java.lang.String, java.lang.String, int)with the same arguments.A language whose own semantics read a missing property as
nullor undefined, as JsonLogic reads avarthat isn't there, returnsnull. Such a language can't tell a misspelled property from an absent one, whether it reads the fact directly or as the mapFactProperties.toDatamakes of it, so the check would fail it for being faithful to its own rules.- Parameters:
fact- The fact's nameproperty- The property the fact doesn't havevalue- The value to compare it with- Returns:
- The condition, or
nullif the language reads a missing property asnullor undefined, which skips the check
-
putFactProperty
Returns an action that puts one property of a fact, read through its getter, into the output map under a key.nestedRunInsideAnAction,nestedRunFailsInsideAnActionandnestedRunFailureRethrownAsIsrun it against aExpressionLanguageContractTest.Nestingfact, whose getter starts a run nested inside the action, on the action's own thread. By default,null.- Parameters:
key- The key to put the value underfact- The fact's nameproperty- The property to read- Returns:
- The action, or
nullif the language's actions can't read a fact's property, which skips the checks
-
bothConditions
Returns a condition that is true when both conditions are, and evaluatesconditionbeforeother, ascondition && otherdoes in MVEL.nestedRunInsideAConditionandnestedRunFailsInsideAConditionjoinfactProperty("nest", "value", 7), whose getter starts a run nested inside the condition, withfactEquals("x", 1), so that the second read is the one that finds any state the nested run left on the thread. A language that evaluates the right side first isn't checked by them: the first passes it, and the second is skipped when the nested run, stopping at the right side, never reads the getter that makes it fail. By default,null.- Parameters:
condition- The condition evaluated firstother- The condition evaluated second- Returns:
- The condition, or
nullif the language's conditions can't require both of two conditions, which skips the checks
-
endlessAction
Returns an action that runs until its run passes its deadline, and stops then, such aswhile (true) {}in a language that registers an action withEvaluationContext.onCancel(Runnable)which stops its runtime, or that pollsEvaluationContext.isCancelled().endlessActionStopsAtTimeoutruns it with a short run timeout, and checks that the run fails with the timeout well within 30 seconds, without its thread interrupted. An action that never stops leaves the thread that runs it going once the check has failed. By default,null.- Returns:
- The action, or
nullif the language can't stop an action part-way, which skips the check
-
configure
Configures each engine the checks build, for a language that needs what the builder carries to compile its expressions: declared facts, imports or options of its own. By default, nothing. It's called once for each engine, after the kit has started anallMatchesengine whose output is aHashMap, and added the language.The facts the checks' expressions refer to are
x,y,applicantandnest, and the namesusableFactNames()returns. The checks supplyxas aBoolean, aString,null, anInteger, aLong, aShortand aBigDecimal,yas anInteger,applicantas anExpressionLanguageContractTest.Applicant, anExpressionLanguageContractTest.ApplicantBean, aExpressionLanguageContractTest.WritableApplicantand aMap,nestas aExpressionLanguageContractTest.Nesting, and each of the namesusableFactNames()andunusableFactName()return as theInteger1. So a language that declares them declaresxandapplicantasObject, andnestasObjectorExpressionLanguageContractTest.Nesting: the engine fails a run whose fact isn't an instance of its declared type with anIllegalArgumentException, before the language evaluates anything, and the check with it. Nor may the language reservex,y,applicantornest, asExpressionLanguage.reservedFactNames()returns them: every check that builds an engine then fails, before it builds one, with the one message that it reserves them.It must not change what the checks depend on, or they fail for reasons that have nothing to do with the language:
requireDeclaredFacts(): each run supplies only the facts its check reads, so the engine fails it with anIllegalArgumentExceptionbefore the language evaluates anything.- A declaration of the name
unusableFactName()returns:load()checks every declared name with the language, or, for a compiler whosefactNamesRead()returns a set, every declared name in it, and the language rejects that one. Most checks then fail to load their rules, with aRuleCompilationException, and the checks that expect a load to fail then pass or fail for reasons unrelated to the language. runTimeout(...): a run that outlasts it fails, and its check with it.maxCopies(...)below 2: two checks make two copies of the rules when they load, whichbuild()refuses with more copies than the limit. A higher limit also caps how many sessionsconcurrentRunsasks for, somaxCopies(2)hides anewSession()that returns a session twice only after its second call.- Another language, or a
defaultLanguage(...)other than the language's own name:build()throws anIllegalStateExceptionfor an engine with two languages and no default, or with a default that isn't one of its languages.defaultLanguage(language().name())changes nothing. outputWriter(...): the checks read what an action returned from the output map, whereOutputWriter.beansAndMaps()puts it.
The two checks that need copies made when the rules load,
copiesAtLoadandsessionsClosed, setcopiesAtLoad(2)after this.compilerClosedsetscopiesAtLoad(1), so that each compiler has made a session when the rules load before it's closed; the engine warms up each session that isn'tSession.none(), which has nothing to warm up. The two that need a run to make its own copy,sessionClosedWhileAnotherRunsandsessionClosedOnAnotherThread, setcopiesAtLoad(0), so acopiesAtLoadset here doesn't change them.sessionClosedWhileAnotherRunsalso setsmaxCopies(1)after this, so that a run nested in another gets an extra copy, and a limit set here doesn't change it either;concurrentRunssets none, so a limit set here caps the copies, and the sessions, its runs get. The checks that look at what a run leaves for a later one,actionVariablesStayLocalin its parts with a later run,failedActionVariablesStayLocal,sharedStateStaysLocalandconditionDetail, and those that run a rejected rule a second time,conditionAssignmentRejected,conditionWritesRejected,outputNotReplaceableandmissingPropertyFailsTheRun, setmaxCopies(1)andcopiesAtLoad(0)after this, so that each run gets the one copy, and its sessions, that the run before it used: with two copies made when the rules load, a later run would get the other one. Listeners that don't change the output may be added: the engine only logs what a listener throws, unless it's a fatalError, butbeforeExecuteandafterExecuteare given the output the checks compare.evaluateAgreesWithDetailbuilds no engine, and compiles withcompileContext()instead, asoutputUnconvertibleValueFailsTheRuledoes for a language whose actions change the output in place: a language that overrides both keeps them consistent.- Parameters:
builder- The builder of an engine a check is about to build
-
compileContext
Returns the contextevaluateAgreesWithDetailcompiles its condition with, since that check compiles it with the language's compiler, outside an engine, and the context of the compileroutputUnconvertibleValueFailsTheRuleasks to convert a value when the language's actions change the output in place. By default,LanguageTestContexts.compile(), which has no imports, options or declared facts.A language that needs
configure(io.github.brantunger.unruly.api.RulesEngineBuilder<java.util.Map<java.lang.String, java.lang.Object>>)to compile needs this too, with the same imports, options and declared facts, which theLanguageTestContextscompilemethods take. The conditions arefactEquals("x", 1), evaluated for anxthat is anInteger, aLong, aShortand aBigDecimal, thenalwaysTrue(),factValue("x"), for aBooleanx, andbothConditions(java.lang.String, java.lang.String): a context that declaresxas a number may make the compiler refusefactValue("x"), which skips it.- Returns:
- The context
-