package software.amazon.event.ruler; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * Machines whose complexity evaluation has a known cost, shared by {@link MachineComplexityEvaluatorRegressionTest} * (deterministic pins plus generous time bounds, runs in every build) and {@link MachineComplexityEvaluatorBenchmarks} * (timings, opt-in). * *
Every machine is built through the public {@code Machine.addRule} API rather than a bare {@code ByteMachine}, so an
* evaluation walks the NameState graph too — the recursion into next NameStates across exact-match value lists, and the
* absent-key ({@code {"exists": false}}) edges — which a benchmark over a single ByteMachine cannot see.
*/
final class MachineComplexityEvaluatorCorpus {
/**
* {@code maxComplexity} of a consumer enforcing a per-rule maximum of 10 (README, complexity strategy #2): 10 + 1,
* because the README's snippet rejects on {@code complexity > max}, so the evaluator must be able to report max + 1.
*/
static final int PRODUCTION_CAP = 11;
/** A cap no machine here reaches: the evaluator reports the machine's true complexity. */
static final int UNCAPPED = 1_000_000;
/** The four anything-but wildcard strings of {@link MachineComplexityEvaluatorWalkCountTest}, all leading with a star. */
private static final String[] LEADING_STAR_STRINGS = {
"*kiwi*", "*BASE_QuxB*", "*Delta*", "*a/example-lake-abc98765432*",
};
/** The wildcard set that "explodes" a naive automaton; also in {@code MachineComplexityEvaluatorTest}. */
private static final String[] QUAMINA_EXPLODER = {
"aahed*", "aal*ii", "aargh*", "aarti*", "a*baca", "*abaci", "a*back", "ab*acs", "abaf*t", "*abaka", "ab*amp",
"a*band", "*abase", "abash*", "abas*k", "ab*ate", "aba*ya", "abbas*", "abbed*", "ab*bes", "abbey*", "*abbot",
"ab*cee", "abea*m", "abe*ar", "a*bele", "a*bers", "abet*s", "*abhor", "abi*de", "a*bies", "*abled",
};
private MachineComplexityEvaluatorCorpus() {
}
/**
* How expensive an uncapped evaluation of an entry is, which decides where it runs. Declared in ascending cost
* order; the benchmark compares on it.
*/
enum Tier {
/** Milliseconds: the regression test evaluates it uncapped and pins the result; the benchmark repeats it. */
FAST,
/**
* Seconds and gigabytes of transient allocation: the regression test evaluates it only under
* {@link #PRODUCTION_CAP}; the benchmark evaluates it uncapped once ({@code -Druler.perf.heavy=true}, the
* default).
*/
HEAVY,
/**
* Minutes, with several gigabytes of live heap: the regression test evaluates it only under
* {@link #PRODUCTION_CAP}, as the guard that the cap bounds the walk; the benchmark evaluates it uncapped once
* only with {@code -Druler.perf.heavy=all}.
*/
EXPENSIVE
}
/**
* One machine of the corpus.
*/
static final class Entry {
/** Short name, used in test names, benchmark output and {@code -Druler.perf.only}. */
final String name;
/** The rule JSON the machine is built from. */
private final String rule;
/** The complexity an uncapped evaluation reports. */
final int expectedComplexity;
/**
* How many ByteMachines an evaluation walks: one per key holding value patterns on every NameState the walk
* reaches, exact-match keys included; an absent-key pattern adds no ByteMachine. The count is the same capped
* and uncapped for every entry here: a capped walk stops recursing into the NameStates behind a ByteMachine
* that reaches the cap, and no entry puts a cap-reaching key ahead of another key. An entry that does needs a
* capped and an uncapped count.
*/
final int expectedWalks;
/** Cost class of the uncapped evaluation. */
final Tier tier;
private Entry(String name, String rule, int expectedComplexity, int expectedWalks, Tier tier) {
this.name = name;
this.rule = rule;
this.expectedComplexity = expectedComplexity;
this.expectedWalks = expectedWalks;
this.tier = tier;
}
Machine build() throws Exception {
Machine machine = new Machine();
machine.addRule(name, rule);
return machine;
}
/** The name: JUnit's parameterized runner uses it to label each entry's tests. */
@Override
public String toString() {
return name;
}
}
/**
* Counts how many ByteMachine walks an evaluation starts: {@code ByteMachine.evaluateComplexity} calls
* {@code evaluate(ByteState)} exactly once, so the count is the number of ByteMachine evaluations.
*/
static final class WalkCountingEvaluator extends MachineComplexityEvaluator {
private int walks = 0;
WalkCountingEvaluator(int maxComplexity) {
super(maxComplexity);
}
@Override
int evaluate(ByteState state) {
walks++;
return super.evaluate(state);
}
int getWalks() {
return walks;
}
}
/**
* The fixed corpus and its expected complexity values. A change in an expected value changes the meaning of
* the metric and needs a deliberate decision.
*/
static List"}} patterns. */
private static String wildcards(String... values) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < values.length; i++) {
sb.append(i > 0 ? ", " : "").append("{\"wildcard\": \"").append(values[i]).append("\"}");
}
return sb.append(']').toString();
}
/** A JSON array holding one {@code {"anything-but": {"wildcard": [...]}}} pattern over all the values. */
private static String anythingButWildcards(String... values) {
StringBuilder sb = new StringBuilder("[{\"anything-but\": {\"wildcard\": [");
for (int i = 0; i < values.length; i++) {
sb.append(i > 0 ? ", " : "").append('"').append(values[i]).append('"');
}
return sb.append("]}}]").toString();
}
/** A JSON array of quoted exact-match values. */
private static String exactValues(List