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 ac0a8a471ee3 CAMEL-26790: camel-chaos - an experiment that cannot tell
does not pass: a fault that never fired, a route without messages, failures
outside its expectations, a failed expectation behind a dead letter channel
(#29517)
ac0a8a471ee3 is described below
commit ac0a8a471ee3422c3f7f7680e946beb638e7f64f
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 10 00:20:34 2026 +0100
CAMEL-26790: camel-chaos - an experiment that cannot tell does not pass: a
fault that never fired, a route without messages, failures outside its
expectations, a failed expectation behind a dead letter channel (#29517)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../org/apache/camel/catalog/docs/chaos.adoc | 22 ++++--
components/camel-chaos/src/main/docs/chaos.adoc | 22 ++++--
.../apache/camel/component/chaos/ChaosAdvisor.java | 2 +-
.../camel/component/chaos/ChaosExperiment.java | 83 +++++++++++++++++++-
.../apache/camel/component/chaos/ChaosManager.java | 19 +++++
.../camel/component/chaos/ChaosExperimentTest.java | 88 ++++++++++++++++++++++
.../action/CamelChaosExperimentAction.java | 11 ++-
.../dsl/jbang/core/commands/process/ListChaos.java | 10 +++
.../core/commands/action/CamelChaosActionTest.java | 9 +++
.../jbang/core/commands/process/ListChaosTest.java | 19 ++++-
.../src/test/resources/golden/cli-help.txt | 12 +--
.../src/test/resources/golden/cli-parse.txt | 65 ++++++++++++++++
12 files changed, 336 insertions(+), 26 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 5f74f1a59e07..0c8b96325a9c 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
@@ -161,6 +161,10 @@ names some of the endpoints, routes or steps there are. It
is not an error, as t
creates its endpoints when it sends, and a route can be added later). The
rules of `camel.chaos.rules` are checked when
the context has started.
+A rule that expects failed exchanges in a route whose error handler is a dead
letter channel
+(`expect=parked:failed<=0`) has a warning too: the dead letter channel handles
what fails there, so nothing fails, and
+the expectation is always met. `handled` counts what the dead letter channel
handled (`expect=parked:handled<=0`).
+
To see what a rule would hit before it is added, `chaos-add --dry-run` checks
it as `chaos-add` would, and adds
nothing. It prints the rule in plain words, then each send, poll, route or
step it matches now, with its route (and the
step that sends), the sends known only when they send that may match (a
`toD`), and the warnings:
@@ -391,9 +395,16 @@ faults of each rule, the state of each expectation, and a
verdict:
The dead letter channel then handles the message without delivering it, so no
expectation of a route sees it: the
experiment counts it as lost (`lostAtDeadLetterChannel`, and per rule
`lostAtDeadLetterChannelPerRule`), as the
report does.
-* `PASS`: all expectations are met.
-* `PENDING`: it is still open and an expectation is not decided yet.
-* `INCONCLUSIVE`: it ended before an expectation was decided, so the messages
may still have come.
+* `PASS`: all expectations are met, a fault fired, and the routes of the
expectations had messages.
+* `PENDING`: it is still open, and an expectation is not decided yet or no
fault has fired yet.
+* `INCONCLUSIVE`: it ended before an expectation was decided, so the messages
may still have come; none of its
+faults fired, so its expectations say nothing about them; or an expectation
was met on a route that had no messages
+while it ran (`refunds:failed<=0` when the traffic went to another route), or
is a time (`maxTime`, `meanTime`) of
+such a route. The `reason` says which.
+
+The routes that failed while it ran, and whose failures no expectation of the
experiment counts, are listed as
+`alsoFailed`, with how many failed. An experiment that expects
`refunds:maxTime<=30s` passes while 2 refunds failed,
+and `alsoFailed` says so.
The experiment is ended with the exploration when that finishes, or when its
time is up, with what came of it so far.
@@ -406,8 +417,9 @@ The journal has them as `experiments`, each with:
* `number`, `hypothesis`, the `rules` (their ids) and `ruleTexts`
* `open`, `startedAt` and `elapsed`
-* the `verdict` and the `expectations`: per rule its `expect`, `state` (`met`,
`unmet` or `pending`), the `actual` value,
-and the `value` in words
+* the `verdict` (and the `reason` of an `INCONCLUSIVE` one) and the
`expectations`: per rule its `expect`, `state`
+(`met`, `unmet` or `pending`), the `actual` value, and the `value` in words
+* `alsoFailed`: the routes that failed outside the expectations, when there
are such
* the `faults` per rule
* the `routes` that did something while it ran, with the same counts as the
baseline
* once ended, `endedAt`, `endedBy` (`ended`, `finished` or `deadline`) and the
`note`
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc
b/components/camel-chaos/src/main/docs/chaos.adoc
index 5f74f1a59e07..0c8b96325a9c 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -161,6 +161,10 @@ names some of the endpoints, routes or steps there are. It
is not an error, as t
creates its endpoints when it sends, and a route can be added later). The
rules of `camel.chaos.rules` are checked when
the context has started.
+A rule that expects failed exchanges in a route whose error handler is a dead
letter channel
+(`expect=parked:failed<=0`) has a warning too: the dead letter channel handles
what fails there, so nothing fails, and
+the expectation is always met. `handled` counts what the dead letter channel
handled (`expect=parked:handled<=0`).
+
To see what a rule would hit before it is added, `chaos-add --dry-run` checks
it as `chaos-add` would, and adds
nothing. It prints the rule in plain words, then each send, poll, route or
step it matches now, with its route (and the
step that sends), the sends known only when they send that may match (a
`toD`), and the warnings:
@@ -391,9 +395,16 @@ faults of each rule, the state of each expectation, and a
verdict:
The dead letter channel then handles the message without delivering it, so no
expectation of a route sees it: the
experiment counts it as lost (`lostAtDeadLetterChannel`, and per rule
`lostAtDeadLetterChannelPerRule`), as the
report does.
-* `PASS`: all expectations are met.
-* `PENDING`: it is still open and an expectation is not decided yet.
-* `INCONCLUSIVE`: it ended before an expectation was decided, so the messages
may still have come.
+* `PASS`: all expectations are met, a fault fired, and the routes of the
expectations had messages.
+* `PENDING`: it is still open, and an expectation is not decided yet or no
fault has fired yet.
+* `INCONCLUSIVE`: it ended before an expectation was decided, so the messages
may still have come; none of its
+faults fired, so its expectations say nothing about them; or an expectation
was met on a route that had no messages
+while it ran (`refunds:failed<=0` when the traffic went to another route), or
is a time (`maxTime`, `meanTime`) of
+such a route. The `reason` says which.
+
+The routes that failed while it ran, and whose failures no expectation of the
experiment counts, are listed as
+`alsoFailed`, with how many failed. An experiment that expects
`refunds:maxTime<=30s` passes while 2 refunds failed,
+and `alsoFailed` says so.
The experiment is ended with the exploration when that finishes, or when its
time is up, with what came of it so far.
@@ -406,8 +417,9 @@ The journal has them as `experiments`, each with:
* `number`, `hypothesis`, the `rules` (their ids) and `ruleTexts`
* `open`, `startedAt` and `elapsed`
-* the `verdict` and the `expectations`: per rule its `expect`, `state` (`met`,
`unmet` or `pending`), the `actual` value,
-and the `value` in words
+* the `verdict` (and the `reason` of an `INCONCLUSIVE` one) and the
`expectations`: per rule its `expect`, `state`
+(`met`, `unmet` or `pending`), the `actual` value, and the `value` in words
+* `alsoFailed`: the routes that failed outside the expectations, when there
are such
* the `faults` per rule
* the `routes` that did something while it ran, with the same counts as the
baseline
* once ended, `endedAt`, `endedBy` (`ended`, `finished` or `deadline`) and the
`note`
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
index 5c88f23aca89..1dedff27ba84 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
@@ -519,7 +519,7 @@ final class ChaosAdvisor {
}
/** The dead letter channel of the route, without its options, or null
when its error handler has none. */
- private static String deadLetterUri(Route route) {
+ static String deadLetterUri(Route route) {
if (route.getErrorHandlerFactory() instanceof
DeadLetterChannelDefinition dlc && dlc.getDeadLetterUri() != null) {
String uri = dlc.getDeadLetterUri();
return uri.indexOf('?') > 0 ? uri.substring(0, uri.indexOf('?')) :
uri;
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExperiment.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExperiment.java
index 33f6d676dc53..94ef7db1fc9c 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExperiment.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExperiment.java
@@ -16,10 +16,13 @@
*/
package org.apache.camel.component.chaos;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.StringJoiner;
+import java.util.TreeSet;
import java.util.function.Function;
import org.apache.camel.component.chaos.ChaosRule.Expectation;
@@ -36,8 +39,11 @@ import org.apache.camel.util.json.JsonObject;
* The verdict is {@code FAIL} when an expectation is unmet, or a rule of it
failed or dropped the send of a dead letter
* channel, which then handled the message without delivering it (lost, as the
report counts it, CAMEL-26789), and
* {@code PASS} when all are met. While it runs and an expectation is not
decided it is {@code PENDING}, and an
- * experiment that ended before an expectation was decided is {@code
INCONCLUSIVE}: the messages may still have come.
- * The manager guards it with its lock.
+ * experiment that ended before an expectation was decided is {@code
INCONCLUSIVE}: the messages may still have come. An
+ * experiment whose faults never fired is not a {@code PASS} either, as its
expectations say nothing about the faults
+ * ({@code INCONCLUSIVE} once it ended, CAMEL-26790). The routes that failed
while it ran, and that no expectation of it
+ * counts the failures of, are listed ({@code alsoFailed}), so a verdict on
another count does not hide them. The
+ * manager guards it with its lock.
*/
final class ChaosExperiment {
@@ -46,6 +52,17 @@ final class ChaosExperiment {
static final String PENDING = "PENDING";
static final String INCONCLUSIVE = "INCONCLUSIVE";
+ // why an experiment is INCONCLUSIVE
+ static final String NEVER_FIRED = "its faults never fired, so its
expectations say nothing about them";
+ static final String NOT_DECIDED = "it ended before an expectation was
decided, so the messages may still have come";
+
+ /** Why an experiment whose expectation was met on a route without
messages is INCONCLUSIVE. */
+ static String noMessages(Set<String> routes) {
+ return (routes.size() == 1 ? "route " : "routes ") + String.join(", ",
routes)
+ + " had no messages while it ran, so " + (routes.size() == 1 ?
"its expectation says" : "their expectations say")
+ + " nothing: feed " + (routes.size() == 1 ? "it" : "them");
+ }
+
/** How an experiment ended. */
enum End {
// endExperiment
@@ -143,6 +160,11 @@ final class ChaosExperiment {
JsonArray texts = new JsonArray();
JsonObject faults = new JsonObject();
JsonArray expectations = new JsonArray();
+ long firedAll = 0;
+ // the routes whose failures an expectation counts, and the routes of
the expectations that are met
+ Set<String> failedExpected = new HashSet<>();
+ Set<String> metRoutes = new TreeSet<>();
+ Set<String> unmetTimes = new HashSet<>();
for (String id : rules) {
ChaosRule r = rule.apply(id);
if (r == null) {
@@ -154,6 +176,7 @@ final class ChaosExperiment {
fired += r.firedAt(hook);
}
faults.put(id, fired);
+ firedAll += fired;
long kept = r.lostAtDlqCount();
if (kept > 0) {
lostAtDlq.put(id, kept);
@@ -161,11 +184,22 @@ final class ChaosExperiment {
}
Expectation e = r.expectation;
if (e != null) {
+ if (e.kind == Expectation.Kind.FAILED) {
+ failedExpected.add(e.route);
+ }
Expectation.Reading reading = e.read(now);
// an expectation the experiment did not wait for is
undecided, not cancelled
Outcome outcome = reading.outcome() == Outcome.CANCELLED ?
Outcome.PENDING : reading.outcome();
- unmet |= outcome == Outcome.UNMET;
+ if (outcome == Outcome.UNMET && e.kind.time()) {
+ // a time is unmet without an exchange too: decided once
the routes are known
+ unmetTimes.add(e.route);
+ } else {
+ unmet |= outcome == Outcome.UNMET;
+ }
undecided |= outcome == Outcome.PENDING;
+ if (outcome == Outcome.MET) {
+ metRoutes.add(e.route);
+ }
JsonObject x = new JsonObject();
x.put("rule", id);
x.put("expect", e.spec());
@@ -182,8 +216,9 @@ final class ChaosExperiment {
jo.put("lostAtDeadLetterChannel", lost);
jo.put("lostAtDeadLetterChannelPerRule", lostAtDlq);
}
- jo.put("verdict", unmet || lost > 0 ? FAIL : !undecided ? PASS : ended
? INCONCLUSIVE : PENDING);
JsonArray routes = new JsonArray();
+ JsonObject alsoFailed = new JsonObject();
+ Set<String> active = new HashSet<>();
counts.forEach((route, c) -> {
long[] now0 = read(c);
long[] then = before.getOrDefault(route, new long[5]);
@@ -194,6 +229,7 @@ final class ChaosExperiment {
any |= delta[i] > 0;
}
if (any) {
+ active.add(route);
JsonObject r = new JsonObject();
r.put("route", route);
r.put("completed", delta[0]);
@@ -202,9 +238,38 @@ final class ChaosExperiment {
r.put("handled", delta[3]);
r.put("redeliveries", delta[4]);
routes.add(r);
+ if (delta[1] > 0 && !failedExpected.contains(route)) {
+ alsoFailed.put(route, delta[1]);
+ }
}
});
jo.put("routes", routes);
+ if (!alsoFailed.isEmpty()) {
+ jo.put("alsoFailed", alsoFailed);
+ }
+ // nothing fired: the expectations were not put to the test (an unmet
one is still a FAIL, with its evidence).
+ // This is for the experiment as a whole: when one of its rules fired,
the experiment was put to the test,
+ // and the expectations still decide each rule
+ boolean neverFired = firedAll == 0;
+ // an expectation met while its route had no messages says nothing
either, such as refunds:failed<=0 when the
+ // traffic went to another route
+ for (String route : unmetTimes) {
+ if (active.contains(route)) {
+ unmet = true;
+ } else {
+ // maxTime<=10s with no exchange in its route says nothing
about the time either
+ metRoutes.add(route);
+ }
+ }
+ metRoutes.removeAll(active);
+ boolean idle = !metRoutes.isEmpty();
+ String verdict = unmet || lost > 0
+ ? FAIL : !undecided && !neverFired && !idle ? PASS : ended ?
INCONCLUSIVE : PENDING;
+ jo.put("verdict", verdict);
+ if (INCONCLUSIVE.equals(verdict)) {
+ // never fired comes first: when no fault ran, the expectations
that did not decide are a consequence of it
+ jo.put("reason", neverFired ? NEVER_FIRED : idle ?
noMessages(metRoutes) : NOT_DECIDED);
+ }
if (countsReset) {
jo.put("countsReset", true);
}
@@ -239,11 +304,21 @@ final class ChaosExperiment {
sb.append("\n expect
").append(x.getString("expect")).append(": ").append(x.getString("state"))
.append(", ").append(x.getString("value"));
}
+ String reason = jo.getString("reason");
+ if (reason != null) {
+ sb.append("\n inconclusive: ").append(reason);
+ }
long lost = jo.getLongOrDefault("lostAtDeadLetterChannel", 0);
if (lost > 0) {
sb.append("\n lost at the dead letter channel:
").append(lost)
.append(lost == 1 ? " message" : " messages").append(",
handled there without being delivered");
}
+ JsonObject alsoFailed = (JsonObject) jo.get("alsoFailed");
+ if (alsoFailed != null) {
+ StringJoiner also = new StringJoiner(", ");
+ alsoFailed.forEach((route, n) -> also.add(route + " " + n));
+ sb.append("\n also failed, in no expectation:
").append(also);
+ }
StringJoiner sj = new StringJoiner(", ");
for (Object o : jo.getCollection("routes")) {
JsonObject r = (JsonObject) o;
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 404e77c66d24..f711af4f1304 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
@@ -452,9 +452,28 @@ public final class ChaosManager {
if (target != null) {
answer.add(target);
}
+ String dead = deadLetterOfFailedExpectation(rule);
+ if (dead != null) {
+ String route = rule.expectation.route;
+ answer.add("expects failed exchanges in route " + route + ", but
its error handler is the dead letter channel "
+ + dead + ", which handles what fails there, so nothing
fails: expect=" + route
+ + ":handled<=0 counts what the dead letter channel
handled");
+ }
return answer;
}
+ /**
+ * The dead letter channel of the route of an expectation of failed
exchanges ({@code expect=parked:failed<=0}), as
+ * such an expectation is always met there (CAMEL-26790), or null.
+ */
+ private String deadLetterOfFailedExpectation(ChaosRule rule) {
+ if (rule.expectation == null || rule.expectation.kind !=
ChaosRule.Expectation.Kind.FAILED) {
+ return null;
+ }
+ Route route = camelContext.getRoute(rule.expectation.route);
+ return route != null ? ChaosAdvisor.deadLetterUri(route) : null;
+ }
+
/**
* A rule in plain words, such as {@code fail half of the sends to
http:stock* with HTTP status 503, for 5 minutes},
* at the hooks where it can fire in the context now (see the {@code
hooks} of the report).
diff --git
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosExperimentTest.java
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosExperimentTest.java
index aa4ff153c363..afbf20d548cb 100644
---
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosExperimentTest.java
+++
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosExperimentTest.java
@@ -161,6 +161,94 @@ class ChaosExperimentTest extends ChaosTestSupport {
"lost at the dead letter channel: 3 messages, handled there
without being delivered");
}
+ @Test
+ void anExperimentWhoseFaultsNeverFiredDoesNotPass() {
+ chaos().startExploration("10m", null);
+ // the fault is on a step the orders do not reach (CAMEL-26790): its
expectation is met, but says nothing
+ chaos().beginExperiment("orders keep going when parking fails",
+ "endpoint=mock:parked error expect=orders:2 within=1m");
+ send(2);
+ assertThat(experiment(1).getString("verdict")).isEqualTo("PENDING");
+
+ Map<String, Object> ended = chaos().endExperiment(null);
+
+ assertThat(ended).containsEntry("verdict",
"INCONCLUSIVE").containsEntry("reason",
+ "its faults never fired, so its expectations say nothing about
them");
+ assertThat(first(new JsonObject(ended),
"expectations").getString("state")).isEqualTo("met");
+ assertThat(chaos().report().toText()).contains("1 INCONCLUSIVE orders
keep going when parking fails",
+ "inconclusive: its faults never fired");
+
+ // ended before its expectation was decided: the other reason
+ chaos().beginExperiment(null, "endpoint=mock:shipped delay
duration=10ms expect=orders:5 within=1m");
+ send(1);
+ assertThat(chaos().endExperiment(null)).containsEntry("verdict",
"INCONCLUSIVE").containsEntry("reason",
+ "it ended before an expectation was decided, so the messages
may still have come");
+ }
+
+ @Test
+ void anExpectationMetOnARouteWithoutMessagesDoesNotPass() {
+ chaos().startExploration("10m", null);
+ // the faults fire in orders, but the expectation is about billing,
which gets no message (CAMEL-26790)
+ chaos().beginExperiment("billing never fails", "endpoint=mock:shipped
error expect=billing:failed<=0 within=1m");
+ send(3);
+ clock.advance(61_000);
+
+ Map<String, Object> ended = chaos().endExperiment(null);
+
+ assertThat(ended).containsEntry("verdict",
"INCONCLUSIVE").containsEntry("reason",
+ "route billing had no messages while it ran, so its
expectation says nothing: feed it");
+ assertThat(first(new JsonObject(ended),
"expectations").getString("state")).isEqualTo("met");
+ assertThat(((JsonObject)
ended.get("faults")).getLong("r1")).isEqualTo(3);
+
+ // a time without an exchange in its route: unmet, but it says nothing
about the time either
+ chaos().beginExperiment("billing is fast", "endpoint=mock:shipped
error expect=billing:maxTime<=1s within=1m");
+ send(3);
+ clock.advance(61_000);
+ ended = chaos().endExperiment(null);
+ assertThat(ended).containsEntry("verdict",
"INCONCLUSIVE").containsEntry("reason",
+ "route billing had no messages while it ran, so its
expectation says nothing: feed it");
+ assertThat(first(new JsonObject(ended),
"expectations").getString("state")).isEqualTo("unmet");
+ }
+
+ @Test
+ void theRoutesThatFailedOutsideTheExpectationsAreListed() {
+ chaos().startExploration("10m", null);
+ // the expectation counts the completed orders, not the failed ones
(CAMEL-26790)
+ chaos().beginExperiment("orders keep coming", "endpoint=mock:shipped
error rate=every:2 expect=orders:2 within=1m");
+ send(4);
+
+ Map<String, Object> ended = chaos().endExperiment(null);
+
+ assertThat(ended).containsEntry("verdict", "PASS");
+ assertThat(((JsonObject)
ended.get("alsoFailed")).getLong("orders")).isEqualTo(2);
+ assertThat(chaos().report().toText()).contains("also failed, in no
expectation: orders 2");
+
+ // an expectation of the failures counts them: not listed again
+ chaos().beginExperiment(null, "endpoint=mock:shipped error
rate=every:2 expect=orders:failed<=5 within=1m");
+ send(2);
+ clock.advance(61_000);
+ assertThat(chaos().endExperiment(null)).containsEntry("verdict",
"PASS").doesNotContainKey("alsoFailed");
+ }
+
+ @Test
+ void anExpectationOfFailuresInARouteWithADeadLetterChannelIsWarned() {
+ // billing fails to its dead letter channel, which handles the
message: failed stays 0 there (CAMEL-26790)
+ JsonObject checked = (JsonObject) list(
+ new JsonObject(chaos().checkRules("endpoint=mock:charged error
expect=billing:failed<=0")), "rules")
+ .get(0);
+ assertThat(list(checked,
"warnings")).singleElement().asString().contains(
+ "expects failed exchanges in route billing, but its error
handler is the dead letter channel"
+
+ " direct:parked",
+ "expect=billing:handled<=0 counts what the dead letter channel
handled");
+
+ // what the dead letter channel handled, or a route without one: no
warning
+ for (String rule : List.of("endpoint=mock:charged error
expect=billing:handled<=0",
+ "endpoint=mock:shipped error expect=orders:failed<=0")) {
+ JsonObject ok = (JsonObject) list(new
JsonObject(chaos().checkRules(rule)), "rules").get(0);
+ assertThat(ok.containsKey("warnings")).as(rule).isFalse();
+ }
+ }
+
@Test
void oneAtATimeWithAnExpectationAndInAnExploration() {
String rule = "endpoint=mock:shipped error expect=orders:failed<=0
within=1m";
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosExperimentAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosExperimentAction.java
index e2b2404ec08c..3d6a42842a7f 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosExperimentAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosExperimentAction.java
@@ -47,9 +47,10 @@ import picocli.CommandLine;
"what the routes did, and the traffic. It stays
in the journal of the exploration",
"(camel get chaos --report).",
"%nThe verdict is FAIL when an expectation is
unmet (exit code 1), PASS when all are",
- "met, and INCONCLUSIVE when it ended before an
expectation was decided: wait longer",
- "than the within= of the expectation. --feed can
be the id of a route, as the feed:",
- "line of camel cmd chaos-suggest has it.",
+ "met, and INCONCLUSIVE when it cannot tell: it
ended before an expectation was decided",
+ "(wait longer than the within= of the
expectation), its faults never fired, or the",
+ "route of a met expectation had no messages.
--feed can be the id of a route, as the",
+ "feed: line of camel cmd chaos-suggest has it.",
"%nExamples:",
" camel cmd chaos-experiment --hypothesis='a
failed refund is read again' \\",
" --rule='route=refunds error rate=every:5
expect=refunds:failed<=0 within=30s' \\",
@@ -202,7 +203,9 @@ public class CamelChaosExperimentAction extends
CamelChaosAction {
if (!sb.isEmpty()) {
lines.add(" " + sb);
}
- if ("INCONCLUSIVE".equals(experiment.getString("verdict"))) {
+ // the reason of the verdict says why; waiting longer helps only when
it ended before a decision (CAMEL-26790)
+ String reason = experiment.getString("reason");
+ if ("INCONCLUSIVE".equals(experiment.getString("verdict")) && (reason
== null || reason.startsWith("it ended"))) {
lines.add(" An expectation was not decided when it ended: wait
longer than its within= (--wait)");
}
return lines;
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 4d26bf1f35c1..f52fb81f9dcd 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
@@ -389,11 +389,21 @@ public class ListChaos extends ProcessWatchCommand {
+ x.getString("value"));
}
}
+ String reason = experiment.getString("reason");
+ if (reason != null) {
+ lines.add(more + "inconclusive: " + reason);
+ }
long lost = experiment.getLongOrDefault("lostAtDeadLetterChannel", 0);
if (lost > 0) {
lines.add(more + "lost at the dead letter channel: " + lost +
(lost == 1 ? " message" : " messages")
+ ", handled there without being delivered");
}
+ JsonObject alsoFailed = (JsonObject) experiment.get("alsoFailed");
+ if (alsoFailed != null && !alsoFailed.isEmpty()) {
+ StringJoiner also = new StringJoiner(", ");
+ alsoFailed.forEach((route, n) -> also.add(route + " " + n));
+ lines.add(more + "also failed, in no expectation: " + also);
+ }
List<Object> routes = experiment.getCollection("routes");
if (routes != null && !routes.isEmpty()) {
lines.add(more + "routes: " + routeCounts(routes));
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
index 6a84a4bd54ce..a4ec332abbf1 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
@@ -573,6 +573,15 @@ class CamelChaosActionTest extends
ActionCommandTestSupport {
" waited 2m0s for the verdict; 8m15s left of the exploration",
" An expectation was not decided when it ended: wait longer
than its within= (--wait)");
+ // another reason: waiting longer would not help (CAMEL-26790)
+ JsonObject idle = new JsonObject(
+ Map.of("number", 4, "verdict", "INCONCLUSIVE", "rules", new
JsonArray(List.of("r4")),
+ "reason", "route billing had no messages while it ran,
so its expectation says nothing: feed it"));
+ assertThat(CamelChaosExperimentAction.lines(idle))
+ .contains(
+ " inconclusive: route billing had no messages while
it ran, so its expectation says nothing: feed it")
+ .doesNotContain(" An expectation was not decided when it
ended: wait longer than its within= (--wait)");
+
JsonObject notSent = new JsonObject(
Map.of("number", 3, "verdict", "PASS", "elapsed", 1_000L,
"traffic", new JsonObject(Map.of("endpoint",
"refunds", "status", "failed", "error", "No endpoint"))));
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 6c06f3e46c9c..f3e9a502f341 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
@@ -221,7 +221,20 @@ class ListChaosTest extends ProcessCommandTestSupport {
experiment.put("routes", new JsonArray(List.of(during)));
experiment.put("note", "orders has no error handler");
experiment.put("lostAtDeadLetterChannel", 58L);
- exploration.put("experiments", new JsonArray(List.of(experiment)));
+ JsonObject alsoFailed = new JsonObject();
+ alsoFailed.put("refunds", 2L);
+ experiment.put("alsoFailed", alsoFailed);
+ // its fault never fired
+ JsonObject second = new JsonObject();
+ second.put("number", 2);
+ second.put("open", false);
+ second.put("endedBy", "ended");
+ second.put("elapsed", 41_000L);
+ second.put("rules", new JsonArray(List.of("r3")));
+ second.put("verdict", "INCONCLUSIVE");
+ second.put("reason", "its faults never fired, so its expectations say
nothing about them");
+ second.put("expectations", new JsonArray());
+ exploration.put("experiments", new JsonArray(List.of(experiment,
second)));
((JsonObject) chaos.get("report")).put("exploration", exploration);
writeStatus(chaos);
@@ -233,8 +246,10 @@ class ListChaosTest extends ProcessCommandTestSupport {
"experiments:", "1 FAIL an order never fails (r1, r2, ended
after 1m4s)",
"expect orders:failed<=0: unmet, 9 failed",
"lost at the dead letter channel: 58 messages, handled there
without being delivered",
+ "also failed, in no expectation: refunds 2",
"routes: orders 41 completed, 9 failed",
- "note: orders has no error handler");
+ "note: orders has no error handler", "2 INCONCLUSIVE",
+ "inconclusive: its faults never fired, so its expectations say
nothing about them");
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
index db328bcb784c..2b32127c9287 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
@@ -807,11 +807,13 @@ what the routes did, and the traffic. It stays in the
journal of the exploration
The verdict is FAIL when an expectation is unmet (exit code 1), PASS when all
are
-met, and INCONCLUSIVE when it ended before an expectation was decided: wait
-longer
-than the within= of the expectation. --feed can be the id of a route, as the
-feed:
-line of camel cmd chaos-suggest has it.
+met, and INCONCLUSIVE when it cannot tell: it ended before an expectation was
+decided
+(wait longer than the within= of the expectation), its faults never fired, or
+the
+route of a met expectation had no messages. --feed can be the id of a route, as
+the
+feed: line of camel cmd chaos-suggest has it.
Examples:
camel cmd chaos-experiment --hypothesis='a failed refund is read again' \
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-parse.txt
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-parse.txt
index dbae4927d33f..e44083f6722e 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-parse.txt
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-parse.txt
@@ -551,6 +551,65 @@
(defaults)
> cmd chaos-clear --timeout 7
timeoutOption.timeout=7
+> cmd chaos-experiment
+ body=null
+ cmd:helpRequested=false
+ end=false
+ feed=null
+ headers=null
+ helpRequested=false
+ hypothesis=null
+ interval=null
+ name=null
+ note=null
+ repeat=0
+ rules=null
+ timeoutOption.timeout=10000
+ wait="2m"
+> cmd chaos-experiment --body v
+ body="v"
+> cmd chaos-experiment --end
+ end=true
+> cmd chaos-experiment --end=false
+ (defaults)
+> cmd chaos-experiment --feed v
+ feed="v"
+> cmd chaos-experiment --header v --header w
+ headers=["v", "w"]
+> cmd chaos-experiment --hypothesis v
+ hypothesis="v"
+> cmd chaos-experiment --interval v
+ interval="v"
+> cmd chaos-experiment --note v
+ note="v"
+> cmd chaos-experiment --repeat 7
+ repeat=7
+> cmd chaos-experiment --rule v --rule w
+ rules=["v", "w"]
+> cmd chaos-experiment --timeout 7
+ timeoutOption.timeout=7
+> cmd chaos-experiment --wait v
+ wait="v"
+> cmd chaos-explore-finish
+ cmd:helpRequested=false
+ helpRequested=false
+ name=null
+ timeoutOption.timeout=10000
+> cmd chaos-explore-finish --timeout 7
+ timeoutOption.timeout=7
+> cmd chaos-explore-start
+ budget="10m"
+ cmd:helpRequested=false
+ focus=null
+ helpRequested=false
+ name=null
+ timeoutOption.timeout=10000
+> cmd chaos-explore-start --budget v
+ budget="v"
+> cmd chaos-explore-start --focus v
+ focus="v"
+> cmd chaos-explore-start --timeout 7
+ timeoutOption.timeout=7
> cmd chaos-load v
cmd:helpRequested=false
file="v"
@@ -1090,6 +1149,7 @@
headers=null
helpRequested=false
infra=null
+ interval=null
loggingColor=true
mavenResolver.download=true
mavenResolver.fresh=false
@@ -1099,6 +1159,7 @@
pretty=false
propertiesFiles=null
property=null
+ repeat=0
reply=false
replyFile=null
showBody=true
@@ -1123,6 +1184,8 @@
headers=["v", "w"]
> cmd send --infra v
infra="v"
+> cmd send --interval v
+ interval="v"
> cmd send --logging-color
(defaults)
> cmd send --logging-color=false
@@ -1139,6 +1202,8 @@
propertiesFiles="v"
> cmd send --property v --property w
property=["v", "w"]
+> cmd send --repeat 7
+ repeat=7
> cmd send --reply
reply=true
> cmd send --reply=false