This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch tui-chaos-explore-start in repository https://gitbox.apache.org/repos/asf/camel.git
commit f3320abb6899fdf1a5783a5d06521ae5ff47ef36 Author: Claus Ibsen <[email protected]> AuthorDate: Tue Nov 10 17:56:36 2026 +0100 chore: camel-jbang - let there be chaos: polish from trying it on more examples The picks take a kind of fault once before it is tried again in another route (message-size has three hand-offs of one kind), L says why when chaos is off or not known yet, and the dialog says what to do when every route gets its messages from outside (http-client). Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../ROOT/pages/camel-jbang-tui-observe.adoc | 7 ++-- .../dsl/jbang/core/commands/tui/ChaosControl.java | 12 +++++- .../core/commands/tui/ChaosExploreDialog.java | 48 +++++++++++++++++----- .../dsl/jbang/core/commands/tui/ChaosTab.java | 2 + .../src/main/resources/tui/help/chaos.md | 8 ++-- .../jbang/core/commands/tui/ChaosExploreTest.java | 29 +++++++++++++ 6 files changed, 89 insertions(+), 17 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc index eb9fb55ead48..8ca57af15b05 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc @@ -395,9 +395,10 @@ that never fired, or a route that had no messages). *l* or Esc shows the rules a Press *L* to start an exploration by hand, on the integration you look at: pick the rules to try from what the integration suggests, the most worth trying first, each with what should hold despite its faults, and a budget of 2 to -30 minutes. The single faults not tried yet are picked, of the routes that get messages by themselves (a timer, or a -route they are called from); a route whose messages come from outside is marked, as its rule says nothing without -messages. *Space* picks a rule, *Left* and *Right* change the budget, and *Enter* starts. The picked rules are tried one +30 minutes. Picked are a few faults not tried yet, each kind once before it is tried again in another route, and then +two faults at once, such as an outage while the dead letter channel fails; only of the routes that get messages by +themselves (a timer, or a route they are called from). A route whose messages come from outside is marked, as its rule +says nothing without messages. *Space* picks a rule, *Left* and *Right* change the budget, and *Enter* starts. The picked rules are tried one at a time, each for its share of the budget, and each ends with PASS, FAIL or INCONCLUSIVE, while the line above the experiments says which rule is tried. *x* stops it, which removes the rules at once. Every rule is removed when the budget ends, also when the TUI is closed. For a longer exploration, where an AI picks what to try next, use diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java index 45442bd1b7de..93d290500f57 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java @@ -358,7 +358,17 @@ final class ChaosControl { * suggests to pick from, or says why it cannot start. */ void openExplore(IntegrationInfo info) { - if (info == null || info.chaos == null || !info.chaos.enabled) { + if (info == null) { + return; + } + if (info.chaos == null || !info.chaos.enabled) { + // the key says why it does nothing: chaos is off, or the status of the integration has not said yet + name = info.name; + rule = null; + command = Command.ADD; + result("Let there be chaos", true, info.chaos == null + ? "camel-chaos is not in " + info.name + " (yet): run it with camel run --chaos" + : info.chaos.reason != null ? info.chaos.reason : "Chaos is off in " + info.name); return; } pid = info.pid; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreDialog.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreDialog.java index 6098e30fd15d..65c21a00d316 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreDialog.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreDialog.java @@ -18,7 +18,9 @@ package org.apache.camel.dsl.jbang.core.commands.tui; import java.util.ArrayList; import java.util.EnumSet; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -113,16 +115,39 @@ final class ChaosExploreDialog { // the faults not tried yet whose routes get messages by themselves, the most worth trying first: a few single // ones, then two at once, as most bugs that lose messages need two failures that meet; a route without messages // says nothing, and a dead letter channel fails only when another fault sends it a message (the pair does) + items.forEach(item -> picked.add(false)); + // a kind of fault once, before one is tried again in another route: the budget asks more questions so + pick(true); + pick(false); + } + + private void pick(boolean distinctKinds) { + Set<String> kinds = new HashSet<>(); int singles = 0; int combined = 0; - for (Item item : items) { - boolean worth = !item.tried() && item.fed() && item.expect() != null && !item.deadLetter(); - boolean pick = worth && (item.combined() ? combined < PICKED_COMBINED : singles < PICKED_SINGLE); - picked.add(pick); - if (pick && item.combined()) { - combined++; - } else if (pick) { - singles++; + for (int i = 0; i < items.size(); i++) { + Item item = items.get(i); + if (picked.get(i)) { + kinds.add(item.kind()); + if (item.combined()) { + combined++; + } else { + singles++; + } + } + } + for (int i = 0; i < items.size(); i++) { + Item item = items.get(i); + boolean worth = !picked.get(i) && !item.tried() && item.fed() && item.expect() != null && !item.deadLetter() + && (!distinctKinds || !kinds.contains(item.kind())); + if (worth && (item.combined() ? combined < PICKED_COMBINED : singles < PICKED_SINGLE)) { + picked.set(i, true); + kinds.add(item.kind()); + if (item.combined()) { + combined++; + } else { + singles++; + } } } } @@ -159,7 +184,10 @@ final class ChaosExploreDialog { /** Why the exploration cannot start, or null when it can. */ String cannotStart() { if (picked().isEmpty()) { - return "Pick a rule to try (Space)"; + return !items.isEmpty() && items.stream().noneMatch(Item::fed) + ? "The routes here get their messages from outside: pick a rule (Space), and send its route messages" + + " while it runs, or its verdict says nothing" + : "Pick a rule to try (Space)"; } if (share() < MIN_SHARE) { return "Too many rules for " + budget() + ": pick fewer, or a longer budget"; @@ -189,7 +217,7 @@ final class ChaosExploreDialog { void render(Frame frame, Rect area) { int width = Math.min(area.width() - 4, 150); - int height = Math.min(area.height() - 2, Math.max(16, items.size() + 10)); + int height = Math.min(area.height() - 2, items.isEmpty() ? 5 : Math.max(16, items.size() + 10)); Rect popup = DialogHelper.centered(area, width, height); frame.renderWidget(Clear.INSTANCE, popup); Block outer = Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(Theme.warning()) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java index 8a02e7cdc47b..5441d686df5d 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java @@ -248,6 +248,8 @@ class ChaosTab extends AbstractTableTab { .text(Text.from(Line.from(Span.raw("")), Line.from(Span.styled(" " + why, Theme.muted())))) .block(block(" Chaos ")) .build(), area); + // such as why L does nothing here + control.render(frame, area); return; } if (explore) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md index 8247f935b2e6..accfdea91b6f 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md @@ -78,9 +78,11 @@ Esc shows the rules again. **L** starts an exploration here, by hand: pick the rules to try from the suggestions of the integration (the same as **w**), the most worth trying -first, each with what should hold despite its faults. The single faults -not tried yet are picked, of routes that get messages by themselves (a -timer, or a route they are called from); a route whose messages come from +first, each with what should hold despite its faults. Picked are a few +faults not tried yet, each kind once before it is tried again in another +route, and then two faults at once (an outage while the dead letter +channel fails); only of routes that get messages by themselves (a timer, +or a route they are called from). A route whose messages come from outside is marked *needs messages*: send it some while its rule runs, or its verdict says nothing. Space picks a rule, Left and Right change the budget (2m to 30m), and Enter starts. The picked rules are tried one at a diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreTest.java index dc2c1e5414f5..b0813916ce48 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreTest.java @@ -34,6 +34,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; import static org.awaitility.Awaitility.await; /** @@ -210,6 +211,34 @@ class ChaosExploreTest { assertThat(footer()).contains("L", "let there be chaos"); } + @Test + void aKindOfFaultOnceBeforeItIsTriedAgainInAnotherRoute() throws Exception { + JsonObject reply = (JsonObject) Jsoner.deserialize(""" + {"suggestions": [ + {"route": "small", "kind": "lost", "title": "The route after a hand-off fails", + "rule": "route=a error expect=small->a:lost<=0", "feed": {"kind": "self", "fed": true}}, + {"route": "medium", "kind": "lost", "title": "The route after a hand-off fails", + "rule": "route=b error expect=medium->b:lost<=0", "feed": {"kind": "self", "fed": true}}, + {"route": "large", "kind": "lost", "title": "The route after a hand-off fails", + "rule": "route=c error expect=large->c:lost<=0", "feed": {"kind": "self", "fed": true}}, + {"route": "small", "kind": "errors", "title": "Returns an error (HTTP status)", + "rule": "endpoint=http://x error status=503 expect=small:failed<=0", "feed": {"kind": "self", "fed": true}} + ]} + """); + ChaosExploreDialog dialog = new ChaosExploreDialog("shop", reply); + // the HTTP error before a second hand-off, then the hand-offs again while there is room + assertThat(dialog.picked()).extracting(ChaosExploreDialog.Item::route, ChaosExploreDialog.Item::kind) + .containsExactly(tuple("small", "lost"), tuple("medium", "lost"), tuple("small", "errors")); + } + + @Test + void lSaysWhyWhenChaosIsOff() { + info.chaos = null; + key('L'); + assertThat(TuiTestHelper.renderToString(tab, 160, 40)) + .contains("camel-chaos is not in shop (yet): run it with camel run --chaos"); + } + @Test void oneExplorationAtATime() throws Exception { info.chaos = ChaosInfo.parse((JsonObject) Jsoner.deserialize("""
