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);
+    }
+}

Reply via email to