This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 1a1cab52430b CAMEL-26677: camel-chaos - expectations on the error
handler (#29313)
1a1cab52430b is described below
commit 1a1cab52430b97e067616d0255c4a3c0b2215e4b
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 6 10:44:55 2026 +0100
CAMEL-26677: camel-chaos - expectations on the error handler (#29313)
* CAMEL-26675: camel-chaos - report accuracy
- The send of the dead letter channel is a hook of its own (at=dlq): an
endpoint= or component= rule no longer fails it by accident, and a rule
with at=dlq that keeps a message from the dead letter channel counts it
as lost, with a FAIL verdict.
- A route counts an exchange once (the last pass), not once per pass:
direct routes called in a loop or again by a redelivery, expectations
and the entry faults.
- An expectation of a removed or changed rule, or of a stopped context, is
cancelled, so the report can be final again.
- chaos-clear --reset (also reset=true of the MCP and camel ask tool, and
ChaosManager.reset()) starts a new report.
- The rules of camel.chaos.rules start their clock when the context has
started.
- The report lists the rules by id, and the versions of an id from the
latest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-26676: camel-chaos - feedback while adding rules
- A rule that matches nothing in the context now (no endpoint, route or
step id, at=poll without a scheduled poll consumer, an expectation of a
route that does not exist) is added with a warning that names some of
what is there: logged, returned by ChaosManager.checkRule, printed by
chaos-add and chaos-change, and passed through the MCP and camel ask
tools.
- ChaosManager.addRules adds rules separated by ; together, all or none;
chaos-add takes --rule more than once, and the add tool several rules.
- A skipped rule counts why (skippedReasons), in the report and the CLI.
- camel get chaos has a NOTE column (what a lifecycle rule does to its
route, why a rule was skipped), and says on stderr when there is
nothing to show.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-26677: camel-chaos - expectations on the error handler
An expectation names what it counts in its route and how:
expect=<route>:<kind><op><count>, with the kinds completed, handled (by
the error handler, as the dead letter channel), failed and redeliveries,
and the ops >= (at least), <= (at most) and = (exactly). expect=orders:100
stays at least 100 completed exchanges.
At least is met when the count is reached; at most and exactly are unmet
as soon as the count goes over, and decided when the time is up. So
expect=orders:handled<=0 checks that redelivery recovers every message,
and expect=orders:handled>=5 that 5 reach the dead letter channel, also
one that no route consumes. Only an unmet expectation of completed
exchanges counts messages as lost; the others give their own reason.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../org/apache/camel/catalog/docs/chaos.adoc | 44 ++++-
components/camel-chaos/src/main/docs/chaos.adoc | 44 ++++-
.../camel/component/chaos/ChaosEventNotifier.java | 2 +-
.../camel/component/chaos/ChaosExchangeState.java | 7 +-
.../apache/camel/component/chaos/ChaosManager.java | 35 +++-
.../apache/camel/component/chaos/ChaosReport.java | 46 +++--
.../apache/camel/component/chaos/ChaosRule.java | 136 +++++++++++----
.../camel/component/chaos/ChaosRuleParser.java | 49 +++++-
.../chaos/ChaosErrorHandlerExpectationTest.java | 185 +++++++++++++++++++++
.../camel/component/chaos/ChaosRuleParserTest.java | 27 +++
.../camel/component/chaos/ChaosRuleTest.java | 14 +-
.../dsl/jbang/core/commands/process/ListChaos.java | 12 +-
.../jbang/core/commands/process/ListChaosTest.java | 15 ++
13 files changed, 550 insertions(+), 66 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
index bb20880b7ede..6f83ea053c0f 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
@@ -175,7 +175,9 @@ at: send | poll | entry | step (optional)
rate: a probability (rate=0.3), or every Nth match (rate=every:5); without
it every match fires
until: a number of matches (until=100) or a time (until=2m); without it
camel.chaos.maxDuration
time: a number with a unit (ms, s, m, h, d), also combined as 1m30s; a
number alone is milliseconds
-expect: expect=<route id>:<count>, with an optional within=<time>
+expect: expect=<route id>:<count> (at least that many completed exchanges), or
+ expect=<route id>:<kind><op><count> with kind completed | handled |
failed | redeliveries
+ and op >= | <= | = (such as expect=orders:handled<=0); with an
optional within=<time>
----
The patterns of the targets are matched as text, with `*` for any characters;
they are not regular expressions. The
@@ -463,6 +465,46 @@ with a pending expectation as not decided yet. An
expectation is cancelled when
when the context stops first (also when the context is started again): the
messages may still arrive, so it is
neither met nor unmet, and it does not keep the report from being final.
+=== Expectations on the error handler
+
+An exchange that the dead letter channel handled counts as completed, so
`expect=orders:100` is met while every
+message goes to the dead letter channel. To check the error handler itself, an
expectation names what it counts and how,
+`expect=<route id>:<kind><op><count>`:
+
+[cols="1,3"]
+|===
+|Kind |Counts, in the route, after the rule is added
+
+|`completed` |The exchanges that completed (also after the error handler
handled a failure); `orders:100` is
+`orders:completed>=100`.
+|`handled` |The completed exchanges after the error handler handled a failure:
the dead letter channel, or `onException`
+with `handled(true)`.
+|`failed` |The exchanges that failed (also marked rollback only).
+|`redeliveries` |The redelivery attempts of the error handler, in the route
that runs the step it redelivers.
+|===
+
+The op is `>=` (at least), `<=` (at most) or `=` (exactly). At least is met as
soon as the count is reached, and unmet
+when the time is up without it. At most and exactly are unmet as soon as the
count goes over, and decided when the time
+is up otherwise: an at most is met then, an exactly when the count is that
many. Each exchange counts once per kind (the
+first time it completes, is handled or fails in the route).
+
+For example, with redelivery that should recover from a broker that fails one
send in ten:
+
+[source,properties]
+----
+camel.chaos.rules = endpoint=kafka:orders* error rate=0.1 until=2m
expect=orders:handled<=0
+----
+
+The verdict is FAIL as soon as one message goes to the dead letter channel,
with the reason
+(`route orders: 1 exchange handled by the error handler within 2m0s, at most 0
expected (rule r1)`), and PASS when the
+2 minutes are up without one. `expect=orders:handled>=5` checks that 5
messages reach the dead letter channel, also when
+it is an endpoint that no route consumes, and `expect=orders:redeliveries<=30`
that redelivery does not retry too much.
+Only an unmet expectation of completed exchanges (at least or exactly) counts
the missing exchanges as lost; the others
+give their reason only.
+
+In JSON an expectation has its `kind`, `op`, `count` and the `value` counted
so far (and `completed`, the same value, for
+an expectation of completed exchanges).
+
== Time and the seed
The `delay` fault waits through `TimeSource` of camel-util, and the end of a
rule, the time of an expectation and the
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc
b/components/camel-chaos/src/main/docs/chaos.adoc
index bb20880b7ede..6f83ea053c0f 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -175,7 +175,9 @@ at: send | poll | entry | step (optional)
rate: a probability (rate=0.3), or every Nth match (rate=every:5); without
it every match fires
until: a number of matches (until=100) or a time (until=2m); without it
camel.chaos.maxDuration
time: a number with a unit (ms, s, m, h, d), also combined as 1m30s; a
number alone is milliseconds
-expect: expect=<route id>:<count>, with an optional within=<time>
+expect: expect=<route id>:<count> (at least that many completed exchanges), or
+ expect=<route id>:<kind><op><count> with kind completed | handled |
failed | redeliveries
+ and op >= | <= | = (such as expect=orders:handled<=0); with an
optional within=<time>
----
The patterns of the targets are matched as text, with `*` for any characters;
they are not regular expressions. The
@@ -463,6 +465,46 @@ with a pending expectation as not decided yet. An
expectation is cancelled when
when the context stops first (also when the context is started again): the
messages may still arrive, so it is
neither met nor unmet, and it does not keep the report from being final.
+=== Expectations on the error handler
+
+An exchange that the dead letter channel handled counts as completed, so
`expect=orders:100` is met while every
+message goes to the dead letter channel. To check the error handler itself, an
expectation names what it counts and how,
+`expect=<route id>:<kind><op><count>`:
+
+[cols="1,3"]
+|===
+|Kind |Counts, in the route, after the rule is added
+
+|`completed` |The exchanges that completed (also after the error handler
handled a failure); `orders:100` is
+`orders:completed>=100`.
+|`handled` |The completed exchanges after the error handler handled a failure:
the dead letter channel, or `onException`
+with `handled(true)`.
+|`failed` |The exchanges that failed (also marked rollback only).
+|`redeliveries` |The redelivery attempts of the error handler, in the route
that runs the step it redelivers.
+|===
+
+The op is `>=` (at least), `<=` (at most) or `=` (exactly). At least is met as
soon as the count is reached, and unmet
+when the time is up without it. At most and exactly are unmet as soon as the
count goes over, and decided when the time
+is up otherwise: an at most is met then, an exactly when the count is that
many. Each exchange counts once per kind (the
+first time it completes, is handled or fails in the route).
+
+For example, with redelivery that should recover from a broker that fails one
send in ten:
+
+[source,properties]
+----
+camel.chaos.rules = endpoint=kafka:orders* error rate=0.1 until=2m
expect=orders:handled<=0
+----
+
+The verdict is FAIL as soon as one message goes to the dead letter channel,
with the reason
+(`route orders: 1 exchange handled by the error handler within 2m0s, at most 0
expected (rule r1)`), and PASS when the
+2 minutes are up without one. `expect=orders:handled>=5` checks that 5
messages reach the dead letter channel, also when
+it is an endpoint that no route consumes, and `expect=orders:redeliveries<=30`
that redelivery does not retry too much.
+Only an unmet expectation of completed exchanges (at least or exactly) counts
the missing exchanges as lost; the others
+give their reason only.
+
+In JSON an expectation has its `kind`, `op`, `count` and the `value` counted
so far (and `completed`, the same value, for
+an expectation of completed exchanges).
+
== Time and the seed
The `delay` fault waits through `TimeSource` of camel-util, and the end of a
rule, the time of an expectation and the
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosEventNotifier.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosEventNotifier.java
index e96be10d638d..20146e55679a 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosEventNotifier.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosEventNotifier.java
@@ -61,7 +61,7 @@ final class ChaosEventNotifier extends EventNotifierSupport {
Route route = uow != null ? uow.getRoute() : null;
String routeId = route != null ? route.getRouteId() :
exchange.getFromRouteId();
if (routeId != null) {
- manager.counts(routeId).redeliveries.increment();
+ manager.redelivered(routeId);
}
}
}
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExchangeState.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExchangeState.java
index c0ad44e41f23..a11e49945e5b 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExchangeState.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExchangeState.java
@@ -58,8 +58,11 @@ final class ChaosExchangeState implements SafeCopyProperty {
// the counts of the route the outcome is counted in: other counts
(after a reset of the report) have not
// counted the exchange
ChaosManager.RouteCounts counts;
- // the expectations of the route counted the exchange as completed
(once, also when it completes again)
- boolean expected;
+ // the expectations of the route counted the exchange as completed,
handled or failed (each once, also when the
+ // exchange goes through the route again)
+ boolean expectedCompleted;
+ boolean expectedHandled;
+ boolean expectedFailed;
}
private final Map<String, RouteState> routes = new HashMap<>(4);
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
index 8882016fb92b..ba1daa0e85ef 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
@@ -391,7 +391,8 @@ public final class ChaosManager {
* An exchange counts once per route, with how the route was done with it
the last time: camel-core calls the route
* policy each time the exchange goes through the route (a {@code direct}
route called in a loop, or again by a
* redelivery of the calling route), so a later pass moves the count (a
failed pass and then a completed one count
- * as one completed exchange). The expectations of the route count it the
first time it completes.
+ * as one completed exchange). The expectations of the route count it the
first time it completes, is handled by the
+ * error handler, or fails.
*/
void routeDone(Route route, Exchange exchange) {
String routeId = route.getRouteId();
@@ -406,12 +407,30 @@ public final class ChaosManager {
counts.count(outcome);
state.outcome = outcome;
state.counts = counts;
- if (outcome.completed() && !state.expected) {
- state.expected = true;
- for (ChaosRule.Expectation expectation : ruleSet.expectations) {
- if (expectation.route.equals(routeId)) {
- expectation.complete();
- }
+ if (outcome.completed() && !state.expectedCompleted) {
+ state.expectedCompleted = true;
+ record(routeId, ChaosRule.Expectation.Kind.COMPLETED);
+ }
+ if (outcome == ChaosExchangeState.Outcome.HANDLED &&
!state.expectedHandled) {
+ state.expectedHandled = true;
+ record(routeId, ChaosRule.Expectation.Kind.HANDLED);
+ }
+ if (!outcome.completed() && outcome !=
ChaosExchangeState.Outcome.ENTERED && !state.expectedFailed) {
+ state.expectedFailed = true;
+ record(routeId, ChaosRule.Expectation.Kind.FAILED);
+ }
+ }
+
+ /** The error handler redelivers a step of the route: counted for the
report and the expectations. */
+ void redelivered(String routeId) {
+ counts(routeId).redeliveries.increment();
+ record(routeId, ChaosRule.Expectation.Kind.REDELIVERIES);
+ }
+
+ private void record(String routeId, ChaosRule.Expectation.Kind kind) {
+ for (ChaosRule.Expectation expectation : ruleSet.expectations) {
+ if (expectation.kind == kind && expectation.route.equals(routeId))
{
+ expectation.record();
}
}
}
@@ -605,7 +624,7 @@ public final class ChaosManager {
ChaosRule.Expectation.Outcome outcome = reading != null ?
reading.outcome() : null;
if (outcome == ChaosRule.Expectation.Outcome.UNMET) {
evictedUnmet++;
- evictedLost += Math.max(0, expectation.count -
reading.completed());
+ evictedLost += expectation.lost(reading);
} else if (outcome == ChaosRule.Expectation.Outcome.PENDING) {
// a pending expectation that left the report keeps the report
from being final
evictedPending++;
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
index 1cef8c19cbef..6cc1e99a1813 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
@@ -92,11 +92,17 @@ public final class ChaosReport {
}
ChaosRule rule = entry.rule;
if (entry.outcome == Outcome.UNMET) {
- long missing = Math.max(0, rule.expectation.count -
entry.completed);
- lostCount += missing;
- reasons.add("route " + rule.expectation.route + " completed "
+ entry.completed + " of "
- + rule.expectation.count + " exchanges within " +
ChaosRuleParser.printTime(rule.within)
- + " (rule " + rule.id + "): " + missing + " lost");
+ ChaosRule.Expectation e = rule.expectation;
+ lostCount += entry.lost;
+ if (e.kind == ChaosRule.Expectation.Kind.COMPLETED && e.op ==
ChaosRule.Expectation.Op.AT_LEAST) {
+ reasons.add("route " + e.route + " completed " +
entry.value + " of " + e.count + " exchanges within "
+ + ChaosRuleParser.printTime(rule.within) + "
(rule " + rule.id + "): " + entry.lost
+ + " lost");
+ } else {
+ reasons.add("route " + e.route + ": " + entry.value + " "
+ e.kind.words(entry.value) + " within "
+ + ChaosRuleParser.printTime(rule.within) + ",
" + e.op.words + " " + e.count
+ + " expected (rule " + rule.id + ")" +
(entry.lost > 0 ? ": " + entry.lost + " lost" : ""));
+ }
} else if (entry.outcome == Outcome.PENDING) {
anyPending = true;
}
@@ -208,9 +214,9 @@ public final class ChaosReport {
sb.append("\n ").append(lifecycleText(r));
}
if (r.outcome != null) {
- sb.append("\n expect
").append(r.expectRoute).append(':').append(r.expectCount).append(" within ")
+ sb.append("\n expect ").append(r.expectSpec).append("
within ")
.append(ChaosRuleParser.printTime(r.within)).append(":
").append(r.outcome.key)
- .append(", ").append(r.completed).append(" completed");
+ .append(", ").append(r.value).append('
').append(r.expectKind.key);
if (r.outcome == Outcome.PENDING) {
sb.append(",
").append(ChaosRuleParser.printTime(r.timeLeft)).append(" left");
} else if (r.outcome == Outcome.CANCELLED) {
@@ -307,9 +313,15 @@ public final class ChaosReport {
if (r.outcome != null) {
JsonObject e = new JsonObject();
e.put("route", r.expectRoute);
+ e.put("kind", r.expectKind.key);
+ e.put("op", r.expectOp.key);
e.put("count", r.expectCount);
e.put("within", r.within);
- e.put("completed", r.completed);
+ // the count of its kind so far (completed for an expectation
of completed exchanges, as before)
+ e.put("value", r.value);
+ if (r.expectKind == ChaosRule.Expectation.Kind.COMPLETED) {
+ e.put("completed", r.value);
+ }
e.put("outcome", r.outcome.key);
if (r.outcome == Outcome.PENDING) {
e.put("timeLeft", r.timeLeft);
@@ -408,10 +420,14 @@ public final class ChaosReport {
final long lostAtDlq;
final long endsIn;
final String expectRoute;
+ final ChaosRule.Expectation.Kind expectKind;
+ final ChaosRule.Expectation.Op expectOp;
+ final String expectSpec;
final long expectCount;
final long within;
final Outcome outcome;
- final long completed;
+ final long value;
+ final long lost;
final long timeLeft;
// a lifecycle rule: its action, route, duration and what it did, else
null
final Action action;
@@ -447,16 +463,24 @@ public final class ChaosReport {
ChaosRule.Expectation expectation = rule.expectation;
if (expectation != null) {
expectRoute = expectation.route;
+ expectKind = expectation.kind;
+ expectOp = expectation.op;
+ expectSpec = expectation.spec();
expectCount = expectation.count;
ChaosRule.Expectation.Reading reading = expectation.read(now);
outcome = reading.outcome();
- completed = reading.completed();
+ value = reading.value();
+ lost = expectation.lost(reading);
timeLeft = Math.max(0, expectation.deadline() - now);
} else {
expectRoute = null;
+ expectKind = null;
+ expectOp = null;
+ expectSpec = null;
expectCount = 0;
outcome = null;
- completed = 0;
+ value = 0;
+ lost = 0;
timeLeft = 0;
}
}
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
index 45115b5c2bc7..a79ac472d861 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
@@ -218,7 +218,8 @@ final class ChaosRule {
this.runFor = untilDuration > 0 ? untilDuration : maxDuration;
this.endsAt = plus(addedAt, runFor);
this.within = b.within > 0 ? b.within : (untilDuration > 0 ?
untilDuration : maxDuration);
- this.expectation = expectRoute != null ? new Expectation(expectRoute,
expectCount, plus(addedAt, within)) : null;
+ this.expectation = expectRoute != null
+ ? new Expectation(expectRoute, b.expectKind, b.expectOp,
expectCount, plus(addedAt, within)) : null;
for (int i = 0; i < fired.length; i++) {
fired[i] = new LongAdder();
}
@@ -392,10 +393,15 @@ final class ChaosRule {
}
/**
- * An expectation of a rule: the route completes at least that many more
exchanges after the rule is added, before
- * the time is up. It is met when the count is reached in time, unmet when
the time is up without it, and pending
- * before that. It is cancelled when the rule is removed (or replaced), or
the context stops, first: the messages
- * may still arrive, so it is neither met nor unmet, and does not keep the
report from being final.
+ * An expectation of a rule: a count of the route after the rule is added,
before the time is up. Its kind is what
+ * it counts (the exchanges that completed, were handled by the error
handler or failed, or the redeliveries), and
+ * its op how the count compares: at least ({@code >=}, as {@code
expect=orders:100} is), at most ({@code <=}) or
+ * exactly ({@code =}) that many.
+ * <p/>
+ * At least is met as soon as the count is reached in time, and unmet when
the time is up without it. At most and
+ * exactly are unmet as soon as the count goes over, and decided when the
time is up otherwise. Before that it is
+ * pending. It is cancelled when the rule is removed (or replaced), or the
context stops, first: the messages may
+ * still arrive, so it is neither met nor unmet, and does not keep the
report from being final.
*/
static final class Expectation {
@@ -413,35 +419,84 @@ final class ChaosRule {
}
}
+ /** What an expectation counts in its route. */
+ enum Kind {
+ COMPLETED("completed", "exchange completed", "exchanges
completed"),
+ HANDLED("handled", "exchange handled by the error handler",
"exchanges handled by the error handler"),
+ FAILED("failed", "exchange failed", "exchanges failed"),
+ REDELIVERIES("redeliveries", "redelivery", "redeliveries");
+
+ final String key;
+ // for the reason of an unmet expectation
+ private final String one;
+ private final String many;
+
+ Kind(String key, String one, String many) {
+ this.key = key;
+ this.one = one;
+ this.many = many;
+ }
+
+ String words(long n) {
+ return n == 1 ? one : many;
+ }
+ }
+
+ /** How the count of an expectation compares. */
+ enum Op {
+ AT_LEAST(">=", "at least"),
+ AT_MOST("<=", "at most"),
+ EXACTLY("=", "exactly");
+
+ final String key;
+ final String words;
+
+ Op(String key, String words) {
+ this.key = key;
+ this.words = words;
+ }
+ }
+
final String route;
+ final Kind kind;
+ final Op op;
final long count;
private long deadline;
- private long completed;
- private long metAt = -1;
+ private long value;
+ // when it was decided before the time was up (met for at least, unmet
for at most and exactly), and how
+ private long decidedAt = -1;
+ private Outcome decided;
private long frozenAt = -1;
Expectation(String route, long count, long deadline) {
+ this(route, Kind.COMPLETED, Op.AT_LEAST, count, deadline);
+ }
+
+ Expectation(String route, Kind kind, Op op, long count, long deadline)
{
this.route = route;
+ this.kind = kind;
+ this.op = op;
this.count = count;
this.deadline = deadline;
}
/**
- * The route of this expectation completed an exchange. The time is
read under the lock, so once a report saw
- * the expectation unmet, a completion that comes later cannot make it
met.
+ * The route of this expectation counted one more of its kind. The
time is read under the lock, so once a report
+ * saw the expectation decided, a count that comes later cannot change
it.
*/
- synchronized void complete() {
- completeAt(TimeSource.currentTimeMillis());
+ synchronized void record() {
+ recordAt(TimeSource.currentTimeMillis());
}
- /** The route of this expectation completed an exchange at the given
time (the tests give the time). */
- synchronized void completeAt(long now) {
- if (metAt >= 0 || frozenAt >= 0 || now > deadline) {
+ /** The route of this expectation counted one more of its kind at the
given time (the tests give the time). */
+ synchronized void recordAt(long now) {
+ if (decided != null || frozenAt >= 0 || now > deadline) {
return;
}
- completed++;
- if (completed >= count) {
- metAt = now;
+ value++;
+ if (op == Op.AT_LEAST ? value >= count : value > count) {
+ decided = op == Op.AT_LEAST ? Outcome.MET : Outcome.UNMET;
+ decidedAt = now;
}
}
@@ -451,7 +506,7 @@ final class ChaosRule {
/** The clock of the rule starts again (see {@link
ChaosRule#startClock(long)}). */
synchronized void startClock(long deadline) {
- if (metAt < 0 && frozenAt < 0) {
+ if (decided == null && frozenAt < 0) {
this.deadline = deadline;
}
}
@@ -462,32 +517,55 @@ final class ChaosRule {
}
}
- synchronized long completed() {
- return completed;
+ synchronized long value() {
+ return value;
}
synchronized Outcome outcome(long now) {
- if (metAt >= 0) {
- return Outcome.MET;
+ if (decided != null) {
+ return decided;
}
if (frozenAt >= 0 && frozenAt <= deadline) {
- // removed before it was decided: the messages may still
arrive, so they are not called lost
+ // removed before it was decided: the messages may still
arrive, so it is neither met nor unmet
return Outcome.CANCELLED;
}
- return now > deadline ? Outcome.UNMET : Outcome.PENDING;
+ if (now <= deadline) {
+ return Outcome.PENDING;
+ }
+ // the time is up
+ return switch (op) {
+ case AT_LEAST -> Outcome.UNMET;
+ case AT_MOST -> Outcome.MET;
+ case EXACTLY -> value == count ? Outcome.MET : Outcome.UNMET;
+ };
}
synchronized boolean frozen() {
return frozenAt >= 0;
}
- /** The outcome, the completed count and whether it is frozen, read
together (for the report). */
+ /** The outcome, the counted value and whether it is frozen, read
together (for the report). */
synchronized Reading read(long now) {
- return new Reading(outcome(now), completed, frozenAt >= 0);
+ return new Reading(outcome(now), value, frozenAt >= 0);
+ }
+
+ /**
+ * The messages an unmet expectation counts as lost: only one of
completed exchanges, at least or exactly that
+ * many, misses the ones that did not complete.
+ */
+ long lost(Reading reading) {
+ return reading.outcome() == Outcome.UNMET && kind ==
Kind.COMPLETED && op != Op.AT_MOST
+ ? Math.max(0, count - reading.value()) : 0;
+ }
+
+ /** The expectation as the rule writes it, such as orders:100 or
orders:handled<=0. */
+ String spec() {
+ return kind == Kind.COMPLETED && op == Op.AT_LEAST
+ ? route + ":" + count : route + ":" + kind.key + op.key +
count;
}
- /** What the report reads of an expectation at one time. */
- record Reading(Outcome outcome, long completed, boolean frozen) {
+ /** What an expectation reads at one time. */
+ record Reading(Outcome outcome, long value, boolean frozen) {
}
}
@@ -513,6 +591,8 @@ final class ChaosRule {
long untilCount;
long untilDuration;
String expectRoute;
+ Expectation.Kind expectKind = Expectation.Kind.COMPLETED;
+ Expectation.Op expectOp = Expectation.Op.AT_LEAST;
long expectCount;
long within;
Action action;
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
index 335c7f7f0682..4f812b68e961 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
@@ -769,26 +769,65 @@ final class ChaosRuleParser {
private static void expectation(ChaosRule.Builder b, String expect, String
within, long maxDuration) {
if (expect == null) {
if (within != null) {
- throw new IllegalArgumentException("within= is the time of an
expectation, and needs expect=<route>:<count>");
+ throw new IllegalArgumentException(
+ "within= is the time of an expectation, and needs
expect=<route>:<count> (or"
+ + "
<route>:<kind><op><count>)");
}
return;
}
int colon = expect.lastIndexOf(':');
if (colon <= 0 || colon == expect.length() - 1) {
- throw new IllegalArgumentException("expect=" + expect + " is not
<route>:<count>, such as expect=orders:100");
+ throw new IllegalArgumentException(
+ "expect=" + expect + " is not <route>:<count> or
<route>:<kind><op><count>, such as expect=orders:100"
+ + " or
expect=orders:handled<=0");
}
b.expectRoute = expect.substring(0, colon);
if (b.expectRoute.indexOf('*') >= 0) {
throw new IllegalArgumentException(
"expect=" + expect + " needs the id of one route, not a
pattern, such as expect=orders:100");
}
- String count = expect.substring(colon + 1);
+ String spec = expect.substring(colon + 1);
+ String count = spec;
+ if (!isDigits(spec)) {
+ // <kind><op><count>, such as handled<=0
+ int op = 0;
+ while (op < spec.length() && Character.isLetter(spec.charAt(op))) {
+ op++;
+ }
+ String kind = spec.substring(0, op);
+ b.expectKind = null;
+ for (ChaosRule.Expectation.Kind k :
ChaosRule.Expectation.Kind.values()) {
+ if (k.key.equals(kind)) {
+ b.expectKind = k;
+ }
+ }
+ if (b.expectKind == null) {
+ throw new IllegalArgumentException(
+ "expect=" + expect + " counts " + (kind.isEmpty() ?
"nothing" : kind)
+ + ": the kinds are
completed, handled, failed and redeliveries,"
+ + " such as
expect=orders:handled<=0");
+ }
+ String rest = spec.substring(op);
+ if (rest.startsWith(">=")) {
+ b.expectOp = ChaosRule.Expectation.Op.AT_LEAST;
+ } else if (rest.startsWith("<=")) {
+ b.expectOp = ChaosRule.Expectation.Op.AT_MOST;
+ } else if (rest.startsWith("=")) {
+ b.expectOp = ChaosRule.Expectation.Op.EXACTLY;
+ } else {
+ throw new IllegalArgumentException(
+ "expect=" + expect + " needs >=, <= or = after " +
kind + ", such as expect=orders:" + kind
+ + "<=0");
+ }
+ count = rest.substring(b.expectOp.key.length());
+ }
if (!isDigits(count) || count.length() > 18) {
throw new IllegalArgumentException(
- "expect=" + expect + " needs a count that is a whole
number of at most 18 digits after the colon");
+ "expect=" + expect + " needs a count that is a whole
number of at most 18 digits");
}
b.expectCount = Long.parseLong(count);
- if (b.expectCount < 1) {
+ if (b.expectCount < 1 && b.expectOp ==
ChaosRule.Expectation.Op.AT_LEAST) {
+ // at least 0 is always met
throw new IllegalArgumentException("expect=" + expect + " needs a
count of 1 or more");
}
if (within != null) {
diff --git
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosErrorHandlerExpectationTest.java
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosErrorHandlerExpectationTest.java
new file mode 100644
index 000000000000..371ede856b55
--- /dev/null
+++
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosErrorHandlerExpectationTest.java
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.chaos;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Expectations on the error handler of a route: how many exchanges it handled
(the dead letter channel), failed, or
+ * redelivered, at least, at most or exactly. The time of the expectations
runs on a virtual clock (it is static, so
+ * this class runs alone).
+ */
+@Isolated
+class ChaosErrorHandlerExpectationTest extends ChaosTestSupport {
+
+ private FakeClock clock;
+
+ @Override
+ protected void setupResources() throws Exception {
+ super.setupResources();
+ clock = FakeClock.install();
+ }
+
+ @Override
+ protected void cleanupResources() throws Exception {
+ clock.close();
+ super.cleanupResources();
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:orders").routeId("orders")
+
.errorHandler(deadLetterChannel("mock:dead").maximumRedeliveries(2).redeliveryDelay(0))
+ .to("mock:stock");
+
from("direct:plain").routeId("plain").errorHandler(noErrorHandler()).to("mock:stock");
+ }
+ };
+ }
+
+ private void send(int n) {
+ for (int i = 0; i < n; i++) {
+ template.send("direct:orders", e ->
e.getMessage().setBody("order"));
+ }
+ }
+
+ private JsonObject expectation(String id) {
+ return (JsonObject) ruleReport(id).get("expectation");
+ }
+
+ @Test
+ void redeliveryRecoversEverythingSoNothingReachesTheDeadLetterChannel() {
+ String id = chaos().addRule("endpoint=mock:stock error rate=every:3
expect=orders:handled<=0 within=1m");
+ send(5);
+
+ // nothing handled yet, but at most is decided when the time is up
+ assertThat(expectation(id).getString("outcome")).isEqualTo("pending");
+ clock.advance(60_001);
+
+ JsonObject e = expectation(id);
+ assertThat(e.getString("outcome")).isEqualTo("met");
+ assertThat(e.getString("kind")).isEqualTo("handled");
+ assertThat(e.getString("op")).isEqualTo("<=");
+ assertThat(e.getLong("value")).isZero();
+ assertThat(e.containsKey("completed")).isFalse();
+ assertThat(chaos().report().getVerdict()).isEqualTo(ChaosReport.PASS);
+ }
+
+ @Test
+ void anAtMostIsUnmetAsSoonAsTheCountGoesOver() {
+ String id = chaos().addRule("endpoint=mock:stock error
expect=orders:handled<=0 within=1m");
+ send(1);
+
+ // decided before the time is up
+ JsonObject e = expectation(id);
+ assertThat(e.getString("outcome")).isEqualTo("unmet");
+ assertThat(e.getLong("value")).isEqualTo(1);
+ ChaosReport report = chaos().report();
+ assertThat(report.getVerdict()).isEqualTo(ChaosReport.FAIL);
+ assertThat(report.isFinal()).isTrue();
+ assertThat(report.getReasons()).containsExactly(
+ "route orders: 1 exchange handled by the error handler within
1m0s, at most 0 expected (rule " + id
+ + ")");
+ // the message reached the dead letter channel: not lost
+ assertThat(report.getLost()).isZero();
+ assertThat(report.toText()).contains("expect orders:handled<=0 within
1m0s: unmet, 1 handled");
+ }
+
+ @Test
+ void anAtLeastOfHandledExchangesIsMetWhenReached() {
+ String id = chaos().addRule("endpoint=mock:stock error
expect=orders:handled>=2 within=1m");
+ send(1);
+ assertThat(expectation(id).getString("outcome")).isEqualTo("pending");
+ send(1);
+ assertThat(expectation(id).getString("outcome")).isEqualTo("met");
+
+ String unmet = chaos().addRule("endpoint=nothing error
expect=orders:handled>=5 within=1m");
+ clock.advance(60_001);
+ assertThat(expectation(unmet).getString("outcome")).isEqualTo("unmet");
+ assertThat(chaos().report().getReasons()).containsExactly(
+ "route orders: 0 exchanges handled by the error handler within
1m0s, at least 5 expected (rule " + unmet
+ + ")");
+ }
+
+ @Test
+ void redeliveriesAreCounted() {
+ String id = chaos().addRule("endpoint=mock:stock error
expect=orders:redeliveries<=3 within=1m");
+ send(1);
+ assertThat(expectation(id).getLong("value")).isEqualTo(2);
+ assertThat(expectation(id).getString("outcome")).isEqualTo("pending");
+ send(1);
+ assertThat(expectation(id).getString("outcome")).isEqualTo("unmet");
+ assertThat(chaos().report().getReasons()).singleElement().asString()
+ .isEqualTo("route orders: 4 redeliveries within 1m0s, at most
3 expected (rule " + id + ")");
+ }
+
+ @Test
+ void failedExchangesAreCounted() {
+ String handled = chaos().addRule("endpoint=nothing error
expect=orders:failed=0 within=1m");
+ String failed = chaos().addRule("endpoint=mock:stock error
expect=plain:failed>=1 within=1m");
+ send(1);
+ template.send("direct:plain", e -> e.getMessage().setBody("x"));
+
+ assertThat(expectation(failed).getString("outcome")).isEqualTo("met");
+ clock.advance(60_001);
+ // the dead letter channel handled the failure of orders: not failed
+ assertThat(expectation(handled).getString("outcome")).isEqualTo("met");
+ }
+
+ @Test
+ void anExactCountIsDecidedWhenTheTimeIsUp() {
+ String id = chaos().addRule("endpoint=nothing error
expect=orders:completed=2 within=1m");
+ send(2);
+ assertThat(expectation(id).getString("outcome")).isEqualTo("pending");
+ clock.advance(60_001);
+ assertThat(expectation(id).getString("outcome")).isEqualTo("met");
+
+ String over = chaos().addRule("endpoint=nothing error
expect=orders:completed=1 within=1m");
+ send(2);
+ assertThat(expectation(over).getString("outcome")).isEqualTo("unmet");
+ // more completed than expected: nothing lost
+ assertThat(chaos().report().getLost()).isZero();
+
+ String under = chaos().addRule("endpoint=nothing error
expect=orders:completed=5 within=1m");
+ send(3);
+ clock.advance(60_001);
+ // fewer completed than expected: the missing ones are lost, as for at
least
+ assertThat(expectation(under).getString("outcome")).isEqualTo("unmet");
+ assertThat(chaos().report().getLost()).isEqualTo(2);
+ }
+
+ @Test
+ void aCountAloneIsAtLeastThatManyCompletedExchanges() {
+ String id = chaos().addRule("endpoint=nothing error expect=orders:2
within=1m");
+ send(2);
+
+ JsonObject e = expectation(id);
+ assertThat(e.getString("outcome")).isEqualTo("met");
+ assertThat(e.getString("kind")).isEqualTo("completed");
+ assertThat(e.getString("op")).isEqualTo(">=");
+ assertThat(e.getLong("value")).isEqualTo(2);
+ assertThat(e.getLong("completed")).isEqualTo(2);
+ assertThat(chaos().report().toText()).contains("expect orders:2 within
1m0s: met, 2 completed");
+ }
+}
diff --git
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleParserTest.java
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleParserTest.java
index 8fa82cc9ff27..d631b6f7ca00 100644
---
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleParserTest.java
+++
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleParserTest.java
@@ -173,6 +173,27 @@ class ChaosRuleParserTest {
assertThat(parse("endpoint=x error
expect=a:b:7").expectRoute).isEqualTo("a:b");
}
+ @Test
+ void expectationKindsAndOps() {
+ ChaosRule.Builder b = parse("endpoint=x error expect=orders:handled<=0
within=1m");
+ assertThat(b.expectRoute).isEqualTo("orders");
+ assertThat(b.expectKind).isEqualTo(ChaosRule.Expectation.Kind.HANDLED);
+ assertThat(b.expectOp).isEqualTo(ChaosRule.Expectation.Op.AT_MOST);
+ assertThat(b.expectCount).isZero();
+ b = parse("endpoint=x error expect=a:b:redeliveries>=30");
+ assertThat(b.expectRoute).isEqualTo("a:b");
+
assertThat(b.expectKind).isEqualTo(ChaosRule.Expectation.Kind.REDELIVERIES);
+ assertThat(b.expectOp).isEqualTo(ChaosRule.Expectation.Op.AT_LEAST);
+ b = parse("endpoint=x error expect=orders:failed=0");
+ assertThat(b.expectKind).isEqualTo(ChaosRule.Expectation.Kind.FAILED);
+ assertThat(b.expectOp).isEqualTo(ChaosRule.Expectation.Op.EXACTLY);
+ // a count alone is at least that many completed exchanges, as before
+ b = parse("endpoint=x error expect=orders:20");
+
assertThat(b.expectKind).isEqualTo(ChaosRule.Expectation.Kind.COMPLETED);
+ assertThat(b.expectOp).isEqualTo(ChaosRule.Expectation.Op.AT_LEAST);
+ assertThat(parse("endpoint=x error
expect=orders:completed>=20").expectCount).isEqualTo(20);
+ }
+
@Test
void quotedValuesAndEscapes() {
assertThat(parse("endpoint=x error message='it is
\\'down\\''").message).isEqualTo("it is 'down'");
@@ -300,6 +321,12 @@ class ChaosRuleParserTest {
"endpoint=x error expect=:5|is not <route>:<count>",
"endpoint=x error expect=orders:|is not <route>:<count>",
"endpoint=x error expect=orders:0|needs a count of 1 or more",
+ "endpoint=x error expect=orders:completed>=0|needs a count of 1 or
more",
+ "endpoint=x error expect=orders:lost<=0|counts lost: the kinds are
completed, handled, failed and redeliveries",
+ "endpoint=x error expect=orders:<=0|counts nothing",
+ "endpoint=x error expect=orders:handled<0|needs >=, <= or = after
handled",
+ "endpoint=x error expect=orders:handled<=x|needs a count that is a
whole number",
+ "endpoint=x error expect=orders:handled<=|needs a count that is a
whole number",
"endpoint=x error expect=orders:5 within=0s|must be longer than 0",
"endpoint=x error expect=orders:5 within=11m|within=11m is longer
than camel.chaos.maxDuration",
"endpoint=x error expect=orders:5 within=106751991167d|is longer
than camel.chaos.maxDuration",
diff --git
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleTest.java
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleTest.java
index a087d702b0fd..20a1ab0e7305 100644
---
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleTest.java
+++
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleTest.java
@@ -150,23 +150,23 @@ class ChaosRuleTest {
@Test
void expectationMetUnmetPending() {
ChaosRule.Expectation met = new ChaosRule.Expectation("orders", 2, T0
+ 100);
- met.completeAt(T0 + 10);
+ met.recordAt(T0 + 10);
assertThat(met.outcome(T0 + 10)).isEqualTo(Outcome.PENDING);
- met.completeAt(T0 + 100);
+ met.recordAt(T0 + 100);
assertThat(met.outcome(T0 + 1000)).isEqualTo(Outcome.MET);
ChaosRule.Expectation unmet = new ChaosRule.Expectation("orders", 2,
T0 + 100);
- unmet.completeAt(T0 + 10);
+ unmet.recordAt(T0 + 10);
// a completion after the deadline does not count
- unmet.completeAt(T0 + 101);
- assertThat(unmet.completed()).isEqualTo(1);
+ unmet.recordAt(T0 + 101);
+ assertThat(unmet.value()).isEqualTo(1);
assertThat(unmet.outcome(T0 + 100)).isEqualTo(Outcome.PENDING);
assertThat(unmet.outcome(T0 + 101)).isEqualTo(Outcome.UNMET);
ChaosRule.Expectation removed = new ChaosRule.Expectation("orders", 2,
T0 + 100);
removed.freeze(T0 + 50);
- removed.completeAt(T0 + 60);
- assertThat(removed.completed()).isZero();
+ removed.recordAt(T0 + 60);
+ assertThat(removed.value()).isZero();
// removed before it was decided: cancelled, never unmet
assertThat(removed.outcome(T0 + 1000)).isEqualTo(Outcome.CANCELLED);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
index 46456d806aee..5d01776fc7c0 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
@@ -323,9 +323,17 @@ public class ListChaos extends ProcessWatchCommand {
if (!(rule.get("expectation") instanceof JsonObject e)) {
return "";
}
+ // orders:100 counts completed exchanges (at least that many);
orders:handled<=0 names its kind and op
+ String kind = e.getStringOrDefault("kind", "completed");
+ String op = e.getStringOrDefault("op", ">=");
+ Object value = e.get("value") != null ? e.get("value") :
e.get("completed");
StringBuilder sb = new StringBuilder();
-
sb.append(e.getString("route")).append(':').append(e.get("count")).append('
').append(e.getString("outcome"))
- .append(" (").append(e.get("completed")).append(" completed");
+ sb.append(e.getString("route")).append(':');
+ if (!"completed".equals(kind) || !">=".equals(op)) {
+ sb.append(kind).append(op);
+ }
+ sb.append(e.get("count")).append(' ').append(e.getString("outcome"))
+ .append(" (").append(value).append(' ').append(kind);
if (e.get("timeLeft") instanceof Number n) {
sb.append(",
").append(TimeUtils.printDuration(n.longValue())).append(" left");
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java
index 04c7c5abe6ef..98866d1252d8 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java
@@ -207,6 +207,21 @@ class ListChaosTest extends ProcessCommandTestSupport {
assertThat(printer.getStderr()).contains("orders (pid " + TEST_PID +
"): no chaos rules");
}
+ @Test
+ void reportShowsTheKindAndOpOfAnExpectation() throws Exception {
+ JsonObject chaos = chaosOn();
+ JsonObject report = (JsonObject) chaos.get("report");
+ JsonObject rule = (JsonObject) ((JsonArray)
report.getCollection("rules")).get(0);
+ rule.put("expectation", new JsonObject(
+ Map.of("route", "orders", "kind", "handled", "op", "<=",
"count", 0, "within", 60000, "value", 3,
+ "outcome", "unmet")));
+ writeStatus(chaos);
+
+ assertThat(run("--report")).isZero();
+
+ assertThat(printer.getStdout()).contains("orders:handled<=0 unmet (3
handled)");
+ }
+
@Test
void chaosOff() throws Exception {
writeStatus(new JsonObject(Map.of("enabled", false, "reason", "Chaos
is off: it stays off with profile prod")));