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 d6eaeb5a3347 CAMEL-23688: camel-jbang - Tests for get history and 
restart (#27966)
d6eaeb5a3347 is described below

commit d6eaeb5a334734504418deed1e4e0cdf03409bcd
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 16 21:45:42 2026 +0200

    CAMEL-23688: camel-jbang - Tests for get history and restart (#27966)
    
    Tests for camel get history (28% covered) and camel restart (22%), which
    now also covers a successful restart: the integration is a shell script
    that writes the status of a started Camel for its pid. They found these
    bugs, fixed here:
    
    * camel get history failed with a NullPointerException for a failed step
      with --show-exception=false (or without an exception). --diagram and
      --it with several integrations printed a message on stdout and exited
      with 1; they are usage errors now (2), and the errors of --diagram go to
      stderr. --json ignored --mask.
    * camel restart rebuilt the command line of an integration from a single
      text (as on Windows) with a backslash escaping the next character, so a
      path such as C:\Program Files\Java lost its backslashes; it now follows
      the Windows quoting rules (backslashes count only in front of a quote).
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   7 +
 .../core/commands/action/CamelHistoryAction.java   | 158 ++++++------
 .../core/commands/process/RestartProcess.java      |  33 ++-
 .../commands/action/CamelHistoryActionTest.java    | 265 ++++++++++++++++++---
 .../core/commands/process/RestartProcessTest.java  |  89 ++++++-
 5 files changed, 439 insertions(+), 113 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index cf05e55fa98e..740921d04aea 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -514,6 +514,13 @@ The commands that sort their rows by `--sort`, such as 
`camel ps`, `camel get ro
 `camel catalog component`, exit with `2` and list their keys when the key is 
unknown: `camel ps --sort=nmae` printed
 the integrations unsorted. A row without the value of the key now comes first, 
instead of failing the command.
 
+=== camel-jbang - camel get history
+
+* `camel get history --diagram` and `--it` exit with `2` when several 
integrations have a message history, and ask for
+  the name or pid of one (they printed a message on stdout and exited with 
`1`). The errors of `--diagram` are printed on
+  stderr, and a graphical `--theme` in a terminal without graphics exits with 
`2`.
+* `camel get history --json --mask` masks the secrets of the endpoints in the 
JSON too, as in the table.
+
 === camel-jbang - exit codes of config set, dependency list, jolokia and cmd 
memory-leak
 
 * `camel jolokia` with a name or pid that matches no running integration, or 
more than one, exits with `3`, as
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHistoryAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHistoryAction.java
index 11cf64001c85..056b02e7ef6f 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHistoryAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHistoryAction.java
@@ -48,6 +48,8 @@ import 
org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
 import org.apache.camel.diagram.RouteDiagramRenderer;
 import org.apache.camel.diagram.RouteDiagramRenderer.DiagramColors;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
 import org.apache.camel.dsl.jbang.core.common.JsonOutput;
 import org.apache.camel.dsl.jbang.core.common.ProcessFinder;
 import org.apache.camel.dsl.jbang.core.common.ProcessFinder.CamelProcess;
@@ -201,6 +203,18 @@ public class CamelHistoryAction extends ActionWatchCommand 
{
                     }
                 }
             }
+            if (mask) {
+                // the endpoints without their secrets, as in the table
+                for (Object entry : entries) {
+                    if (entry instanceof JsonObject jo) {
+                        if (jo.get("traces") instanceof JsonArray traces) {
+                            traces.forEach(CamelHistoryAction::maskTrace);
+                        } else {
+                            maskTrace(jo);
+                        }
+                    }
+                }
+            }
             JsonOutput.printList(printer(), entries);
             return 0;
         }
@@ -210,8 +224,8 @@ public class CamelHistoryAction extends ActionWatchCommand {
         if (!pids.isEmpty()) {
             if (diagram) {
                 if (pids.size() > 1) {
-                    printer().println("Diagram mode only operate on a single 
Camel application");
-                    return 1;
+                    throw CliException.usage("--diagram shows the history of 
one integration, and " + pids.size()
+                                             + " integrations have one: give 
the name or pid of the integration");
                 }
                 List<Row> rows = pids.get(0);
                 long pid = rows.get(0).pid;
@@ -219,8 +233,8 @@ public class CamelHistoryAction extends ActionWatchCommand {
             }
             if (it) {
                 if (pids.size() > 1) {
-                    printer().println("Interactive mode only operate on a 
single Camel application");
-                    return 1;
+                    throw CliException.usage("--it shows the history of one 
integration, and " + pids.size()
+                                             + " integrations have one: give 
the name or pid of the integration");
                 }
                 return doInteractiveCall(pids.get(0));
             }
@@ -389,8 +403,8 @@ public class CamelHistoryAction extends ActionWatchCommand {
         }
 
         if (nodeIds.isEmpty()) {
-            printer().println("No node IDs found in message history");
-            return 1;
+            printer().printErr("No node IDs found in message history");
+            return CliExitCode.FAILURE;
         }
 
         // auto-detect style from exchange status
@@ -406,78 +420,85 @@ public class CamelHistoryAction extends 
ActionWatchCommand {
             action.put("metric", false);
         });
 
-        try {
-            List<RouteInfo> routes = 
RouteDiagramHelper.parseRoutes(structureJson);
-            if (routes.isEmpty()) {
-                printer().println("No routes found");
-                return 1;
-            }
+        List<RouteInfo> routes = RouteDiagramHelper.parseRoutes(structureJson);
+        if (routes.isEmpty()) {
+            printer().printErr("No routes found");
+            return CliExitCode.FAILURE;
+        }
 
-            // add structural parent node IDs for each route that has 
highlighted nodes
-            // (the history trace only has processor nodeIds, not structural 
route/from
-            // or scope EIP nodes like circuitBreaker, filter, split, doTry, 
etc.)
-            for (RouteInfo route : routes) {
-                boolean routeHasHighlight = route.nodes.stream().anyMatch(n -> 
n.id != null && nodeIds.contains(n.id));
-                if (routeHasHighlight) {
-                    addParentNodes(route.nodes, nodeIds);
-                }
+        // add structural parent node IDs for each route that has highlighted 
nodes
+        // (the history trace only has processor nodeIds, not structural 
route/from
+        // or scope EIP nodes like circuitBreaker, filter, split, doTry, etc.)
+        for (RouteInfo route : routes) {
+            boolean routeHasHighlight = route.nodes.stream().anyMatch(n -> 
n.id != null && nodeIds.contains(n.id));
+            if (routeHasHighlight) {
+                addParentNodes(route.nodes, nodeIds);
             }
+        }
 
-            // filter and order routes by highlighted path
-            RouteDiagramHelper.HighlightInfo highlightInfo
-                    = new RouteDiagramHelper.HighlightInfo(nodeIds, new 
ArrayList<>(routeOrderSet), hlStyle);
-            routes = RouteDiagramHelper.filterAndOrderRoutes(routes, 
highlightInfo);
-            if (routes.isEmpty()) {
-                printer().println("No routes contain highlighted nodes from 
message history");
-                return 1;
-            }
+        // filter and order routes by highlighted path
+        RouteDiagramHelper.HighlightInfo highlightInfo
+                = new RouteDiagramHelper.HighlightInfo(nodeIds, new 
ArrayList<>(routeOrderSet), hlStyle);
+        routes = RouteDiagramHelper.filterAndOrderRoutes(routes, 
highlightInfo);
+        if (routes.isEmpty()) {
+            printer().printErr("No routes contain highlighted nodes from 
message history");
+            return CliExitCode.FAILURE;
+        }
 
-            // layout
-            RouteDiagramLayoutEngine engine
-                    = new RouteDiagramLayoutEngine(180, 12, 
NodeLabelMode.BOTH);
+        // layout
+        RouteDiagramLayoutEngine engine
+                = new RouteDiagramLayoutEngine(180, 12, NodeLabelMode.BOTH);
 
-            List<LayoutRoute> layoutRoutes = new ArrayList<>();
-            int currentY = RouteDiagramLayoutEngine.PADDING;
-            for (RouteInfo route : routes) {
-                LayoutRoute lr = engine.layoutRoute(route, currentY);
-                layoutRoutes.add(lr);
-                currentY = lr.maxY + RouteDiagramLayoutEngine.V_GAP;
-            }
+        List<LayoutRoute> layoutRoutes = new ArrayList<>();
+        int currentY = RouteDiagramLayoutEngine.PADDING;
+        for (RouteInfo route : routes) {
+            LayoutRoute lr = engine.layoutRoute(route, currentY);
+            layoutRoutes.add(lr);
+            currentY = lr.maxY + RouteDiagramLayoutEngine.V_GAP;
+        }
 
-            boolean textMode = isTextTheme();
-            if (textMode) {
-                RouteDiagramAsciiRenderer asciiRenderer
-                        = new RouteDiagramAsciiRenderer(engine.getNodeWidth(), 
isUnicodeTheme());
-                String ascii = asciiRenderer.renderDiagramAnsi(layoutRoutes, 
currentY, nodeIds, hlStyle);
-                printer().println(ascii);
-            } else {
-                DiagramColors colors = DiagramColors.parse(theme);
-                RouteDiagramRenderer renderer = new RouteDiagramRenderer(
-                        engine.getNodeWidth(), 12 * 
RouteDiagramLayoutEngine.SCALE, engine.getNodeTextPadding(), false);
+        boolean textMode = isTextTheme();
+        if (textMode) {
+            RouteDiagramAsciiRenderer asciiRenderer
+                    = new RouteDiagramAsciiRenderer(engine.getNodeWidth(), 
isUnicodeTheme());
+            String ascii = asciiRenderer.renderDiagramAnsi(layoutRoutes, 
currentY, nodeIds, hlStyle);
+            printer().println(ascii);
+        } else {
+            DiagramColors colors = DiagramColors.parse(theme);
+            RouteDiagramRenderer renderer = new RouteDiagramRenderer(
+                    engine.getNodeWidth(), 12 * 
RouteDiagramLayoutEngine.SCALE, engine.getNodeTextPadding(), false);
+
+            BufferedImage image;
+            try {
+                image = renderer.renderDiagram(layoutRoutes, currentY, colors, 
nodeIds, hlStyle);
+            } catch (IllegalStateException e) {
+                printer().printErr(e.getMessage());
+                return CliExitCode.FAILURE;
+            }
 
-                BufferedImage image;
-                try {
-                    image = renderer.renderDiagram(layoutRoutes, currentY, 
colors, nodeIds, hlStyle);
-                } catch (IllegalStateException e) {
-                    printer().println(e.getMessage());
-                    return 1;
+            try (Terminal terminal = 
TerminalBuilder.builder().system(true).build()) {
+                TerminalGraphics tg = 
TerminalGraphicsManager.getBestProtocol(terminal).orElse(null);
+                if (tg == null) {
+                    printer().printErr(
+                            "Terminal does not support graphics. Use 
--theme=ascii or --theme=unicode for text output.");
+                    return CliExitCode.USAGE;
                 }
+                TerminalGraphics.ImageOptions opts = new 
TerminalGraphics.ImageOptions().preserveAspectRatio(true);
+                tg.displayImage(terminal, image, opts);
+                terminal.writer().println();
+                terminal.flush();
+            }
+        }
+        return 0;
+    }
 
-                try (Terminal terminal = 
TerminalBuilder.builder().system(true).build()) {
-                    TerminalGraphics tg = 
TerminalGraphicsManager.getBestProtocol(terminal).orElse(null);
-                    if (tg == null) {
-                        printer().println(
-                                "Terminal does not support graphics. Use 
--theme=ascii or --theme=unicode for text output.");
-                        return 1;
-                    }
-                    TerminalGraphics.ImageOptions opts = new 
TerminalGraphics.ImageOptions().preserveAspectRatio(true);
-                    tg.displayImage(terminal, image, opts);
-                    terminal.writer().println();
-                    terminal.flush();
+    private static void maskTrace(Object trace) {
+        if (trace instanceof JsonObject jo) {
+            for (String key : List.of("endpointUri", "nodeLabel")) {
+                if (jo.get(key) instanceof String text) {
+                    jo.put(key, URISupport.sanitizeUri(text));
                 }
             }
-            return 0;
-        } finally {
         }
     }
 
@@ -888,7 +909,8 @@ public class CamelHistoryAction extends ActionWatchCommand {
     private String getMessage(Row r) {
         boolean original = r.routeId != null && 
r.routeId.equals(r.fromRouteId);
         if (r.failed && !r.last) {
-            return "Exception: " + r.exception.getString("message");
+            // the exception is left out with --show-exception=false
+            return r.exception != null ? "Exception: " + 
r.exception.getString("message") : "Failed";
         }
         if (r.last && original) {
             return r.failed ? "Failed" : "Success";
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/RestartProcess.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/RestartProcess.java
index f9f4bbab00ca..5a6359a5539c 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/RestartProcess.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/RestartProcess.java
@@ -168,19 +168,38 @@ public class RestartProcess extends ProcessBaseCommand {
                                    + log);
     }
 
+    /**
+     * The arguments of a command line in one text (as Windows has it): a 
space separates them, except between quotes,
+     * and a backslash is a character, except in front of a quote, where 2n 
backslashes are n backslashes and the quote
+     * starts or ends a quoted part, and 2n + 1 backslashes are n backslashes 
and a quote character.
+     */
     static List<String> parseCommandLine(String commandLine) {
         List<String> tokens = new ArrayList<>();
         StringBuilder current = new StringBuilder();
         boolean inQuotes = false;
-        boolean escaped = false;
+        int length = commandLine.length();
 
-        for (int i = 0; i < commandLine.length(); i++) {
+        for (int i = 0; i < length; i++) {
             char c = commandLine.charAt(i);
-            if (escaped) {
-                current.append(c);
-                escaped = false;
-            } else if (c == '\\') {
-                escaped = true;
+            if (c == '\\') {
+                int backslashes = 1;
+                while (i + backslashes < length && commandLine.charAt(i + 
backslashes) == '\\') {
+                    backslashes++;
+                }
+                if (i + backslashes < length && commandLine.charAt(i + 
backslashes) == '"') {
+                    current.append("\\".repeat(backslashes / 2));
+                    if (backslashes % 2 == 1) {
+                        current.append('"');
+                    } else {
+                        inQuotes = !inQuotes;
+                    }
+                    // the quote too
+                    i += backslashes;
+                } else {
+                    // such as in a Windows path
+                    current.append("\\".repeat(backslashes));
+                    i += backslashes - 1;
+                }
             } else if (c == '"') {
                 inQuotes = !inQuotes;
             } else if (c == ' ' && !inQuotes) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHistoryActionTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHistoryActionTest.java
index 521368d55234..ea8689acef97 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHistoryActionTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHistoryActionTest.java
@@ -16,23 +16,37 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.action;
 
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
 import org.mockito.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mockStatic;
 
+/**
+ * camel get history: the message history of the last completed exchange of 
the integrations.
+ */
 @ExtendWith(MockitoExtension.class)
 class CamelHistoryActionTest extends ActionCommandTestSupport {
 
     private static final long FIXED_TIMESTAMP = 1_700_000_000_000L;
+    private static final long OTHER_PID = 23456L;
 
     @Test
     void testRendersMessageHistoryFromHistoryFile() throws Exception {
@@ -40,39 +54,20 @@ class CamelHistoryActionTest extends 
ActionCommandTestSupport {
         // CamelHistoryAction reads a pre-existing <pid>-history.json, not the 
action/output round trip
         writeMessageHistoryFile(TEST_PID, singleTraceLine());
 
-        CamelHistoryAction command = new CamelHistoryAction(new 
CamelJBangMain().withPrinter(printer));
-        command.name = "myApp";
-        // options are normally defaulted by picocli; set them as we construct 
the command directly
-        // depth defaults to 9 in picocli; without it depth=0 filters down to 
only level-1 created/completed nodes
-        command.depth = 9;
-        command.loggingColor = false;
-
-        int exit = callWithSingleProcess(command);
-
-        assertEquals(0, exit);
+        assertThat(callWithSingleProcess(command("myApp"))).isZero();
 
-        String out = printer.getOutput();
-        assertTrue(out.contains("Message History of last completed"),
-                "should print the message history header, was: " + out);
-        assertTrue(out.contains("myApp"), "should print the integration name, 
was: " + out);
-        assertTrue(out.contains("ABCDEFGH-0001"), "should print the exchange 
id, was: " + out);
+        assertThat(printer.getOutput()).contains("Message History of last 
completed", "myApp", "ABCDEFGH-0001");
     }
 
     @Test
     void testJsonOutputIsTheRecordedHistory() throws Exception {
         writeStatusFile(TEST_PID, "myApp");
         writeMessageHistoryFile(TEST_PID, singleTraceLine());
-        CamelHistoryAction command = new CamelHistoryAction(new 
CamelJBangMain().withPrinter(printer));
-        command.name = "myApp";
-        command.depth = 9;
-        command.loggingColor = false;
-        command.jsonOutput = true;
-        int exit = callWithSingleProcess(command);
-        assertEquals(0, exit);
-        String out = printer.getOutput();
-        assertTrue(out.contains("\"traces\""), "the recorded history as JSON, 
was: " + out);
-        assertTrue(out.contains("ABCDEFGH-0001"), "the exchange id in the 
JSON, was: " + out);
-        assertTrue(!out.contains("Message History of last completed"), "no 
table in JSON mode, was: " + out);
+
+        assertThat(callWithSingleProcess(command("myApp", "--json"))).isZero();
+
+        assertThat(printer.getOutput()).contains("\"traces\"", "ABCDEFGH-0001")
+                .doesNotContain("Message History of last completed");
     }
 
     @Test
@@ -80,15 +75,137 @@ class CamelHistoryActionTest extends 
ActionCommandTestSupport {
         writeStatusFile(TEST_PID, "myApp");
         writeMessageHistoryFile(TEST_PID, singleTraceLine());
 
-        CamelHistoryAction command = new CamelHistoryAction(new 
CamelJBangMain().withPrinter(printer));
-        command.name = "doesNotExist";
-        command.loggingColor = false;
+        CamelHistoryAction command = command("doesNotExist");
+
+        assertThatThrownBy(() -> callWithSingleProcess(command))
+                .isInstanceOfSatisfying(CliException.class,
+                        e -> 
assertThat(e.getExitCode()).isEqualTo(CliExitCode.NOT_FOUND))
+                .hasMessage("No running Camel integration matches: 
doesNotExist");
+        assertThat(printer.getOutput()).as("nothing should be rendered when no 
process matches").isEmpty();
+    }
+
+    @Test
+    void theStepsOfTheExchange() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+        writeMessageHistoryFile(TEST_PID, exchange(false));
+
+        assertThat(callWithSingleProcess(command("myApp"))).isZero();
+
+        String out = printer.getOutput();
+        assertThat(out).contains("status:success", "from[timer://tick]", 
"log[Hello]", "to[nested]",
+                "split[body]", "Success");
+        assertThat(out).doesNotContain("Stacktrace");
+    }
+
+    @Test
+    void depthHidesTheNestedSteps() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+        writeMessageHistoryFile(TEST_PID, exchange(false));
+
+        assertThat(callWithSingleProcess(command("myApp", 
"--depth=1"))).isZero();
+
+        
assertThat(printer.getOutput()).contains("log[Hello]").doesNotContain("to[nested]");
+    }
+
+    @Test
+    void depthZeroShowsTheStartAndEndOnly() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+        writeMessageHistoryFile(TEST_PID, exchange(false));
+
+        assertThat(callWithSingleProcess(command("myApp", 
"--depth=0"))).isZero();
+
+        
assertThat(printer.getOutput()).contains("from[timer://tick]").doesNotContain("log[Hello]");
+    }
+
+    @Test
+    void limitSplitShowsTheFirstSplitMessages() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+        writeMessageHistoryFile(TEST_PID, exchange(false));
+
+        assertThat(callWithSingleProcess(command("myApp", 
"--limit-split=1"))).isZero();
+
+        
assertThat(printer.getOutput()).contains("to[split0]").doesNotContain("to[split1]");
+    }
+
+    @Test
+    void sourceShowsTheLocationOfTheSteps() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+        writeMessageHistoryFile(TEST_PID, exchange(false));
+
+        assertThat(callWithSingleProcess(command("myApp", 
"--source"))).isZero();
+
+        assertThat(printer.getOutput()).contains("route.yaml:12");
+    }
+
+    @Test
+    void maskHidesTheSecretsOfTheEndpoints() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+        writeMessageHistoryFile(TEST_PID, exchange(false));
 
-        CliException e = assertThrows(CliException.class, () -> 
callWithSingleProcess(command));
+        assertThat(callWithSingleProcess(command("myApp", "--mask"))).isZero();
 
-        assertEquals(CliExitCode.NOT_FOUND, e.getExitCode());
-        assertEquals("No running Camel integration matches: doesNotExist", 
e.getMessage());
-        assertTrue(printer.getOutput().isEmpty(), "nothing should be rendered 
when no process matches");
+        // the masked password is cut in the column, as it is longer than the 
password
+        
assertThat(printer.getOutput()).contains("to[q?password=x").doesNotContain("password=b");
+    }
+
+    @Test
+    void maskHidesTheSecretsInTheJsonToo() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+        writeMessageHistoryFile(TEST_PID, exchange(false));
+
+        assertThat(callWithSingleProcess(command("myApp", "--json", 
"--mask"))).isZero();
+
+        
assertThat(printer.getOutput()).contains("password=xxxxxx").doesNotContain("password=b");
+    }
+
+    @Test
+    void theStackTraceOfAFailedExchange() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+        writeMessageHistoryFile(TEST_PID, exchange(true));
+
+        assertThat(callWithSingleProcess(command("myApp"))).isZero();
+
+        String out = printer.getOutput();
+        assertThat(out).contains("status:failed", "Exception: Forced error", 
"Stacktrace",
+                "java.lang.IllegalArgumentException: Forced error");
+    }
+
+    @Test
+    void aFailedStepWithoutItsException() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+        writeMessageHistoryFile(TEST_PID, exchange(true));
+
+        assertThat(callWithSingleProcess(command("myApp", 
"--show-exception=false"))).isZero();
+
+        assertThat(printer.getOutput()).contains("status:failed", 
"Failed").doesNotContain("Stacktrace");
+    }
+
+    @Test
+    void diagramOfSeveralIntegrationsIsAUsageError() throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+        writeStatusFile(OTHER_PID, "payments");
+        writeMessageHistoryFile(TEST_PID, exchange(false));
+        Files.writeString(CommandLineHelper.getCamelDir().resolve(OTHER_PID + 
"-history.json"), exchange(false).toJson());
+        CamelHistoryAction command = command("--diagram");
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            List<ProcessHandle> handles = List.of(mockProcessHandle(TEST_PID), 
mockProcessHandle(OTHER_PID));
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
handles.stream());
+
+            assertThatThrownBy(command::doCall)
+                    .isInstanceOfSatisfying(CliException.class,
+                            e -> 
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE))
+                    .hasMessageContaining("--diagram shows the history of one 
integration, and 2 integrations have one");
+        }
+    }
+
+    private CamelHistoryAction command(String... args) {
+        CamelHistoryAction command = new CamelHistoryAction(new 
CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(command,
+                Stream.concat(Stream.of("--logging-color=false"), 
Arrays.stream(args)).toArray(String[]::new));
+        return command;
     }
 
     private static JsonObject singleTraceLine() {
@@ -121,4 +238,80 @@ class CamelHistoryActionTest extends 
ActionCommandTestSupport {
         line.put("traces", traces);
         return line;
     }
+
+    // the history of an exchange: from, a log, a nested step, two split 
messages, an endpoint with a secret, the end
+    private static JsonObject exchange(boolean failed) {
+        List<JsonObject> traces = new ArrayList<>();
+        traces.add(flag(step(1, "from1", "from", null, 1, "timer://tick", 
null), "first"));
+        traces.add(step(2, "log1", "log", "log[Hello]", 1, null, null));
+        traces.add(step(3, "to2", "to", "to[nested]", 2, null, null));
+        traces.add(step(4, "split1", "split", "split[body]", 1, null, null));
+        traces.add(step(5, "to3", "to", "to[split0]", 2, null, 0));
+        traces.add(step(6, "to3", "to", "to[split1]", 2, null, 1));
+        traces.add(step(7, "to4", "to", "to[q?password=b]", 1, null, null));
+        if (failed) {
+            JsonObject exception = new JsonObject();
+            exception.put("type", "java.lang.IllegalArgumentException");
+            exception.put("message", "Forced error");
+            exception.put("stackTrace", "java.lang.IllegalArgumentException: 
Forced error\n\tat org.example.Route");
+            JsonObject failure = step(8, "throw1", "throwException", 
"throwException[IllegalArgumentException]", 1,
+                    null, null);
+            failure.put("failed", true);
+            failure.put("exception", exception);
+            traces.add(failure);
+            JsonObject last = flag(step(9, "from1", "from", null, 1, 
"timer://tick", null), "last");
+            last.put("failed", true);
+            last.put("exception", exception);
+            traces.add(last);
+        } else {
+            traces.add(flag(step(8, "from1", "from", null, 1, "timer://tick", 
null), "last"));
+        }
+        JsonObject line = new JsonObject();
+        line.put("name", "myApp");
+        line.put("traces", new JsonArray(traces));
+        return line;
+    }
+
+    // the first or last step of the route
+    private static JsonObject flag(JsonObject step, String flag) {
+        step.put(flag, true);
+        return step;
+    }
+
+    private static JsonObject step(
+            long uid, String nodeId, String shortName, String label, int 
level, String endpointUri, Integer splitIndex) {
+        JsonObject message = new JsonObject();
+        message.put("exchangePattern", "InOnly");
+        message.put("exchangeId", "ABCDEFGH-0001");
+        message.put("body", new JsonObject(Map.of("type", "java.lang.String", 
"value", "Hello")));
+        if (splitIndex != null) {
+            message.put("exchangeProperties", new JsonArray(
+                    List.of(
+                            new JsonObject(
+                                    Map.of("key", "CamelSplitIndex", "type", 
"java.lang.Integer", "value", splitIndex)))));
+        }
+        JsonObject trace = new JsonObject();
+        trace.put("uid", uid);
+        trace.put("first", false);
+        trace.put("last", false);
+        trace.put("location", "route.yaml:" + (10 + uid));
+        trace.put("routeId", "myRoute");
+        trace.put("fromRouteId", "myRoute");
+        trace.put("nodeId", nodeId);
+        trace.put("nodeShortName", shortName);
+        trace.put("nodeLabel", label);
+        trace.put("nodeLevel", level);
+        if (endpointUri != null) {
+            trace.put("endpointUri", endpointUri);
+            trace.put("remoteEndpoint", false);
+        }
+        trace.put("timestamp", FIXED_TIMESTAMP + uid);
+        trace.put("elapsed", uid);
+        trace.put("failed", false);
+        trace.put("done", true);
+        trace.put("threadName", "Camel (myApp) thread #1 - timer://tick");
+        trace.put("exchangeId", "ABCDEFGH-0001");
+        trace.put("message", message);
+        return trace;
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/RestartProcessTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/RestartProcessTest.java
index 7f9d8051fafd..0082dea3c7ed 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/RestartProcessTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/RestartProcessTest.java
@@ -18,14 +18,20 @@ package org.apache.camel.dsl.jbang.core.commands.process;
 
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.List;
 import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Stream;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.common.BackgroundLog;
 import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.io.TempDir;
 import org.mockito.MockedStatic;
@@ -39,8 +45,6 @@ import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-// TODO: add tests for the successful restart path once 
ProcessBuilder/Runtime.exec can be mocked
-//       or an integration-test harness is available. The current tests only 
cover early-exit guards.
 @ExtendWith(MockitoExtension.class)
 class RestartProcessTest extends ProcessCommandTestSupport {
 
@@ -109,6 +113,87 @@ class RestartProcessTest extends ProcessCommandTestSupport 
{
         }
     }
 
+    @Test
+    @DisabledOnOs(OS.WINDOWS)
+    void testRestartStopsAndRelaunchesTheIntegration() throws Exception {
+        // the integration: it writes the status of a started Camel for its 
pid, as Camel does, and runs until stopped
+        // (exec: the shell becomes the sleep, so stopping the pid stops it)
+        String script = "echo '{\"context\":{\"name\":\"myApp\",\"phase\":5}}' 
> '"
+                        + CommandLineHelper.getCamelDir().toAbsolutePath() + 
"'/$$-status.json; echo started; exec sleep 60";
+
+        int exit;
+        try {
+            exit = restart("/bin/sh", "-c", script);
+        } finally {
+            stopTheRelaunchedIntegration();
+        }
+
+        assertEquals(0, exit);
+        String out = printer.getOutput();
+        assertTrue(out.contains("Stopping Camel integration: myApp (PID: " + 
TEST_PID + ")"), out);
+        assertTrue(out.contains("Restarted Camel integration: myApp (PID: "), 
out);
+        assertTrue(out.contains("Log: "), out);
+    }
+
+    @Test
+    @DisabledOnOs(OS.WINDOWS)
+    void testRelaunchThatStopsRightAwayIsAFailure() throws Exception {
+        CliException e = assertThrows(CliException.class, () -> 
restart("/bin/sh", "-c", "echo boom; exit 3"));
+
+        assertEquals(CliExitCode.FAILURE, e.getExitCode());
+        assertTrue(e.getMessage().contains("stopped right after the restart 
(exit code 3)"), e.getMessage());
+        // the end of its log
+        assertTrue(printer.getStderr().contains("boom"), printer.getStderr());
+    }
+
+    @Test
+    void testCommandLineOfTheIntegration() {
+        assertEquals(List.of("java", "-jar", "app.jar"), 
RestartProcess.parseCommandLine("java  -jar app.jar"));
+        assertEquals(List.of("java", "-Dname=my app", "-jar", "app.jar"),
+                RestartProcess.parseCommandLine("java \"-Dname=my app\" -jar 
app.jar"));
+        assertEquals(List.of("echo", "say \"hi\""), 
RestartProcess.parseCommandLine("echo \"say \\\"hi\\\"\""));
+        // a Windows command line keeps the backslashes of its paths
+        assertEquals(List.of("C:\\Program Files\\Java\\bin\\java.exe", "-jar", 
"C:\\work\\app.jar"),
+                RestartProcess.parseCommandLine(
+                        "\"C:\\Program Files\\Java\\bin\\java.exe\" -jar 
C:\\work\\app.jar"));
+        // and a quoted path that ends with a backslash: two backslashes in 
front of the closing quote are one
+        assertEquals(List.of("\\\\server\\share\\", "-x"),
+                RestartProcess.parseCommandLine("\"\\\\server\\share\\\\\" 
-x"));
+        // three are one and a quote character
+        assertEquals(List.of("say \\\"hi"), 
RestartProcess.parseCommandLine("\"say \\\\\\\"hi\""));
+    }
+
+    // restarts myApp, which runs the given command
+    private int restart(String command, String... args) throws Exception {
+        writeStatusFile(TEST_PID, buildContextStatus("myApp", 5));
+        RestartProcess restart = new RestartProcess(new 
CamelJBangMain().withPrinter(printer));
+        restart.name = "myApp";
+        restart.timeout = 10000;
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle ph = mockProcessHandle(TEST_PID);
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+            // found, and gone once it is stopped
+            mocked.when(() -> 
ProcessHandle.of(TEST_PID)).thenReturn(Optional.of(ph), Optional.empty());
+            when(ph.info().command()).thenReturn(Optional.of(command));
+            when(ph.info().arguments()).thenReturn(Optional.of(args));
+
+            int exit = restart.doCall();
+            // there is no pid file of camel run to delete, so the integration 
is asked to stop by its process
+            verify(ph).destroy();
+            return exit;
+        }
+    }
+
+    private void stopTheRelaunchedIntegration() {
+        Matcher m = Pattern.compile("Restarted Camel integration: myApp 
\\(PID: (\\d+)\\)").matcher(printer.getOutput());
+        if (m.find()) {
+            
ProcessHandle.of(Long.parseLong(m.group(1))).ifPresent(ProcessHandle::destroyForcibly);
+        }
+    }
+
     @Test
     void testTailOfTheLog(@TempDir Path dir) throws Exception {
         Path log = dir.resolve("restart.log");

Reply via email to