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 c1c397b7574f CAMEL-23688: camel-jbang - Tests for log, trace, cmd
receive, cmd send, transform message and cmd memory-leak (#27963)
c1c397b7574f is described below
commit c1c397b7574fbb6002e65f6f770226d204275b4a
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 16 20:45:15 2026 +0200
CAMEL-23688: camel-jbang - Tests for log, trace, cmd receive, cmd send,
transform message and cmd memory-leak (#27963)
Tests for the commands that show the messages and logs of an integration,
which had little coverage (cmd receive 9%, trace 17%, transform message
14%, cmd memory-leak 6%). They found these bugs, fixed here:
* camel log, camel trace --action=dump and camel cmd receive --action=dump
exited with 0 for a name that matches no running integration, when they
do not follow; now 3, as the exit codes say.
* camel cmd receive --action=dump without a name showed nothing, and
--output=json printed a batch of messages once for each message in it.
camel trace and camel cmd receive ignored an unknown --sort key; now 2.
* The colored parts of camel trace, cmd send, transform message and camel
log were printed on the console of the JVM, so a CLI that runs commands in
its JVM (camel ask, the AI panel) lost them. CamelCommand.printColored
prints them with the printer of the command when it does not print to the
console, and StringPrinter keeps the escape character that starts a
colored line.
* camel cmd send and camel transform message failed with a
NullPointerException (exit 70) when the integration failed the action
itself; they print its error and exit with 1.
* camel cmd memory-leak printed nothing and exited with 0 when the
integration failed the action or has no jfr-memory-leak console, misread
the answers to --status, and printed the duration ratio with the decimal
comma of the locale.
* camel trace --tail showed the end of an exchange as returning from its
route.
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 18 ++
.../dsl/jbang/core/commands/CamelCommand.java | 29 +++
.../jbang/core/commands/action/CamelLogAction.java | 17 +-
.../core/commands/action/CamelMemoryLeak.java | 127 +++++-----
.../core/commands/action/CamelReceiveAction.java | 52 ++--
.../core/commands/action/CamelSendAction.java | 20 +-
.../core/commands/action/CamelTraceAction.java | 51 ++--
.../commands/action/TransformMessageAction.java | 23 +-
.../camel/dsl/jbang/core/common/StringPrinter.java | 9 +-
.../commands/action/ActionCommandTestSupport.java | 22 +-
.../core/commands/action/CamelLogActionTest.java | 29 +++
.../core/commands/action/CamelMemoryLeakTest.java | 179 +++++++++++++-
.../commands/action/CamelReceiveActionTest.java | 265 +++++++++++++++++++--
.../commands/action/CamelSendActionResultTest.java | 24 ++
.../core/commands/action/CamelTraceActionTest.java | 154 +++++++++++-
.../action/TransformMessageActionTest.java | 172 +++++++++++++
16 files changed, 1008 insertions(+), 183 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 c51838b3a63e..0cd7ed47f3a3 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
@@ -538,6 +538,24 @@ integration share, are deprecated. A plugin command sends
an action with `Action
a file of its own, so that two clients cannot overwrite each other's action or
answer, and uses the shared files only
for an integration before Camel 4.21. `camel debug` sends its commands this
way too.
+=== camel-jbang - camel log, trace, cmd receive, cmd send, transform message
and cmd memory-leak
+
+* `camel log`, `camel trace --action=dump` and `camel cmd receive
--action=dump` exit with `3` when the name or pid
+ matches no running integration, and they do not follow (in a script or an AI
agent, or with `--follow=false`): they
+ printed nothing and exited with `0`.
+* `camel cmd receive --action=dump` without a name shows the messages of the
running integrations (it showed nothing).
+ With `--output=json` it prints each received message once, as one JSON
document per line with its `exchangeId`: it
+ printed the batch of messages a line of the received file holds once for
each message in it. `--since`, `--grep` and
+ `--mask` apply to the JSON too.
+* `camel trace` and `camel cmd receive` exit with `2` for an unknown `--sort`
key, as the other listings do.
+* `camel trace --tail` shows the end of the last exchange as completed, not as
returning from its route.
+* `camel cmd send` and `camel transform message` print the error and exit with
`1` when the integration fails the action
+ itself (they failed with an internal error, `70`).
+* `camel cmd memory-leak` exits with `1` when the integration fails the
action, such as when it cannot record, or has no
+ `jfr-memory-leak` dev console: it printed nothing and exited with `0`.
`--status` shows the progress of a recording
+ (it said the recording started), and after a recording that its results can
be queried (it said that no samples were
+ captured).
+
=== camel-jpa, camel-hazelcast - an exchange marked rollback only is not
consumed
When a route marks the exchange rollback only (for example with
`markRollbackOnly()` or `markRollbackOnlyLast()`)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
index ce97bafee5ef..0df7012e796c 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
@@ -35,6 +35,7 @@ import org.apache.camel.dsl.jbang.core.common.Printer;
import org.apache.camel.dsl.jbang.core.common.RuntimeUtil;
import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
import org.apache.camel.util.StringHelper;
+import org.jline.jansi.AnsiConsole;
import picocli.CommandLine;
import picocli.CommandLine.IParameterConsumer;
import picocli.CommandLine.Model.ArgSpec;
@@ -241,6 +242,34 @@ public abstract class CamelCommand implements
Callable<Integer> {
return out;
}
+ /**
+ * Prints text with colors (ANSI escape codes): on the console when the
command prints to the console, which leaves
+ * the colors out when the output is not a terminal; else with the printer
of the command, as in the shell, or in a
+ * CLI that runs the command in its own JVM and keeps its output (see
{@link CliRunner}).
+ */
+ protected void printColored(Object text) {
+ if (printsToConsole()) {
+ AnsiConsole.out().print(text);
+ } else {
+ printer().print(String.valueOf(text));
+ }
+ }
+
+ /**
+ * Prints a line with colors (ANSI escape codes), see {@link
#printColored(Object)}.
+ */
+ protected void printlnColored(Object text) {
+ if (printsToConsole()) {
+ AnsiConsole.out().println(text);
+ } else {
+ printer().println(String.valueOf(text));
+ }
+ }
+
+ private boolean printsToConsole() {
+ return printer() instanceof Printer.SystemOutPrinter &&
!EnvironmentHelper.isEmbedded();
+ }
+
protected int terminalWidth() {
return TerminalWidthHelper.getTerminalWidth();
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogAction.java
index c93497d259fb..50f900d2e97f 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogAction.java
@@ -47,7 +47,6 @@ import org.apache.camel.util.StringHelper;
import org.apache.camel.util.TimeUtils;
import org.apache.camel.util.json.JsonObject;
import org.jline.jansi.Ansi;
-import org.jline.jansi.AnsiConsole;
import picocli.CommandLine;
@CommandLine.Command(name = "log",
@@ -156,6 +155,10 @@ public class CamelLogAction extends ActionBaseCommand {
// find new pids
updatePids(rows);
+ if (rows.isEmpty() && !follow) {
+ // shown once: a name that matches no running integration is not
found
+ findProcesses(name);
+ }
if (!rows.isEmpty()) {
// read existing log files (skip by tail/since)
Date limit = null;
@@ -400,11 +403,7 @@ public class CamelLogAction extends ActionBaseCommand {
}
String n = String.format("%-" + nameMaxWidth + "s", name);
String prefix = Ansi.ansi().fg(color).a(n).a("|
").reset().toString();
- if (EnvironmentHelper.isEmbedded()) {
- printer().print(prefix);
- } else {
- AnsiConsole.out().print(prefix);
- }
+ printColored(prefix);
}
} else {
line = unescapeAnsi(line);
@@ -435,11 +434,7 @@ public class CamelLogAction extends ActionBaseCommand {
line = before != null ? before + "---" + after : after;
}
if (loggingColor) {
- if (EnvironmentHelper.isEmbedded()) {
- printer().println(line);
- } else {
- AnsiConsole.out().println(line);
- }
+ printlnColored(line);
} else {
printer().println(line);
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelMemoryLeak.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelMemoryLeak.java
index f208643e0881..4f5ffc2b7d7c 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelMemoryLeak.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelMemoryLeak.java
@@ -28,6 +28,7 @@ import com.github.freva.asciitable.HorizontalAlign;
import com.github.freva.asciitable.OverflowBehaviour;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.dsl.jbang.core.common.ActionClient;
+import org.apache.camel.dsl.jbang.core.common.ActionClient.ActionResult;
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.TerminalWidthHelper;
@@ -116,13 +117,17 @@ public class CamelMemoryLeak extends ActionBaseCommand {
command = "status";
}
- JsonObject jo = jfrAction(pid, command, start ? duration : 0);
- if (jo == null) {
+ ActionResult result = jfrAction(pid, command, start ? duration : 0);
+ if (result.reply() == null) {
printer().printErr("Response from running Camel with PID " + pid +
" not received within timeout");
return CliExitCode.TIMEOUT;
+ } else if (!result.isOk()) {
+ // the integration failed the action, such as when it cannot
record (an error, or an exception)
+ printer().printErr(result.error());
+ return result.exitCode();
}
- return handleResponse(pid, jo);
+ return handleResponse(pid, result.reply());
}
private int doDualRecording(long pid) throws Exception {
@@ -131,64 +136,65 @@ public class CamelMemoryLeak extends ActionBaseCommand {
// run 1
printer().printf("Recording 1 of 2 (%ds)...%n", dur1);
- JsonObject startResp = jfrAction(pid, "start", dur1);
- if (startResp == null) {
- return noReply(pid, "Failed to start recording 1");
- } else if (!"recording".equals(startResp.getString("status"))) {
- printer().printErr("Failed to start recording 1");
- return CliExitCode.FAILURE;
+ int exit = step(pid, jfrAction(pid, "start", dur1), "recording",
"Failed to start recording 1");
+ if (exit != 0) {
+ return exit;
}
waitForRecordingComplete(pid, dur1);
- JsonObject stopResp = jfrAction(pid, "stop", 0);
- if (stopResp == null) {
- return noReply(pid, "Recording 1 did not complete");
- } else if (!"completed".equals(stopResp.getString("status"))) {
- printer().printErr("Recording 1 did not complete");
- return CliExitCode.FAILURE;
+ exit = step(pid, jfrAction(pid, "stop", 0), "completed", "Recording 1
did not complete");
+ if (exit != 0) {
+ return exit;
}
// run 2
printer().printf("Recording 2 of 2 (%ds)...%n", dur2);
- startResp = jfrAction(pid, "start", dur2);
- if (startResp == null) {
- return noReply(pid, "Failed to start recording 2");
- } else if (!"recording".equals(startResp.getString("status"))) {
- printer().printErr("Failed to start recording 2");
- return CliExitCode.FAILURE;
+ exit = step(pid, jfrAction(pid, "start", dur2), "recording", "Failed
to start recording 2");
+ if (exit != 0) {
+ return exit;
}
waitForRecordingComplete(pid, dur2);
- stopResp = jfrAction(pid, "stop", 0);
- if (stopResp == null) {
- return noReply(pid, "Recording 2 did not complete");
- } else if (!"completed".equals(stopResp.getString("status"))) {
- printer().printErr("Recording 2 did not complete");
- return CliExitCode.FAILURE;
+ exit = step(pid, jfrAction(pid, "stop", 0), "completed", "Recording 2
did not complete");
+ if (exit != 0) {
+ return exit;
}
// compare
- JsonObject cmpResp = jfrAction(pid, "compare", 0);
- if (cmpResp == null) {
- return noReply(pid, "Failed to get comparison data");
+ ActionResult compared = jfrAction(pid, "compare", 0);
+ exit = step(pid, compared, null, "Failed to get comparison data");
+ if (exit != 0) {
+ return exit;
}
- return handleResponse(pid, cmpResp);
+ return handleResponse(pid, compared.reply());
}
- // the integration did not reply to a step in time
- private int noReply(long pid, String message) {
- printer().printErr(message + ": no response from running Camel with
PID " + pid + " within timeout");
- return CliExitCode.TIMEOUT;
+ /**
+ * How a step of the dual recording went: 0 when the integration answered
with the expected status (any status when
+ * <tt>null</tt>), else the exit code, and the error is printed.
+ */
+ private int step(long pid, ActionResult result, String expectedStatus,
String message) {
+ if (result.reply() == null) {
+ printer().printErr(message + ": no response from running Camel
with PID " + pid + " within timeout");
+ return CliExitCode.TIMEOUT;
+ } else if (!result.isOk()) {
+ printer().printErr(message + ": " + result.error());
+ return result.exitCode();
+ } else if (expectedStatus != null &&
!expectedStatus.equals(result.reply().getString("status"))) {
+ printer().printErr(message);
+ return CliExitCode.FAILURE;
+ }
+ return 0;
}
private void waitForRecordingComplete(long pid, int dur) throws Exception {
long deadline = System.currentTimeMillis() + (dur + 30) * 1000L;
while (System.currentTimeMillis() < deadline) {
Thread.sleep(3000);
- JsonObject status = jfrAction(pid, "status", 0);
+ JsonObject status = jfrAction(pid, "status", 0).reply();
if (status != null &&
!"recording".equals(status.getString("status"))) {
break;
}
@@ -196,10 +202,10 @@ public class CamelMemoryLeak extends ActionBaseCommand {
}
/**
- * Sends a command of the JFR memory leak action, and returns the answer,
or <tt>null</tt> when there is none in
- * time.
+ * Sends a command of the JFR memory leak action, and returns the answer
(without a reply when there is none in
+ * time).
*/
- private JsonObject jfrAction(long pid, String command, int dur) throws
IOException {
+ private ActionResult jfrAction(long pid, String command, int dur) throws
IOException {
JsonObject root = new JsonObject();
root.put("action", "jfr-memory-leak");
root.put("command", command);
@@ -215,15 +221,20 @@ public class CamelMemoryLeak extends ActionBaseCommand {
}
int timeout = "start".equals(command) ? 15000 : 30000;
- return ActionClient.execute(pid, root, timeout).reply();
+ return ActionClient.execute(pid, root, timeout);
}
private int handleResponse(long pid, JsonObject jo) {
String responseStatus = jo.getString("status");
- if ("error".equals(responseStatus)) {
- printer().printErr(jo.getString("error"));
- return CliExitCode.FAILURE;
+ if ("recording".equals(responseStatus) && jo.containsKey("elapsedMs"))
{
+ // the status of the recording
+ printer().println("JFR recording in progress for PID: " + pid);
+ printer().println("Elapsed: " +
formatDuration(jo.getLongOrDefault("elapsedMs", 0)));
+ if (jo.containsKey("remainingMs")) {
+ printer().println("Remaining: " +
formatDuration(jo.getLongOrDefault("remainingMs", 0)));
+ }
+ return 0;
}
if ("recording".equals(responseStatus)) {
@@ -244,6 +255,13 @@ public class CamelMemoryLeak extends ActionBaseCommand {
return 0;
}
+ if ("completed".equals(responseStatus) &&
jo.containsKey("hasCachedResults")) {
+ // the status after the recording
+ printer().println("JFR recording completed. Use --query to view
results.");
+ printer().println("Cached samples: " +
jo.getIntegerOrDefault("sampleCount", 0));
+ return 0;
+ }
+
if ("completed".equals(responseStatus)) {
int sampleCount = jo.getIntegerOrDefault("sampleCount", 0);
long durationMs = jo.getLongOrDefault("recordingDurationMs", 0);
@@ -285,8 +303,9 @@ public class CamelMemoryLeak extends ActionBaseCommand {
long baseDur = baselineInfo != null ?
baselineInfo.getLongOrDefault("recordingDurationMs", 0) : 0;
long curDur = currentInfo != null ?
currentInfo.getLongOrDefault("recordingDurationMs", 0) : 0;
double durRatio = jo.getDoubleOrDefault("durationRatio", 1.0);
- printer().printf("PID: %s\tRun 1: %s\tRun 2: %s\tDuration ratio:
%.1fx%n",
- pid, formatDuration(baseDur), formatDuration(curDur),
durRatio);
+ // the same decimal point in every locale
+ printer().println(String.format(Locale.US, "PID: %s\tRun 1:
%s\tRun 2: %s\tDuration ratio: %.1fx",
+ pid, formatDuration(baseDur), formatDuration(curDur),
durRatio));
JsonArray comps = (JsonArray) jo.get("comparisons");
if (comps != null && !comps.isEmpty()) {
@@ -321,20 +340,10 @@ public class CamelMemoryLeak extends ActionBaseCommand {
return 0;
}
- // status response with cached results info
- if (jo.containsKey("hasCachedResults") &&
jo.getBooleanOrDefault("hasCachedResults", false)) {
- printer().println("JFR recording completed. Use --query to view
results.");
- printer().println("Cached samples: " +
jo.getIntegerOrDefault("sampleCount", 0));
- } else if (jo.containsKey("elapsedMs")) {
- long elapsed = jo.getLongOrDefault("elapsedMs", 0);
- printer().println("JFR recording in progress for PID: " + pid);
- printer().println("Elapsed: " + formatDuration(elapsed));
- if (jo.containsKey("remainingMs")) {
- printer().println("Remaining: " +
formatDuration(jo.getLongOrDefault("remainingMs", 0)));
- }
- }
-
- return 0;
+ // an integration without the jfr-memory-leak dev console answers
without a status
+ printer().printErr("Running Camel with PID " + pid
+ + " cannot record memory leaks, as it has no
jfr-memory-leak dev console (camel-console)");
+ return CliExitCode.FAILURE;
}
private void printTable(List<Row> rows) {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReceiveAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReceiveAction.java
index 425a25b4f02b..352b0a4f036c 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReceiveAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReceiveAction.java
@@ -57,6 +57,7 @@ import
org.apache.camel.dsl.jbang.core.common.PidNameAgeCompletionCandidates;
import org.apache.camel.dsl.jbang.core.common.ProcessFinder;
import org.apache.camel.dsl.jbang.core.common.ProcessFinder.CamelProcess;
import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
+import org.apache.camel.dsl.jbang.core.common.RowSort;
import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
import org.apache.camel.dsl.jbang.core.common.TimeoutConverter;
import org.apache.camel.main.KameletMain;
@@ -84,6 +85,8 @@ public class CamelReceiveAction extends ActionBaseCommand {
private static final int NAME_MAX_WIDTH = 25;
private static final int NAME_MIN_WIDTH = 10;
+ private static final RowSort<StatusRow> ORDER = RowSort.<StatusRow>
integration(r -> r.pid, r -> r.name, r -> r.uptime);
+
public static class PrefixCompletionCandidates implements Iterable<String>
{
public PrefixCompletionCandidates() {
@@ -461,8 +464,7 @@ public class CamelReceiveAction extends ActionBaseCommand {
}
});
- // sort rows
- rows.sort(this::sortStatusRow);
+ rows.sort(ORDER.comparator(sort));
if (!rows.isEmpty()) {
int tw = terminalWidth();
@@ -497,25 +499,6 @@ public class CamelReceiveAction extends ActionBaseCommand {
return "Disabled";
}
- protected int sortStatusRow(StatusRow o1, StatusRow o2) {
- String s = sort;
- int negate = 1;
- if (s.startsWith("-")) {
- s = s.substring(1);
- negate = -1;
- }
- switch (s) {
- case "pid":
- return Long.compare(Long.parseLong(o1.pid),
Long.parseLong(o2.pid)) * negate;
- case "name":
- return o1.name.compareToIgnoreCase(o2.name) * negate;
- case "age":
- return Long.compare(o1.uptime, o2.uptime) * negate;
- default:
- return 0;
- }
- }
-
protected Integer doDumpCall() throws Exception {
follow = isFollowing(follow, durationOption.getDuration());
// setup table helper
@@ -537,6 +520,10 @@ public class CamelReceiveAction extends ActionBaseCommand {
// find new pids
updatePids(pids);
+ if (pids.isEmpty() && !follow) {
+ // shown once: a name that matches no running integration is not
found
+ findProcesses(name);
+ }
if (!pids.isEmpty()) {
// read existing received files (skip by tail/since)
Date limit = null;
@@ -630,8 +617,11 @@ public class CamelReceiveAction extends ActionBaseCommand {
List<Long> pids;
if (name != null) {
pids = findPids(name);
- } else {
+ } else if (pid != 0) {
+ // the integration this command started to receive in
pids = List.of(pid);
+ } else {
+ pids = findPids("*");
}
ProcessHandle.allProcesses()
.filter(ph -> pids.contains(ph.pid()))
@@ -728,16 +718,17 @@ public class CamelReceiveAction extends ActionBaseCommand
{
if (arr != null) {
for (Object o : arr) {
Row row = new Row(pid);
- row.original = root;
row.pid = pid.pid;
row.name = pid.name;
JsonObject jo = (JsonObject) o;
+ row.json = jo;
row.uid = jo.getLong("uid");
String uri = jo.getString("endpointUri");
if (uri != null) {
row.endpoint = new JsonObject();
if (mask) {
uri = URISupport.sanitizeUri(uri);
+ jo.put("endpointUri", uri);
}
row.endpoint.put("endpoint", uri);
row.endpoint.put("remote",
jo.getBooleanOrDefault("remoteEndpoint", true));
@@ -751,8 +742,11 @@ public class CamelReceiveAction extends ActionBaseCommand {
row.timestamp = ts;
}
row.message = jo.getMap("message");
- row.message.remove("exchangeId");
- row.message.remove("exchangePattern");
+ if (!"json".equalsIgnoreCase(output)) {
+ // the table of the message does not show them
+ row.message.remove("exchangeId");
+ row.message.remove("exchangePattern");
+ }
if (onlyBody) {
row.message.remove("exchangeProperties");
row.message.remove("exchangeVariables");
@@ -858,7 +852,11 @@ public class CamelReceiveAction extends ActionBaseCommand {
protected void printDump(String name, int pids, Row row, Date limit) {
if ("json".equalsIgnoreCase(output)) {
- String dump = row.original.toJson();
+ // a message per line (a line of the file holds the messages the
integration received since the line before)
+ String dump = row.json.toJson();
+ if (!isValidSince(limit, row.timestamp) || !isValidGrep(dump)) {
+ return;
+ }
if (pretty) {
dump = valueAsStringPretty(dump, loggingColor);
}
@@ -960,7 +958,7 @@ public class CamelReceiveAction extends ActionBaseCommand {
}
private static class Row {
- JsonObject original;
+ JsonObject json;
Pid parent;
String pid;
String name;
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
index ee39a8323030..02aa6575d04e 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
@@ -53,7 +53,6 @@ import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
import org.apache.camel.util.json.Jsoner;
import org.jline.jansi.Ansi;
-import org.jline.jansi.AnsiConsole;
import picocli.CommandLine;
@CommandLine.Command(name = "send",
@@ -232,6 +231,10 @@ public class CamelSendAction extends ActionBaseCommand {
String exchangeId = jo.getString("exchangeId");
JsonObject message = jo.getMap("message");
JsonObject cause = jo.getMap("exception");
+ if (!result.isOk() && message == null && cause == null) {
+ // the integration failed the action itself, so there is no
exchange to show
+ printer().printErr(result.error());
+ }
if (message != null || cause != null) {
if (replyFile != null) {
Path target = Path.of(replyFile);
@@ -342,7 +345,7 @@ public class CamelSendAction extends ActionBaseCommand {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
String ts = sdf.format(new Date(jo.getLongOrDefault("timestamp",
System.currentTimeMillis())));
if (loggingColor) {
-
AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(ts).reset());
+
printColored(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(ts).reset());
} else {
printer().print(ts);
}
@@ -350,8 +353,8 @@ public class CamelSendAction extends ActionBaseCommand {
printer().print(" ");
String p = String.format("%5.5s", this.pid);
if (loggingColor) {
- AnsiConsole.out().print(Ansi.ansi().fgMagenta().a(p).reset());
-
AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a("
--- ").reset());
+ printColored(Ansi.ansi().fgMagenta().a(p).reset());
+
printColored(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a("
--- ").reset());
} else {
printer().print(p);
printer().print(" --- ");
@@ -364,7 +367,7 @@ public class CamelSendAction extends ActionBaseCommand {
}
ids = String.format("%40.40s", ids);
if (loggingColor) {
- AnsiConsole.out().print(Ansi.ansi().fgCyan().a(ids).reset());
+ printColored(Ansi.ansi().fgCyan().a(ids).reset());
} else {
printer().print(ids);
}
@@ -373,11 +376,12 @@ public class CamelSendAction extends ActionBaseCommand {
// status
printer().print(getStatus(jo));
// elapsed
- String e = TimeUtils.printDuration(jo.getLong("elapsed"), true);
+ // an action the integration failed has no elapsed time
+ String e = TimeUtils.printDuration(jo.getLongOrDefault("elapsed", 0),
true);
if (loggingColor) {
- AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(" (" + e +
")").reset());
+ printColored(Ansi.ansi().fgBrightDefault().a(" (" + e +
")").reset());
} else {
- printer().print("(" + e + ")");
+ printer().print(" (" + e + ")");
}
printer().println();
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceAction.java
index 200c02a14c81..b7d8de355238 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceAction.java
@@ -48,6 +48,7 @@ import org.apache.camel.dsl.jbang.core.common.JsonOutput;
import org.apache.camel.dsl.jbang.core.common.PidNameAgeCompletionCandidates;
import org.apache.camel.dsl.jbang.core.common.ProcessFinder.CamelProcess;
import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
+import org.apache.camel.dsl.jbang.core.common.RowSort;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.StopWatch;
import org.apache.camel.util.StringHelper;
@@ -57,7 +58,6 @@ import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
import org.apache.camel.util.json.Jsoner;
import org.jline.jansi.Ansi;
-import org.jline.jansi.AnsiConsole;
import picocli.CommandLine;
@CommandLine.Command(name = "trace",
@@ -73,6 +73,8 @@ public class CamelTraceAction extends ActionBaseCommand {
private static final int NAME_MIN_WIDTH = 10;
private static final String TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";
+ private static final RowSort<StatusRow> ORDER = RowSort.<StatusRow>
integration(r -> r.pid, r -> r.name, r -> r.uptime);
+
public static class PrefixCompletionCandidates implements Iterable<String>
{
public PrefixCompletionCandidates() {
@@ -258,8 +260,7 @@ public class CamelTraceAction extends ActionBaseCommand {
rows.add(row);
}
- // sort rows
- rows.sort(this::sortStatusRow);
+ rows.sort(ORDER.comparator(sort));
if (jsonOutput) {
JsonOutput.printList(printer(), rows.stream().map(r -> {
@@ -299,25 +300,6 @@ public class CamelTraceAction extends ActionBaseCommand {
return "Stopped";
}
- protected int sortStatusRow(StatusRow o1, StatusRow o2) {
- String s = sort;
- int negate = 1;
- if (s.startsWith("-")) {
- s = s.substring(1);
- negate = -1;
- }
- switch (s) {
- case "pid":
- return Long.compare(Long.parseLong(o1.pid),
Long.parseLong(o2.pid)) * negate;
- case "name":
- return o1.name.compareToIgnoreCase(o2.name) * negate;
- case "age":
- return Long.compare(o1.uptime, o2.uptime) * negate;
- default:
- return 0;
- }
- }
-
protected Integer doDumpCall() throws Exception {
follow = isFollowing(follow, durationOption.getDuration());
// setup table helper
@@ -360,6 +342,10 @@ public class CamelTraceAction extends ActionBaseCommand {
// find new pids
updatePids(pids);
+ if (pids.isEmpty() && !follow) {
+ // shown once: a name that matches no running integration is not
found
+ findProcesses(name);
+ }
if (!pids.isEmpty()) {
// read existing trace files (skip by tail/since)
Date limit = null;
@@ -769,7 +755,7 @@ public class CamelTraceAction extends ActionBaseCommand {
ts = sdf.format(new Date(row.timestamp));
}
if (loggingColor) {
-
AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(ts).reset());
+
printColored(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(ts).reset());
} else {
printer().print(ts);
}
@@ -778,8 +764,8 @@ public class CamelTraceAction extends ActionBaseCommand {
// pid
String p = String.format("%5.5s", row.pid);
if (loggingColor) {
- AnsiConsole.out().print(Ansi.ansi().fgMagenta().a(p).reset());
-
AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a("
--- ").reset());
+ printColored(Ansi.ansi().fgMagenta().a(p).reset());
+
printColored(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a("
--- ").reset());
} else {
printer().print(p);
printer().print(" --- ");
@@ -791,7 +777,7 @@ public class CamelTraceAction extends ActionBaseCommand {
}
tn = String.format("[%25.25s]", tn);
if (loggingColor) {
-
AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(tn).reset());
+
printColored(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(tn).reset());
} else {
printer().print(tn);
}
@@ -808,7 +794,7 @@ public class CamelTraceAction extends ActionBaseCommand {
}
ids = String.format("%40.40s", ids);
if (loggingColor) {
- AnsiConsole.out().print(Ansi.ansi().fgCyan().a(ids).reset());
+ printColored(Ansi.ansi().fgCyan().a(ids).reset());
} else {
printer().print(ids);
}
@@ -816,7 +802,7 @@ public class CamelTraceAction extends ActionBaseCommand {
// uuid
String u = String.format("%5.5s", row.uid);
if (loggingColor) {
- AnsiConsole.out().print(Ansi.ansi().fgMagenta().a(u).reset());
+ printColored(Ansi.ansi().fgMagenta().a(u).reset());
} else {
printer().print(u);
}
@@ -827,9 +813,9 @@ public class CamelTraceAction extends ActionBaseCommand {
String e = getElapsed(row);
if (e != null) {
if (loggingColor) {
- AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(" (" +
e + ")").reset());
+ printColored(Ansi.ansi().fgBrightDefault().a(" (" + e +
")").reset());
} else {
- printer().print("(" + e + ")");
+ printer().print(" (" + e + ")");
}
}
// trace message
@@ -874,7 +860,7 @@ public class CamelTraceAction extends ActionBaseCommand {
}
if (depth == 0) {
// special with only created/completed
- return row.parent.depth == 1 && row.first || row.parent.depth == 0
&& row.last;
+ return row.parent.depth == 1 && row.first || row.parent.depth <= 0
&& row.last;
}
return row.parent.depth <= depth;
}
@@ -904,7 +890,8 @@ public class CamelTraceAction extends ActionBaseCommand {
}
} else if (r.last) {
String done = r.exception != null ? "Completed (exception)" :
"Completed (success)";
- String s = r.parent.depth == 0 ? done : "Returning from " +
r.routeId;
+ // below 0 when --tail cut the start of the exchange
+ String s = r.parent.depth <= 0 ? done : "Returning from " +
r.routeId;
if (loggingColor) {
return Ansi.ansi().fg(r.failed ? Ansi.Color.RED :
Ansi.Color.GREEN).a(s).reset().toString();
} else {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java
index bee1c4036ddd..8deb440625a9 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java
@@ -39,7 +39,6 @@ import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
import org.apache.camel.util.json.Jsoner;
import org.jline.jansi.Ansi;
-import org.jline.jansi.AnsiConsole;
import picocli.CommandLine;
@CommandLine.Command(name = "message",
@@ -127,7 +126,8 @@ public class TransformMessageAction extends
ActionWatchCommand {
@CommandLine.Mixin
MavenResolverMixin mavenResolver;
- private volatile long pid;
+ // the Camel this command started
+ volatile long pid;
private MessageTableHelper tableHelper;
@@ -251,6 +251,10 @@ public class TransformMessageAction extends
ActionWatchCommand {
String exchangeId = jo.getString("exchangeId");
JsonObject message = jo.getMap("message");
JsonObject cause = jo.getMap("exception");
+ if (!result.isOk() && message == null && cause == null) {
+ // the integration failed the action itself, so there is no
message to show
+ printer().printErr(result.error());
+ }
if (message != null || cause != null) {
if (output != null) {
Path target = Path.of(output);
@@ -292,9 +296,10 @@ public class TransformMessageAction extends
ActionWatchCommand {
private void printStatusLine(JsonObject jo) {
// timstamp
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
- String ts = sdf.format(new Date(jo.getLong("timestamp")));
+ // an action the integration failed has no timestamp nor elapsed time
+ String ts = sdf.format(new Date(jo.getLongOrDefault("timestamp",
System.currentTimeMillis())));
if (loggingColor) {
-
AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(ts).reset());
+
printColored(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(ts).reset());
} else {
printer().print(ts);
}
@@ -302,8 +307,8 @@ public class TransformMessageAction extends
ActionWatchCommand {
printer().print(" ");
String p = String.format("%5.5s", this.pid);
if (loggingColor) {
- AnsiConsole.out().print(Ansi.ansi().fgMagenta().a(p).reset());
-
AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a("
--- ").reset());
+ printColored(Ansi.ansi().fgMagenta().a(p).reset());
+
printColored(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a("
--- ").reset());
} else {
printer().print(p);
printer().print(" --- ");
@@ -311,11 +316,11 @@ public class TransformMessageAction extends
ActionWatchCommand {
// status
printer().print(getStatus(jo));
// elapsed
- String e = TimeUtils.printDuration(jo.getLong("elapsed"), true);
+ String e = TimeUtils.printDuration(jo.getLongOrDefault("elapsed", 0),
true);
if (loggingColor) {
- AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(" (" + e +
")").reset());
+ printColored(Ansi.ansi().fgBrightDefault().a(" (" + e +
")").reset());
} else {
- printer().print("(" + e + ")");
+ printer().print(" (" + e + ")");
}
printer().println();
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/StringPrinter.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/StringPrinter.java
index 355b812c00d8..55bd9a1f1c25 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/StringPrinter.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/StringPrinter.java
@@ -71,21 +71,22 @@ public class StringPrinter implements Printer {
* @return
*/
public String getOutput() {
- return writer.toString().trim();
+ // strip and not trim, which also removes the escape character that
starts a colored line
+ return writer.toString().strip();
}
/**
* The output of the command only (what the CLI prints on stdout).
*/
public String getStdout() {
- return stdout.toString().trim();
+ return stdout.toString().strip();
}
/**
* The errors, warnings and tips only (what the CLI prints on stderr).
*/
public String getStderr() {
- return stderr.toString().trim();
+ return stderr.toString().strip();
}
/**
@@ -99,7 +100,7 @@ public class StringPrinter implements Printer {
List<String> lines = new ArrayList<>();
String line;
while ((line = buf.readLine()) != null) {
- lines.add(line.trim());
+ lines.add(line.strip());
}
return lines;
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/ActionCommandTestSupport.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/ActionCommandTestSupport.java
index 31a2a51ee07b..6a3540e83ca7 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/ActionCommandTestSupport.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/ActionCommandTestSupport.java
@@ -24,6 +24,7 @@ import java.time.Instant;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
+import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
@@ -41,7 +42,6 @@ import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
-import static org.mockito.Mockito.when;
/**
* Base class for action command tests. Handles file system setup, status file
writing and ProcessHandle mock creation
@@ -140,7 +140,19 @@ abstract class ActionCommandTestSupport extends
CamelCommandBaseTestSupport {
return call(command, response);
}
+ /**
+ * Runs a step of a command, such as the one that sends the action of a
command that first starts an integration of
+ * its own, against the fake integration (TEST_PID) that answers with
{@code response}.
+ */
+ protected int callWithResponse(Callable<Integer> step, JsonObject
response) throws Exception {
+ return call(step, response);
+ }
+
private static int call(CamelCommand command, JsonObject response) throws
Exception {
+ return call(command::doCall, response);
+ }
+
+ private static int call(Callable<Integer> step, JsonObject response)
throws Exception {
lastAction = null;
AtomicBoolean running = new AtomicBoolean(true);
Thread responder = new Thread(() -> {
@@ -162,7 +174,7 @@ abstract class ActionCommandTestSupport extends
CamelCommandBaseTestSupport {
ProcessHandle current = mockCurrentHandle();
mocked.when(ProcessHandle::current).thenReturn(current);
mocked.when(ProcessHandle::allProcesses).thenAnswer(inv ->
Stream.of(ph));
- return command.doCall();
+ return step.call();
} finally {
running.set(false);
responder.join(TimeUnit.SECONDS.toMillis(10));
@@ -204,7 +216,9 @@ abstract class ActionCommandTestSupport extends
CamelCommandBaseTestSupport {
protected static ProcessHandle mockProcessHandle(long pid) {
ProcessHandle ph = mock(ProcessHandle.class);
ProcessHandle.Info info = mock(ProcessHandle.Info.class);
- when(ph.pid()).thenReturn(pid);
+ // lenient: a step of a command that knows the pid (see
callWithResponse(Callable, JsonObject)) does not look
+ // for the process
+ lenient().when(ph.pid()).thenReturn(pid);
// lenient: info()/commandLine()/startInstant() are only read when a
status file is matched; a test that
// exercises the "no status file for this pid" path never reaches that
code, leaving these unused
lenient().when(ph.info()).thenReturn(info);
@@ -219,7 +233,7 @@ abstract class ActionCommandTestSupport extends
CamelCommandBaseTestSupport {
*/
protected static ProcessHandle mockCurrentHandle() {
ProcessHandle current = mock(ProcessHandle.class);
- when(current.pid()).thenReturn(CURRENT_PID);
+ lenient().when(current.pid()).thenReturn(CURRENT_PID);
return current;
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogActionTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogActionTest.java
index 205e4b64330a..1380faaf597c 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogActionTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogActionTest.java
@@ -23,6 +23,8 @@ import java.util.List;
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.dsl.jbang.core.common.StringPrinter;
import org.junit.jupiter.api.Test;
@@ -31,6 +33,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import picocli.CommandLine;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* camel log: the lines of the log of an integration, without following it.
@@ -137,6 +140,32 @@ class CamelLogActionTest extends ActionCommandTestSupport {
assertThat(lines()).hasSize(1);
}
+ @Test
+ void nameThatMatchesNoIntegrationIsNotFound() throws Exception {
+ writeStatusFile(TEST_PID, "orders");
+ CamelLogAction command = new CamelLogAction(new
CamelJBangMain().withPrinter(printer));
+ CommandLine.populateCommand(command, "payments", "--follow=false");
+
+ assertThatThrownBy(() -> callWithSingleProcess(command))
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.NOT_FOUND))
+ .hasMessage("No running Camel integration matches: payments");
+ }
+
+ @Test
+ void coloredLinesAreKeptByThePrinterOfTheCommand() throws Exception {
+ writeStatusFile(TEST_PID, "orders");
+ writeLog(TEST_PID + ".log", line(-300, "Starting"), line(-1,
"Failed"));
+ CamelLogAction command = new CamelLogAction(new
CamelJBangMain().withPrinter(printer));
+ CommandLine.populateCommand(command, "orders", "--follow=false",
"--prefix=true");
+
+ assertThat(callWithSingleProcess(command)).isZero();
+
+ // a CLI that runs the command in its JVM keeps the colored lines (not
printed on the console)
+ assertThat(lines()).hasSize(2);
+ assertThat(lines().get(1)).startsWith("orders").endsWith(": Failed");
+ }
+
private CamelLogAction command(String... args) throws Exception {
writeStatusFile(TEST_PID, "orders");
CamelLogAction command = new CamelLogAction(new
CamelJBangMain().withPrinter(printer));
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelMemoryLeakTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelMemoryLeakTest.java
index 4ab719b96614..c368da2ea329 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelMemoryLeakTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelMemoryLeakTest.java
@@ -16,17 +16,157 @@
*/
package org.apache.camel.dsl.jbang.core.commands.action;
+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.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.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
- * camel cmd memory-leak --min-size: the size of the samples to show.
+ * camel cmd memory-leak: recording the old objects of an integration with
JFR, and the samples of the recording.
*/
-class CamelMemoryLeakTest {
+@ExtendWith(MockitoExtension.class)
+class CamelMemoryLeakTest extends ActionCommandTestSupport {
+
+ @Test
+ void startARecording() throws Exception {
+ JsonObject reply = reply("recording");
+ reply.put("durationSeconds", 30);
+
+ assertThat(callWithResponse(command("--start", "--mode=single",
"--duration=30"), reply)).isZero();
+
+ JsonObject action = readActionFile(TEST_PID);
+ assertThat(action.getString("action")).isEqualTo("jfr-memory-leak");
+ assertThat(action.getString("command")).isEqualTo("start");
+ assertThat(action.getString("duration")).isEqualTo("30");
+ assertThat(printer.getOutput()).contains("JFR recording started for
PID: " + TEST_PID,
+ "Duration: 30 seconds (auto-stop)");
+ }
+
+ @Test
+ void statusOfARecordingInProgress() throws Exception {
+ JsonObject reply = reply("recording");
+ reply.put("durationSeconds", 60);
+ reply.put("elapsedMs", 20000);
+ reply.put("remainingMs", 40000);
+
+ assertThat(callWithResponse(command("--status"), reply)).isZero();
+
+
assertThat(readActionFile(TEST_PID).getString("command")).isEqualTo("status");
+ assertThat(printer.getOutput()).contains("JFR recording in progress
for PID: " + TEST_PID, "Elapsed: 20.0s",
+ "Remaining: 40.0s").doesNotContain("started");
+ }
+
+ @Test
+ void statusAfterTheRecording() throws Exception {
+ JsonObject reply = reply("completed");
+ reply.put("hasCachedResults", true);
+ reply.put("hasComparisonData", false);
+ reply.put("sampleCount", 12);
+
+ assertThat(callWithResponse(command("--status"), reply)).isZero();
+
+ assertThat(printer.getOutput()).contains("JFR recording completed. Use
--query to view results.",
+ "Cached samples: 12").doesNotContain("No old object samples");
+ }
+
+ @Test
+ void statusWithoutARecording() throws Exception {
+ assertThat(callWithResponse(command("--status"),
reply("idle"))).isZero();
+
+ assertThat(printer.getOutput()).contains("No active JFR recording for
PID: " + TEST_PID);
+ }
+
+ @Test
+ void stopShowsTheSamples() throws Exception {
+ assertThat(callWithResponse(command("--stop"), samples())).isZero();
+
+
assertThat(readActionFile(TEST_PID).getString("command")).isEqualTo("stop");
+ assertThat(printer.getOutput()).contains("Samples: 2", "Recording
duration: 1m 1s", "HashMap$Node", "2.0 KB",
+ "Cache.map -> HashMap.table");
+ }
+
+ @Test
+ void queryWithTheStackTraces() throws Exception {
+ assertThat(callWithResponse(command("--query", "--stacktrace",
"--min-size=1MB"), samples())).isZero();
+
+ JsonObject action = readActionFile(TEST_PID);
+ assertThat(action.getString("command")).isEqualTo("query");
+ assertThat(action.getString("stacktrace")).isEqualTo("true");
+ assertThat(action.getString("minSize")).isEqualTo(Long.toString(1024 *
1024));
+ assertThat(printer.getOutput()).contains("1) java.util.HashMap$Node
count:3 sampled:2.0 KB age:1.5s",
+ "chain: Cache.map -> HashMap.table", "at
org.example.Cache.put:42");
+ }
+
+ @Test
+ void theTopSamplesOnly() throws Exception {
+ assertThat(callWithResponse(command("--query", "--top=1"),
samples())).isZero();
+
+
assertThat(printer.getOutput()).contains("HashMap$Node").doesNotContain("byte[]");
+ }
+
+ @Test
+ void anErrorOfTheRecordingIsAFailure() throws Exception {
+ JsonObject reply = reply("error");
+ reply.put("error", "A JFR recording is already active. Stop it
first.");
+
+ assertThat(callWithResponse(command("--start", "--mode=single"),
reply)).isEqualTo(CliExitCode.FAILURE);
+
+ assertThat(printer.getStderr()).contains("A JFR recording is already
active. Stop it first.");
+ }
+
+ @Test
+ void anActionTheIntegrationFailsIsAFailure() throws Exception {
+ // the camel-cli-connector answers so when the action throws an
exception
+ JsonObject exception = new JsonObject(Map.of("type",
"java.lang.IllegalStateException", "message", "JFR is off"));
+ JsonObject reply = reply("failed");
+ reply.put("exception", exception);
+
+ assertThat(callWithResponse(command("--query"),
reply)).isEqualTo(CliExitCode.FAILURE);
+
+ assertThat(printer.getStderr()).contains("JFR is off");
+ }
+
+ @Test
+ void anIntegrationWithoutTheConsoleIsAFailure() throws Exception {
+ // the answer of the camel-cli-connector when the integration has no
jfr-memory-leak dev console
+ assertThat(callWithResponse(command("--query"), new
JsonObject())).isEqualTo(CliExitCode.FAILURE);
+
+ assertThat(printer.getStderr()).contains("cannot record memory leaks");
+ }
+
+ @Test
+ void comparisonOfTwoRecordings() throws Exception {
+ JsonObject comparison = new JsonObject();
+ comparison.put("allocationClass", "java.util.HashMap$Node");
+ comparison.put("baselineSampledSize", 1024);
+ comparison.put("currentSampledSize", 4096);
+ comparison.put("growthRatio", 4.0);
+ comparison.put("trend", "growing");
+ JsonObject reply = reply("compared");
+ reply.put("durationRatio", 2.0);
+ reply.put("baseline", new JsonObject(Map.of("recordingDurationMs",
60000)));
+ reply.put("current", new JsonObject(Map.of("recordingDurationMs",
120000)));
+ reply.put("comparisons", new JsonArray(List.of(comparison)));
+
+ // the answer to the compare command, which ends a dual recording
+ assertThat(callWithResponse(command("--query"), reply)).isZero();
+
+ assertThat(printer.getOutput()).contains("Run 1: 1m 0s", "Run 2: 2m
0s", "Duration ratio: 2.0x",
+ "HashMap$Node");
+ }
@Test
void sizes() {
@@ -46,4 +186,39 @@ class CamelMemoryLeakTest {
.hasMessage("--min-size must be a size such as 1024, 10KB or
1MB, was: lots")
.extracting(e -> ((CliException)
e).getExitCode()).isEqualTo(CliExitCode.USAGE);
}
+
+ private CamelMemoryLeak command(String... args) throws Exception {
+ writeStatusFile(TEST_PID, "orders");
+ CamelMemoryLeak command = new CamelMemoryLeak(new
CamelJBangMain().withPrinter(printer));
+ CommandLine.populateCommand(command,
Stream.concat(Stream.of("orders"), Arrays.stream(args)).toArray(String[]::new));
+ return command;
+ }
+
+ private static JsonObject reply(String status) {
+ JsonObject reply = new JsonObject();
+ reply.put("status", status);
+ return reply;
+ }
+
+ private static JsonObject samples() {
+ JsonObject node = new JsonObject();
+ node.put("allocationClass", "java.util.HashMap$Node");
+ node.put("count", 3);
+ node.put("sampledSize", 2048);
+ node.put("objectAge", 1500);
+ node.put("referenceChain", new JsonArray(
+ List.of(
+ new JsonObject(Map.of("type", "org.example.Cache",
"field", "map")),
+ new JsonObject(Map.of("type", "java.util.HashMap",
"field", "table")))));
+ node.put("stackTrace", new JsonArray(List.of(new
JsonObject(Map.of("method", "org.example.Cache.put", "line", 42)))));
+ JsonObject bytes = new JsonObject();
+ bytes.put("allocationClass", "byte[]");
+ bytes.put("count", 1);
+ bytes.put("sampledSize", 512);
+ JsonObject reply = reply("completed");
+ reply.put("sampleCount", 2);
+ reply.put("recordingDurationMs", 61000);
+ reply.put("samples", new JsonArray(List.of(node, bytes)));
+ return reply;
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReceiveActionTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReceiveActionTest.java
index 7f9fdd83e8cb..532870ce4d3c 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReceiveActionTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReceiveActionTest.java
@@ -16,19 +16,31 @@
*/
package org.apache.camel.dsl.jbang.core.commands.action;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+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.apache.camel.util.json.Jsoner;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-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;
+/**
+ * camel cmd receive: starting and stopping to receive in a running
integration, its status, and the messages it
+ * received.
+ */
@ExtendWith(MockitoExtension.class)
class CamelReceiveActionTest extends ActionCommandTestSupport {
@@ -36,40 +48,241 @@ class CamelReceiveActionTest extends
ActionCommandTestSupport {
void testStartsReceivingFromNamedIntegration() throws Exception {
writeStatusFile(TEST_PID, "myApp");
- CamelReceiveAction command = new CamelReceiveAction(new
CamelJBangMain().withPrinter(printer));
- command.name = "myApp";
- // request the start sub-action against the running integration (no
--endpoint, so no auto-dump/spawn)
- command.action = "start";
- command.loggingColor = false;
+ // the start sub-action against the running integration (no
--endpoint, so no auto-dump/spawn)
+ int exit = callWithResponse(command("myApp", "--action=start"), new
JsonObject());
+
+ assertThat(exit).isZero();
+ JsonObject action = readActionFile(TEST_PID);
+ assertThat(action).as("action file should be written for the matched
process").isNotNull();
+ assertThat(action.getString("action")).isEqualTo("receive");
+ assertThat(action.getString("enabled")).isEqualTo("true");
+ assertThat(action.getString("endpoint")).isEqualTo("*");
+ assertThat(printer.getOutput()).contains("Starting to receive messages
from existing Camel: myApp");
+ }
- int exit = callWithResponse(command, new JsonObject());
+ @Test
+ void testRendersNothingWhenNameDoesNotMatch() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
- assertEquals(0, exit);
+ CamelReceiveAction command = command("doesNotExist", "--action=start");
- JsonObject action = readActionFile(TEST_PID);
- assertNotNull(action, "action file should be written for the matched
process");
- assertEquals("receive", action.getString("action"));
- assertEquals("true", action.getString("enabled"));
- assertEquals("*", action.getString("endpoint"));
+ 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 stopTurnsReceivingOff() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+
+ assertThat(callWithResponse(command("myApp", "--action=stop"), new
JsonObject())).isZero();
+
+
assertThat(readActionFile(TEST_PID).getString("enabled")).isEqualTo("false");
+ }
+
+ @Test
+ void failureToStartReceivingIsAnError() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+ JsonObject reply = new JsonObject();
+ reply.put("url", "kafka:payments");
+ reply.put("error", "Cannot connect to the broker");
+
+ assertThat(callWithResponse(command("myApp", "--action=start"),
reply)).isEqualTo(CliExitCode.FAILURE);
+
+ assertThat(printer.getStderr())
+ .contains("Error starting to receive messages from:
kafka:payments due to: Cannot connect to the broker");
+ }
+
+ @Test
+ void endpointStartsReceivingAndDumpsTheMessages() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+ writeReceived(batch(message(1, "Hello Camel")));
+
+ assertThat(callWithResponse(command("myApp",
"--endpoint=seda:orders"), new JsonObject())).isZero();
+
+
assertThat(readActionFile(TEST_PID).getString("endpoint")).isEqualTo("seda:orders");
+ assertThat(printer.getOutput()).contains("Received Message: (1)",
"Hello Camel");
+ }
+
+ @Test
+ void clearEmptiesTheReceivedMessages() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+ Path received = writeReceived(batch(message(1, "Hello Camel")));
+
+ assertThat(callWithSingleProcess(command("myApp",
"--action=clear"))).isZero();
+
+ assertThat(Files.readString(received)).isEqualTo("{}");
+ }
+
+ @Test
+ void unknownActionIsAUsageError() {
+ assertThatThrownBy(() -> command("myApp", "--action=pause").doCall())
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE))
+ .hasMessageContaining("Unknown action: pause");
+ }
+
+ @Test
+ void statusListsTheEndpointsOfTheIntegration() throws Exception {
+ writeStatusFile(TEST_PID, receiveStatus("seda:orders",
"jms:payments?password=secret"));
+
+ assertThat(callWithSingleProcess(command("myApp", "--mask"))).isZero();
String out = printer.getOutput();
- assertTrue(out.contains("Starting to receive messages from existing
Camel: myApp"),
- "should report connecting to the running integration, was: " +
out);
+ assertThat(out).contains("myApp", "Enabled", "seda:orders",
"jms:payments");
+ assertThat(out).as("--mask hides the
password").doesNotContain("secret");
}
@Test
- void testRendersNothingWhenNameDoesNotMatch() throws Exception {
+ void statusWithAnUnknownSortIsAUsageError() throws Exception {
+ writeStatusFile(TEST_PID, receiveStatus("seda:orders"));
+
+ CamelReceiveAction command = command("myApp", "--sort=size");
+
+ assertThatThrownBy(() -> callWithSingleProcess(command))
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE))
+ .hasMessageContaining("--sort must be one of");
+ }
+
+ @Test
+ void dumpPrintsTheReceivedMessages() throws Exception {
writeStatusFile(TEST_PID, "myApp");
+ writeReceived(batch(message(1, "Hello Camel"), message(2, "Bye
Camel")));
+ assertThat(callWithSingleProcess(command("myApp",
"--action=dump"))).isZero();
+
+ String out = printer.getOutput();
+ assertThat(out).contains("Received Message: (1)", "Received Message:
(2)", "seda:orders", "orderId",
+ "Hello Camel", "Bye Camel");
+ }
+
+ @Test
+ void dumpWithoutANameReadsTheRunningIntegrations() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+ writeReceived(batch(message(1, "Hello Camel")));
+
+ assertThat(callWithSingleProcess(command("--action=dump"))).isZero();
+
+ assertThat(printer.getOutput()).contains("Received Message: (1)",
"Hello Camel");
+ }
+
+ @Test
+ void dumpOfANameThatMatchesNoIntegrationIsNotFound() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+
+ CamelReceiveAction command = command("doesNotExist", "--action=dump");
+
+ assertThatThrownBy(() -> callWithSingleProcess(command))
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.NOT_FOUND));
+ }
+
+ @Test
+ void dumpOnlyTheBody() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+ writeReceived(batch(message(1, "Hello Camel")));
+
+ assertThat(callWithSingleProcess(command("myApp", "--action=dump",
"--only-body"))).isZero();
+
+ assertThat(printer.getOutput()).contains("Hello
Camel").doesNotContain("orderId");
+ }
+
+ @Test
+ void dumpTheLastMessages() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+ writeReceived(batch(message(1, "first")), batch(message(2, "second"),
message(3, "third")));
+
+ assertThat(callWithSingleProcess(command("myApp", "--action=dump",
"--tail=1"))).isZero();
+
+
assertThat(printer.getOutput()).contains("third").doesNotContain("first",
"second");
+ }
+
+ @Test
+ void dumpTheMatchingMessages() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+ writeReceived(batch(message(1, "Hello Camel"), message(2, "Bye
World")));
+
+ assertThat(callWithSingleProcess(command("myApp", "--action=dump",
"--grep=world"))).isZero();
+
+ // without the highlighting of the match
+ assertThat(printer.getOutput().replaceAll("\u001B\\[[;\\d]*m",
"")).contains("Bye World")
+ .doesNotContain("Hello Camel");
+ }
+
+ @Test
+ void dumpAsJsonPrintsEachMessageOnce() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+ writeReceived(batch(message(1, "Hello Camel"), message(2, "Bye
Camel")));
+
+ assertThat(callWithSingleProcess(command("myApp", "--action=dump",
"--output=json"))).isZero();
+
+ List<String> lines = printer.getLines();
+ assertThat(lines).hasSize(2);
+ assertThat(lines).extracting(l -> Jsoner.deserialize(l, (JsonObject)
null).getLong("uid")).containsExactly(1L, 2L);
+ assertThat(lines.get(0)).contains("Hello Camel").doesNotContain("Bye
Camel");
+ }
+
+ private CamelReceiveAction command(String... args) {
CamelReceiveAction command = new CamelReceiveAction(new
CamelJBangMain().withPrinter(printer));
- command.name = "doesNotExist";
- command.action = "start";
- command.loggingColor = false;
+ CommandLine.populateCommand(command,
+ Stream.concat(Stream.of("--follow=false",
"--logging-color=false"), Arrays.stream(args))
+ .toArray(String[]::new));
+ return command;
+ }
- CliException e = assertThrows(CliException.class, () ->
callWithSingleProcess(command));
+ // the received messages, a batch per line as the camel-cli-connector
writes them
+ private static Path writeReceived(JsonObject... batches) throws Exception {
+ Path f = CommandLineHelper.getCamelDir().resolve(TEST_PID +
"-receive.json");
+ Files.write(f,
Arrays.stream(batches).map(JsonObject::toJson).toList());
+ return f;
+ }
+
+ private static JsonObject batch(JsonObject... messages) {
+ JsonObject batch = new JsonObject();
+ batch.put("messages", new JsonArray(Arrays.asList(messages)));
+ return batch;
+ }
+
+ private static JsonObject message(long uid, String body) {
+ JsonObject header = new JsonObject();
+ header.put("key", "orderId");
+ header.put("type", "java.lang.String");
+ header.put("value", "42");
+ JsonObject value = new JsonObject();
+ value.put("type", "java.lang.String");
+ value.put("value", body);
+ JsonObject message = new JsonObject();
+ message.put("exchangeId", "A-" + uid);
+ message.put("messageType", "DefaultMessage");
+ message.put("headers", new JsonArray(List.of(header)));
+ message.put("body", value);
+ JsonObject answer = new JsonObject();
+ answer.put("uid", uid);
+ answer.put("endpointUri", "seda:orders");
+ answer.put("remoteEndpoint", false);
+ answer.put("timestamp", System.currentTimeMillis());
+ answer.put("message", message);
+ return answer;
+ }
- 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");
+ private static JsonObject receiveStatus(String... uris) {
+ JsonObject receive = new JsonObject();
+ receive.put("enabled", true);
+ receive.put("total", 3);
+ receive.put("lastTimestamp", System.currentTimeMillis());
+ receive.put("endpoints", new JsonArray(Arrays.stream(uris).map(uri -> {
+ JsonObject endpoint = new JsonObject();
+ endpoint.put("uri", uri);
+ return endpoint;
+ }).toList()));
+ JsonObject context = new JsonObject();
+ context.put("name", "myApp");
+ JsonObject root = new JsonObject();
+ root.put("context", context);
+ root.put("receive", receive);
+ return root;
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendActionResultTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendActionResultTest.java
index e4bc88d58fc2..acbda0f7dc2b 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendActionResultTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendActionResultTest.java
@@ -101,6 +101,30 @@ class CamelSendActionResultTest extends
ActionCommandTestSupport {
assertThat(readActionFile(TEST_PID).getString("body")).isEqualTo("file:" +
body.toAbsolutePath());
}
+ @Test
+ void coloredStatusLineIsKeptByThePrinter() throws Exception {
+ writeStatusFile(TEST_PID, "orders");
+ CamelSendAction command = command("orders", "--endpoint=direct:start");
+ command.loggingColor = true;
+
+ assertThat(callWithResponse(command,
answer("success"))).isEqualTo(CliExitCode.OK);
+
+ // the colored parts of the status line too, so a CLI that runs the
command in its JVM has all of it
+ String out = printer.getStdout().replaceAll("\u001B\\[[;\\d]*m", "");
+ assertThat(out).contains(Long.toString(TEST_PID), "direct://start",
"Reply received (success)", " (5ms)");
+ }
+
+ @Test
+ void anActionTheIntegrationFailsIsReported() throws Exception {
+ writeStatusFile(TEST_PID, "orders");
+ // the answer of the camel-cli-connector when the action throws an
exception: no exchange, no elapsed time
+ JsonObject failed = new JsonObject(Map.of("status", "failed", "error",
"No endpoint could be found for: foo:bar"));
+
+ assertThat(callWithResponse(command("orders", "--endpoint=foo:bar"),
failed)).isEqualTo(CliExitCode.FAILURE);
+
+ assertThat(printer.getStderr()).contains("No endpoint could be found
for: foo:bar");
+ }
+
@Test
void headerWithoutValueIsAUsageError() {
assertThatThrownBy(() -> command("orders", "--endpoint=direct:start",
"--header=id").doCall())
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceActionTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceActionTest.java
index f05201da6a85..d78f731dc978 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceActionTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceActionTest.java
@@ -18,8 +18,13 @@ package org.apache.camel.dsl.jbang.core.commands.action;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
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;
@@ -30,9 +35,10 @@ import org.mockito.junit.jupiter.MockitoExtension;
import picocli.CommandLine;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
- * camel trace: the status of tracing in the integrations, and starting,
stopping and clearing it.
+ * camel trace: the status of tracing in the integrations, starting, stopping
and clearing it, and the traced messages.
*/
@ExtendWith(MockitoExtension.class)
class CamelTraceActionTest extends ActionCommandTestSupport {
@@ -93,6 +99,152 @@ class CamelTraceActionTest extends ActionCommandTestSupport
{
assertThat(Files.readString(traceFile)).isEqualTo("{}");
}
+ @Test
+ void statusWithAnUnknownSortIsAUsageError() throws Exception {
+ writeStatusFile(TEST_PID, status(true, false));
+
+ CamelTraceAction command = command("--sort=size");
+
+ assertThatThrownBy(() -> callWithSingleProcess(command))
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE))
+ .hasMessageContaining("--sort must be one of pid, name, age");
+ }
+
+ @Test
+ void dumpPrintsTheTracedMessages() throws Exception {
+ writeStatusFile(TEST_PID, status(true, false));
+ writeTraces(exchange());
+
+ assertThat(callWithSingleProcess(dump())).isZero();
+
+ String out = printer.getOutput();
+ assertThat(out).contains("Created", "route1/*-->", "route1/log1",
"Processed", "Completed (success)",
+ "Hello Camel", "orderId");
+ assertThat(out).as("the elapsed time of a step").contains(" (5ms)");
+ }
+
+ @Test
+ void dumpWithColorsKeepsAllTheOutputWithThePrinter() throws Exception {
+ writeStatusFile(TEST_PID, status(true, false));
+ writeTraces(exchange());
+
+
assertThat(callWithSingleProcess(dump("--logging-color=true"))).isZero();
+
+ // the timestamp, pid, thread and node of each trace are colored, and
printed with the printer of the command
+ String out = printer.getOutput().replaceAll("\u001B\\[[;\\d]*m", "");
+ assertThat(out).contains(Long.toString(TEST_PID), "[", "route1/log1",
"Completed (success)", "(5ms)");
+ }
+
+ @Test
+ void dumpTheSourceLocationInsteadOfTheIds() throws Exception {
+ writeStatusFile(TEST_PID, status(true, false));
+ writeTraces(exchange());
+
+ assertThat(callWithSingleProcess(dump("--source"))).isZero();
+
+
assertThat(printer.getOutput()).contains("route.yaml:3").doesNotContain("route1/log1");
+ }
+
+ @Test
+ void dumpOnlyCreatedAndCompletedAtDepthZero() throws Exception {
+ writeStatusFile(TEST_PID, status(true, false));
+ writeTraces(exchange());
+
+ assertThat(callWithSingleProcess(dump("--depth=0"))).isZero();
+
+ assertThat(printer.getOutput()).contains("Created", "Completed
(success)").doesNotContain("route1/log1");
+ }
+
+ @Test
+ void dumpTheMatchingTraces() throws Exception {
+ writeStatusFile(TEST_PID, status(true, false));
+ writeTraces(exchange());
+
+ assertThat(callWithSingleProcess(dump("--grep=world"))).isZero();
+
+ // the message of the step after the log matches
+
assertThat(printer.getOutput()).contains("route1/log1").doesNotContain("Created",
"Completed");
+ }
+
+ @Test
+ void dumpTheLastTraces() throws Exception {
+ writeStatusFile(TEST_PID, status(true, false));
+ writeTraces(exchange());
+
+ assertThat(callWithSingleProcess(dump("--tail=1"))).isZero();
+
+ assertThat(printer.getOutput()).contains("Completed
(success)").doesNotContain("Created", "route1/log1");
+ }
+
+ @Test
+ void dumpOfANameThatMatchesNoIntegrationIsNotFound() throws Exception {
+ writeStatusFile(TEST_PID, status(true, false));
+
+ CamelTraceAction command = command("payments", "--action=dump",
"--follow=false");
+
+ assertThatThrownBy(() -> callWithSingleProcess(command))
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.NOT_FOUND))
+ .hasMessage("No running Camel integration matches: payments");
+ }
+
+ private CamelTraceAction dump(String... args) {
+ List<String> all = new ArrayList<>(List.of("--action=dump",
"--follow=false"));
+ if (Arrays.stream(args).noneMatch(a ->
a.startsWith("--logging-color"))) {
+ all.add("--logging-color=false");
+ }
+ all.addAll(Arrays.asList(args));
+ return command(all.toArray(String[]::new));
+ }
+
+ // the traces of an exchange, a line per step as the camel-cli-connector
writes them
+ private static void writeTraces(JsonObject... traces) throws Exception {
+ Path f = CommandLineHelper.getCamelDir().resolve(TEST_PID +
"-trace.json");
+ Files.write(f, Arrays.stream(traces).map(t -> {
+ JsonObject line = new JsonObject();
+ line.put("traces", new JsonArray(List.of(t)));
+ return line.toJson();
+ }).toList());
+ }
+
+ private static JsonObject[] exchange() {
+ return new JsonObject[] {
+ trace(1, true, false, null, false, 0, "Hello Camel"),
+ trace(2, false, false, "log1", true, 5, "Hello World"),
+ trace(3, false, true, null, true, 7, "Hello Camel") };
+ }
+
+ private static JsonObject trace(
+ long uid, boolean first, boolean last, String nodeId, boolean
done, long elapsed, String text) {
+ JsonObject header = new JsonObject();
+ header.put("key", "orderId");
+ header.put("type", "java.lang.String");
+ header.put("value", "42");
+ JsonObject body = new JsonObject();
+ body.put("type", "java.lang.String");
+ body.put("value", text);
+ JsonObject message = new JsonObject();
+ message.put("exchangeId", "A-1");
+ message.put("exchangePattern", "InOnly");
+ message.put("headers", new JsonArray(List.of(header)));
+ message.put("body", body);
+ JsonObject trace = new JsonObject();
+ trace.put("uid", uid);
+ trace.put("first", first);
+ trace.put("last", last);
+ trace.put("location", "route.yaml:" + (uid + 1));
+ trace.put("routeId", "route1");
+ trace.put("nodeId", nodeId);
+ trace.put("timestamp", System.currentTimeMillis());
+ trace.put("elapsed", elapsed);
+ trace.put("failed", false);
+ trace.put("done", done && !last);
+ trace.put("threadName", "Camel (orders) thread #1 - timer://tick");
+ trace.put("message", message);
+ return trace;
+ }
+
private CamelTraceAction command(String... args) {
CamelTraceAction command = new CamelTraceAction(new
CamelJBangMain().withPrinter(printer));
CommandLine.populateCommand(command, args);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageActionTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageActionTest.java
new file mode 100644
index 000000000000..d4b267878a8f
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageActionTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.action;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+
+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.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * camel transform message: what it asks the Camel it started to transform,
and how it shows the answer.
+ */
+@ExtendWith(MockitoExtension.class)
+class TransformMessageActionTest extends ActionCommandTestSupport {
+
+ @TempDir
+ Path dir;
+
+ @Test
+ void transformsTheMessage() throws Exception {
+ TransformMessageAction command = command("--body=Hello World",
"--language=simple", "--template=Bye ${body}",
+ "--header=id=1", "--option=trim=true");
+
+ assertThat(callWithResponse(command::doWatchCall, answer("success",
"Bye World"))).isEqualTo(CliExitCode.OK);
+
+ JsonObject action = readActionFile(TEST_PID);
+ assertThat(action.getString("action")).isEqualTo("transform");
+ assertThat(action.getString("language")).isEqualTo("simple");
+ assertThat(action.getString("template")).isEqualTo("Bye ${body}");
+ assertThat(action.getString("body")).isEqualTo("Hello World");
+ JsonObject header = ((JsonArray) action.get("headers")).getMap(0);
+ assertThat(header.getString("key") + "=" +
header.getString("value")).isEqualTo("id=1");
+ JsonObject option = ((JsonArray) action.get("options")).getMap(0);
+ assertThat(option.getString("key") + "=" +
option.getString("value")).isEqualTo("trim=true");
+ assertThat(printer.getStdout()).contains("Message transformed
(success)", " (3ms)", "Bye World");
+ }
+
+ @Test
+ void outputGoesToTheFile() throws Exception {
+ Path output = dir.resolve("out.json");
+ TransformMessageAction command = command("--body=Hello",
"--language=simple", "--template=Bye",
+ "--output=" + output);
+
+ assertThat(callWithResponse(command::doWatchCall, answer("success",
"Bye"))).isEqualTo(CliExitCode.OK);
+
+ assertThat(printer.getStdout()).contains("Output saved to file
(success)").doesNotContain("Bye");
+ JsonObject written = Jsoner.deserialize(Files.readString(output),
(JsonObject) null);
+ assertThat(written.getString("exchangeId")).isEqualTo("ex-1");
+ }
+
+ @Test
+ void failedTransformationShowsTheException() throws Exception {
+ TransformMessageAction command = command("--body=Hello",
"--language=simple", "--template=${bean:nope}");
+ JsonObject answer = answer("failed", null);
+ answer.put("exception",
+ new JsonObject(Map.of("type",
"org.apache.camel.NoSuchBeanException", "message", "No bean: nope")));
+
+ assertThat(callWithResponse(command::doWatchCall,
answer)).isEqualTo(CliExitCode.FAILURE);
+
+ assertThat(printer.getStdout()).contains("Failed (exception)", "No
bean: nope");
+ }
+
+ @Test
+ void anActionTheIntegrationFailsIsReported() throws Exception {
+ TransformMessageAction command = command("--body=Hello",
"--language=nope", "--template=Bye");
+ // the answer of the camel-cli-connector when the action throws an
exception: no message, no timestamp
+ JsonObject failed = new JsonObject(Map.of("status", "failed", "error",
"No language could be found for: nope"));
+
+ assertThat(callWithResponse(command::doWatchCall,
failed)).isEqualTo(CliExitCode.FAILURE);
+
+ assertThat(printer.getStdout()).contains("Failed (exception)");
+ assertThat(printer.getStderr()).contains("No language could be found
for: nope");
+ }
+
+ @Test
+ void coloredStatusLineIsKeptByThePrinter() throws Exception {
+ TransformMessageAction command = command("--body=Hello",
"--language=simple", "--template=Bye",
+ "--logging-color=true");
+
+ assertThat(callWithResponse(command::doWatchCall, answer("success",
"Bye"))).isEqualTo(CliExitCode.OK);
+
+ String out = printer.getStdout().replaceAll("\u001B\\[[;\\d]*m", "");
+ assertThat(out).contains(Long.toString(TEST_PID), "Message transformed
(success)", " (3ms)");
+ }
+
+ @Test
+ void headerWithoutValueIsAUsageError() {
+ TransformMessageAction command = command("--body=Hello",
"--language=simple", "--template=Bye", "--header=id");
+
+ assertThatThrownBy(command::doWatchCall)
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE))
+ .hasMessage("Header must be in key=value format, was: id");
+ }
+
+ @Test
+ void templateAndLanguageAreRequired() throws Exception {
+
assertThat(command("--body=Hello").doCall()).isEqualTo(CliExitCode.USAGE);
+ assertThat(printer.getStderr()).contains("Either source or template
and one of language/component must be configured");
+
+ assertThat(command("--body=Hello",
"--language=simple").doCall()).isEqualTo(CliExitCode.USAGE);
+ assertThat(printer.getStderr()).contains("Both template and one of
language/component must be configured");
+ }
+
+ @Test
+ void templateFileThatDoesNotExistIsNotFound() throws Exception {
+ Path template = dir.resolve("nope.txt");
+
+ assertThat(command("--body=Hello", "--language=simple",
"--template=file:" + template).doCall())
+ .isEqualTo(CliExitCode.NOT_FOUND);
+
+ assertThat(printer.getStderr()).contains("Template file does not
exist: " + template);
+ }
+
+ private TransformMessageAction command(String... args) {
+ TransformMessageAction command = new TransformMessageAction(new
CamelJBangMain().withPrinter(printer));
+ CommandLine.populateCommand(command, args);
+ if (!String.join(" ", args).contains("--logging-color")) {
+ command.loggingColor = false;
+ }
+ // the Camel the command started, here the fake integration
+ command.pid = TEST_PID;
+ return command;
+ }
+
+ // the answer of the transform action of the integration
+ private static JsonObject answer(String status, String transformed) {
+ JsonObject answer = new JsonObject();
+ answer.put("language", "simple");
+ answer.put("exchangeId", "ex-1");
+ answer.put("timestamp", 1_700_000_000_000L);
+ answer.put("elapsed", 3L);
+ answer.put("status", status);
+ if (transformed != null) {
+ JsonObject body = new JsonObject();
+ body.put("type", "java.lang.String");
+ body.put("value", transformed);
+ JsonObject message = new JsonObject();
+ message.put("body", body);
+ answer.put("message", message);
+ }
+ return answer;
+ }
+}