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 9b5b9ab26912 chore: camel-jbang - the run options form can turn on
chaos testing (--chaos) (#29567)
9b5b9ab26912 is described below
commit 9b5b9ab26912ab35a1474468396dc4321138d9b5
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 10 16:24:01 2026 +0100
chore: camel-jbang - the run options form can turn on chaos testing
(--chaos) (#29567)
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../pages/camel-jbang-tui-getting-started.adoc | 4 +-
.../ROOT/pages/camel-jbang-tui-observe.adoc | 3 +-
.../jbang/core/commands/tui/RunOptionsForm.java | 47 +++++++++++---
.../core/commands/tui/RunOptionsFormTest.java | 73 +++++++++++++++++++---
4 files changed, 109 insertions(+), 18 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-getting-started.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-getting-started.adoc
index e331ceca0ded..91e204bb826c 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-getting-started.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-getting-started.adoc
@@ -44,8 +44,8 @@ Before launching, a run options form lets you choose the
*runtime*: Camel Main (
Spring Boot, Quarkus, or JBang. This makes it easy to try any example on all
three runtimes without
changing a single line of code. The first three run the example in a separate
JVM that only
contains the dependencies of the example (like a production deployment), while
JBang runs it
-in-process in the Camel CLI JVM, which starts faster but has the CLI on the
classpath as well. You can also set the integration name, toggle dev mode, and
-add extra dependencies. When the port the app will listen on (the one you set,
else 8080) is taken already, the form
+in-process in the Camel CLI JVM, which starts faster but has the CLI on the
classpath as well. You can also set the integration name, toggle dev mode,
+turn on chaos testing (`--chaos`, to inject faults with camel-chaos; not with
profile prod), and add extra dependencies. When the port the app will listen on
(the one you set, else 8080) is taken already, the form
says by whom, so a second app with an HTTP server does not fail to start.
The example starts running in the background. The TUI auto-selects it as soon
as it appears.
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 f62c46e406fb..bbf7ec3e0a3f 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
@@ -311,7 +311,8 @@ Beyond the core tabs described above, the TUI includes
several specialized scree
=== Chaos
-With xref:next@components:others:chaos.adoc[camel-chaos] (`camel run
--chaos`), the Chaos tab (under More >
+With xref:next@components:others:chaos.adoc[camel-chaos] (`camel run --chaos`,
or *Chaos testing* in the run options
+form of the TUI), the Chaos tab (under More >
Observability) shows the faults that the chaos rules inject into the running
integration, and what they do:
* The verdict of the report (PASS or FAIL with the reason), the messages lost,
the expectations (met, unmet, pending)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
index b562236e6a57..1424843b0f5f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
@@ -57,7 +57,10 @@ class RunOptionsForm {
private static final int ROW_STUB = 11;
private static final int ROW_OTEL_AGENT = 12;
private static final int ROW_JFR = 13;
- private static final int ROW_COUNT = 14;
+ private static final int ROW_CHAOS = 14;
+ private static final int ROW_COUNT = 15;
+ /** The last row of the options page: Down and Tab go on from it to the
properties page. */
+ private static final int ROW_LAST = ROW_CHAOS;
/** Height of the options page: a row per option plus the border; the
properties page is aligned to it. */
private static final int PAGE1_HEIGHT = ROW_COUNT + 2;
@@ -117,6 +120,7 @@ class RunOptionsForm {
private boolean otelAgent;
private int otelExportTarget; // 0=TUI, 1=Jaeger
private boolean jfrEnabled;
+ private boolean chaos;
private boolean webConsole;
private String exampleTitle;
@@ -160,6 +164,7 @@ class RunOptionsForm {
otelAgent = false;
otelExportTarget = 0;
jfrEnabled = false;
+ chaos = false;
webConsole = false;
selectedRow = ROW_NAME;
page = PAGE_OPTIONS;
@@ -211,6 +216,15 @@ class RunOptionsForm {
return portWarning;
}
+ /** Chaos is asked for, and the profile is not prod (where camel-chaos
stays off). */
+ boolean isChaosEnabled() {
+ return chaos && !isProdProfile();
+ }
+
+ private boolean isProdProfile() {
+ return "prod".equals(PROFILE_VALUES[profileMode]);
+ }
+
boolean isJaegerExport() {
return otelAgent && otelExportTarget == 1;
}
@@ -319,6 +333,10 @@ class RunOptionsForm {
if (jfrEnabled) {
args.add("--jfr");
}
+ // camel-chaos stays off with profile prod, so the form does not ask
for it then
+ if (isChaosEnabled()) {
+ args.add("--chaos");
+ }
if (webConsole) {
args.add("--console");
}
@@ -346,7 +364,7 @@ class RunOptionsForm {
return true;
}
if (ke.isDown()) {
- if (selectedRow == ROW_JFR && hasProperties()) {
+ if (selectedRow == ROW_LAST && hasProperties()) {
page = PAGE_PROPERTIES;
selectedProperty = 0;
} else {
@@ -355,7 +373,7 @@ class RunOptionsForm {
return true;
}
if (ke.isFocusNext()) {
- if (selectedRow == ROW_JFR && hasProperties()) {
+ if (selectedRow == ROW_LAST && hasProperties()) {
page = PAGE_PROPERTIES;
selectedProperty = 0;
} else {
@@ -425,6 +443,10 @@ class RunOptionsForm {
case ROW_STUB -> stubMode = !stubMode;
case ROW_OTEL_AGENT -> otelAgent = !otelAgent;
case ROW_JFR -> jfrEnabled = !jfrEnabled;
+ case ROW_CHAOS -> {
+ if (!isProdProfile())
+ chaos = !chaos;
+ }
case ROW_CONSOLE -> webConsole = !webConsole;
}
return true;
@@ -457,7 +479,7 @@ class RunOptionsForm {
editingKey = false;
if (selectedProperty == 0) {
page = PAGE_OPTIONS;
- selectedRow = ROW_JFR;
+ selectedRow = ROW_LAST;
} else {
selectedProperty--;
}
@@ -487,7 +509,7 @@ class RunOptionsForm {
} else if (selectedProperty == 0) {
editingKey = false;
page = PAGE_OPTIONS;
- selectedRow = ROW_JFR;
+ selectedRow = ROW_LAST;
} else {
editingKey = false;
selectedProperty--;
@@ -503,7 +525,7 @@ class RunOptionsForm {
return true;
}
page = PAGE_OPTIONS;
- selectedRow = ROW_JFR;
+ selectedRow = ROW_LAST;
editingKey = false;
return true;
}
@@ -518,7 +540,7 @@ class RunOptionsForm {
}
if (properties.isEmpty()) {
page = PAGE_OPTIONS;
- selectedRow = ROW_JFR;
+ selectedRow = ROW_LAST;
}
return true;
}
@@ -648,6 +670,17 @@ class RunOptionsForm {
rowY++;
renderCheckbox(frame, innerX, rowY, innerW, "Java Flight Recorder
(JFR)", jfrEnabled, selectedRow == ROW_JFR);
+ rowY++;
+
+ if (isProdProfile()) {
+ frame.renderWidget(Paragraph.from(Line.from(
+ Span.styled(" [ ] Chaos testing (not available with
profile prod)",
+ selectedRow == ROW_CHAOS ?
Style.EMPTY.dim().reversed() : Style.EMPTY.dim()))),
+ new Rect(innerX, rowY, innerW, 1));
+ } else {
+ renderCheckbox(frame, innerX, rowY, innerW, "Chaos testing (inject
faults with camel-chaos)", chaos,
+ selectedRow == ROW_CHAOS);
+ }
if (warning != null) {
rowY++;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsFormTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsFormTest.java
index ee254414a8d1..09859c2f32cc 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsFormTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsFormTest.java
@@ -30,9 +30,9 @@ import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
- * Drives {@link RunOptionsForm} through key events to verify the JFR checkbox
row toggles {@code --jfr} into
- * {@link RunOptionsForm#buildArgs()}, and that inserting that row did not
break navigation past the other checkbox rows
- * or the down-wrap back to row 0.
+ * Drives {@link RunOptionsForm} through key events to verify the JFR and
chaos checkbox rows toggle {@code --jfr} and
+ * {@code --chaos} into {@link RunOptionsForm#buildArgs()}, and that inserting
those rows did not break navigation past
+ * the other checkbox rows or the down-wrap back to row 0.
*/
class RunOptionsFormTest {
@@ -80,11 +80,11 @@ class RunOptionsFormTest {
}
@Test
- void downNavigationWrapsFromJfrRowBackToNameRow() {
+ void downNavigationWrapsFromChaosRowBackToNameRow() {
RunOptionsForm form = openForm();
- // 14 rows total (0=Name .. 13=JFR); 14 downs from row 0 wraps back to
row 0 (Name).
- for (int i = 0; i < 14; i++) {
+ // 15 rows total (0=Name .. 13=JFR, 14=Chaos); 15 downs from row 0
wraps back to row 0 (Name).
+ for (int i = 0; i < 15; i++) {
form.handleKeyEvent(key(KeyCode.DOWN));
}
form.handleKeyEvent(KeyEvent.ofChar('x'));
@@ -107,6 +107,63 @@ class RunOptionsFormTest {
assertThat(args).doesNotContain("--jfr");
}
+ @Test
+ void chaosOffByDefaultOmitsFlag() {
+ RunOptionsForm form = openForm();
+
+ assertThat(form.buildArgs()).doesNotContain("--chaos");
+ assertThat(form.isChaosEnabled()).isFalse();
+ }
+
+ @Test
+ void togglingChaosRowAddsChaosFlag() {
+ RunOptionsForm form = openForm();
+
+ // up from Name wraps to the last row, Chaos
+ form.handleKeyEvent(key(KeyCode.UP));
+ form.handleKeyEvent(KeyEvent.ofChar(' '));
+
+
assertThat(form.buildArgs()).contains("--chaos").doesNotContain("--jfr");
+ assertThat(form.isChaosEnabled()).isTrue();
+ }
+
+ @Test
+ void chaosNotPassedWithProfileProd() {
+ RunOptionsForm form = openForm();
+
+ form.handleKeyEvent(key(KeyCode.UP));
+ form.handleKeyEvent(KeyEvent.ofChar(' '));
+ // Profile is row 2: cycle it from dev to prod
+ form.handleKeyEvent(key(KeyCode.DOWN));
+ form.handleKeyEvent(key(KeyCode.DOWN));
+ form.handleKeyEvent(key(KeyCode.DOWN));
+ form.handleKeyEvent(KeyEvent.ofChar(' '));
+
+
assertThat(form.buildArgs()).contains("--profile=prod").doesNotContain("--chaos");
+ assertThat(renderPopupRows(form)).anyMatch(row -> row.contains("Chaos
testing (not available with profile prod)"));
+ }
+
+ @Test
+ void chaosRowCannotBeToggledWithProfileProd() {
+ RunOptionsForm form = openForm();
+
+ // Profile is row 2: cycle it from dev to prod, then go up to the
Chaos row and try to tick it
+ form.handleKeyEvent(key(KeyCode.DOWN));
+ form.handleKeyEvent(key(KeyCode.DOWN));
+ form.handleKeyEvent(KeyEvent.ofChar(' '));
+ for (int i = 0; i < 3; i++) {
+ form.handleKeyEvent(key(KeyCode.UP));
+ }
+ form.handleKeyEvent(KeyEvent.ofChar(' '));
+ // back to dev: the box was not ticked while prod was selected
+ for (int i = 0; i < 3; i++) {
+ form.handleKeyEvent(key(KeyCode.DOWN));
+ }
+ form.handleKeyEvent(KeyEvent.ofChar(' '));
+
+
assertThat(form.buildArgs()).contains("--profile=dev").doesNotContain("--chaos");
+ }
+
@Test
void optionsPageHasNoBlankRows() {
RunOptionsForm form = openForm();
@@ -115,8 +172,8 @@ class RunOptionsFormTest {
List<String> inner = renderPopupRows(form);
- // Name .. JFR: one row each, nothing left empty below the last
checkbox
- assertThat(inner).hasSize(14 +
(form.portWarning(MonotonicClock.millis()) != null ? 1 : 0));
+ // Name .. Chaos: one row each, nothing left empty below the last
checkbox
+ assertThat(inner).hasSize(15 +
(form.portWarning(MonotonicClock.millis()) != null ? 1 : 0));
assertThat(inner).noneMatch(String::isBlank);
}