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 98c9e188dd80 chore: camel-jbang - the shell panel puts back the
printer it took, which made AiCliCommandExecutorTest flaky (#27237)
98c9e188dd80 is described below
commit 98c9e188dd803328200e28264e8c4716032547cb
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 1 23:44:45 2026 +0200
chore: camel-jbang - the shell panel puts back the printer it took, which
made AiCliCommandExecutorTest flaky (#27237)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../dsl/jbang/core/commands/tui/ShellPanel.java | 34 +++++--
.../core/commands/tui/ShellPanelLifecycleTest.java | 100 +++++++++++++++++++++
2 files changed, 126 insertions(+), 8 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
index 31b5de33a7e2..907b0ee7f6b9 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
@@ -63,6 +63,7 @@ import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.jline.utils.ScreenTerminal;
import org.jline.utils.ScreenTerminalOutputStream;
+import picocli.CommandLine;
/**
* Embeds a JLine interactive shell inside the TUI using a virtual terminal.
@@ -88,6 +89,7 @@ class ShellPanel {
private ScreenTerminal screenTerminal;
private LineDisciplineTerminal virtualTerminal;
private Thread shellThread;
+ private static final long SHELL_STOP_MILLIS = 2000;
private final ScrollbarState scrollbarState = new ScrollbarState();
@@ -443,6 +445,13 @@ class ShellPanel {
private String startError;
private void startShell(int width, int height) {
+ // the command line of the Camel CLI, taken here and not in the shell
thread: that thread starts later, and
+ // the shell must swap the printer of this command line only (a test
may point the static at another one by then)
+ CommandLine commandLine = CamelJBangMain.getCommandLine();
+ if (commandLine == null) {
+ startError = "The shell needs the Camel CLI";
+ return;
+ }
try {
screenTerminal = new ScreenTerminal(width, height);
lastWidth = width;
@@ -466,7 +475,8 @@ class ShellPanel {
delegateOut.delegate = new ScreenTerminalOutputStream(
screenTerminal, StandardCharsets.UTF_8, feedbackOutput);
- shellThread = new Thread(() -> runShell(virtualTerminal),
"tui-shell");
+ LineDisciplineTerminal terminal = virtualTerminal;
+ shellThread = new Thread(() -> runShell(terminal, commandLine),
"tui-shell");
shellThread.setDaemon(true);
shellThread.start();
} catch (Exception e) {
@@ -476,10 +486,10 @@ class ShellPanel {
}
}
- private void runShell(LineDisciplineTerminal terminal) {
+ private void runShell(LineDisciplineTerminal terminal, CommandLine
commandLine) {
try {
// TODO: replace with new PicocliCommandRegistry(commandLine,
"Camel") when JLine merges #1947
- PicocliCommandRegistry registry = new
PicocliCommandRegistry(CamelJBangMain.getCommandLine()) {
+ PicocliCommandRegistry registry = new
PicocliCommandRegistry(commandLine) {
@Override
public String name() {
return "Camel";
@@ -487,7 +497,7 @@ class ShellPanel {
};
// Redirect command output (printer()) through the virtual terminal
// so it renders in the shell panel instead of the TUI's real
terminal
- CamelJBangMain main = (CamelJBangMain)
CamelJBangMain.getCommandLine().getCommand();
+ CamelJBangMain main = (CamelJBangMain) commandLine.getCommand();
Printer originalPrinter = main.getOut();
Printer terminalPrinter = new Printer() {
@Override
@@ -560,10 +570,7 @@ class ShellPanel {
}
private void stopShell() {
- if (shellThread != null) {
- shellThread.interrupt();
- shellThread = null;
- }
+ // closing the terminal ends the shell's input, so it returns and puts
the printer it swapped back
if (virtualTerminal != null) {
try {
virtualTerminal.close();
@@ -572,6 +579,17 @@ class ShellPanel {
}
virtualTerminal = null;
}
+ Thread thread = shellThread;
+ shellThread = null;
+ if (thread != null) {
+ thread.interrupt();
+ try {
+ // wait for the shell to have restored the printer, so nothing
writes to this panel afterwards
+ thread.join(SHELL_STOP_MILLIS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
screenTerminal = null;
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelLifecycleTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelLifecycleTest.java
new file mode 100644
index 000000000000..a78c1cbaa777
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelLifecycleTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.dsl.jbang.core.commands.tui;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.TimeUnit;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.Printer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The shell of the shell panel swaps the printer of the Camel CLI to write
into the panel. It must swap the printer of
+ * the command line it started with, and put it back when the panel is
destroyed: the shell thread starts later than the
+ * panel, and other code (AiCliCommandExecutorTest) points the static command
line at another main in the meantime,
+ * whose printer was taken and never given back, which made that test flaky.
+ */
+class ShellPanelLifecycleTest {
+
+ private Field commandLineField;
+ private Object previous;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ Theme.resetForTesting();
+ commandLineField =
CamelJBangMain.class.getDeclaredField("commandLine");
+ commandLineField.setAccessible(true);
+ previous = commandLineField.get(null);
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ commandLineField.set(null, previous);
+ }
+
+ private static Buffer render(ShellPanel panel) {
+ Rect area = new Rect(0, 0, 80, 10);
+ Buffer buffer = Buffer.empty(area);
+ panel.render(Frame.forTesting(buffer), area);
+ return buffer;
+ }
+
+ @Test
+ void theShellSwapsThePrinterOfItsOwnCommandLineAndPutsItBack() throws
Exception {
+ CamelJBangMain mainA = new CamelJBangMain();
+ Printer printerA = mainA.getOut();
+ CamelJBangMain mainB = new CamelJBangMain();
+ Printer printerB = mainB.getOut();
+
+ commandLineField.set(null, new CommandLine(mainA));
+ ShellPanel panel = new ShellPanel();
+ panel.open();
+ render(panel);
+ // as another test does while the shell thread is still starting
+ commandLineField.set(null, new CommandLine(mainB));
+
+ await().atMost(10, TimeUnit.SECONDS).until(() -> mainA.getOut() !=
printerA);
+ assertNotSame(printerA, mainA.getOut(), "the shell writes into the
panel");
+ assertSame(printerB, mainB.getOut(), "the shell must not take the
printer of another command line");
+
+ panel.destroy();
+ assertSame(printerA, mainA.getOut(), "the printer is put back before
destroy returns");
+ }
+
+ @Test
+ void noShellWithoutTheCamelCli() throws Exception {
+ commandLineField.set(null, null);
+ ShellPanel panel = new ShellPanel();
+ panel.open();
+ String rendered = HealthTabRenderTest.bufferToString(render(panel));
+ panel.destroy();
+
+ assertTrue(rendered.contains("The shell needs the Camel CLI"),
rendered);
+ }
+}