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");