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 dcb04ce16ebf CAMEL-23688: camel-jbang - Tests for get bean, get
route-controller and top source (#27975)
dcb04ce16ebf is described below
commit dcb04ce16ebfabbc5f5704184ccabde563445703
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 16 23:37:29 2026 +0200
CAMEL-23688: camel-jbang - Tests for get bean, get route-controller and top
source (#27975)
Tests for camel get bean (58% covered), camel get route-controller (52%)
and camel top source (52%), with the answers of the bean, bean-model,
route-controller and top dev consoles. They found these bugs, fixed
here:
* camel get bean --dsl paired the configuration of the properties with
the properties by position: it showed the configuration of another
property, or failed with an IndexOutOfBoundsException with
--nulls=false, where the two lists differ. Now by name. An unknown
--scope showed every bean; now a usage error.
* camel get route-controller read the back-off times and attempts, which
are longs, as ints, and showed "Starting Routes: false" for the default
route controller, which does not report it.
* camel top source --json had its counters as text, unlike the JSON of
the other listings.
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 10 +
.../jbang/core/commands/action/CamelBeanDump.java | 25 ++-
.../jbang/core/commands/action/CamelSourceTop.java | 15 +-
.../commands/action/RouteControllerAction.java | 18 +-
.../core/commands/action/CamelBeanDumpTest.java | 238 +++++++++++++++++----
.../core/commands/action/CamelSourceTopTest.java | 185 +++++++++++++---
.../commands/action/RouteControllerActionTest.java | 233 +++++++++++++++++---
7 files changed, 597 insertions(+), 127 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 a64f86317060..cf02b0eaf395 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
@@ -534,6 +534,16 @@ or follows, like Enter does.
nothing), and a route that cannot be read, such as invalid YAML, exits with
`1` and the error of the parser (it failed
with an internal error, `70`, and a stack trace).
+=== camel-jbang - camel get bean, get route-controller and top source
+
+* `camel get bean --dsl` shows the configuration of each property next to that
property: it paired them by position,
+ which showed the configuration of another property, or failed with an
internal error (`70`) with `--nulls=false`.
+ An unknown `--scope` exits with `2` (it showed every bean).
+* `camel get route-controller` shows back-off times and attempts that do not
fit in an int (it showed other numbers),
+ and no longer shows `Starting Routes: false` for the default route
controller, which does not tell.
+* `camel top source --json` has the counters `total`, `mean`, `min`, `max` and
`last` as numbers instead of text, as
+ the other listings.
+
=== camel-jbang - a boolean option given without a value is on
A boolean option that is on by default is now on when it is given without a
value, like any other boolean option:
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelBeanDump.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelBeanDump.java
index 79b817aadc4c..644f6220ad93 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelBeanDump.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelBeanDump.java
@@ -18,8 +18,10 @@ package org.apache.camel.dsl.jbang.core.commands.action;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import com.github.freva.asciitable.AsciiTable;
import com.github.freva.asciitable.Column;
@@ -46,6 +48,8 @@ import picocli.CommandLine.Command;
" camel get bean --json" })
public class CamelBeanDump extends ActionBaseCommand {
+ private static final List<String> SCOPES = List.of("all", "user", "camel",
"spring", "quarkus");
+
private static final RowSort<Row> ORDER = new RowSort<Row>()
.text("name", r -> r.name)
.text("type", r -> r.type);
@@ -69,7 +73,7 @@ public class CamelBeanDump extends ActionBaseCommand {
@Override
public Iterator<String> iterator() {
- return List.of("all", "user", "camel", "spring",
"quarkus").iterator();
+ return SCOPES.iterator();
}
}
@@ -122,6 +126,10 @@ public class CamelBeanDump extends ActionBaseCommand {
public Integer doCall() throws Exception {
List<Row> rows = new ArrayList<>();
+ if (!SCOPES.contains(scope)) {
+ throw CliException.usage("--scope must be one of " +
String.join(", ", SCOPES) + ", was: " + scope);
+ }
+
CamelProcess process = findOne(name);
this.pid = process.pid();
@@ -149,7 +157,15 @@ public class CamelBeanDump extends ActionBaseCommand {
row.name = jt.getString("name");
row.type = jt.getString("type");
JsonArray arr = jt.getCollection("properties");
+ // the configuration of the properties in the DSL, by name: the
two lists can differ without --nulls
+ Map<String, String> configValues = new HashMap<>();
JsonArray arr2 = jt.getCollection("modelProperties");
+ if (arr2 != null) {
+ for (Object o : arr2) {
+ JsonObject p2 = (JsonObject) o;
+ configValues.put(p2.getString("name"),
p2.getString("value"));
+ }
+ }
if (arr != null) {
row.properties = new ArrayList<>();
for (int i = 0; i < arr.size(); i++) {
@@ -159,12 +175,7 @@ public class CamelBeanDump extends ActionBaseCommand {
pr.name = p.getString("name");
pr.type = p.getString("type");
pr.value = p.get("value");
- if (arr2 != null) {
- JsonObject p2 = (JsonObject) arr2.get(i);
- if (p2 != null) {
- pr.configValue = p2.getString("value");
- }
- }
+ pr.configValue = configValues.get(pr.name);
}
}
rows.add(row);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceTop.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceTop.java
index d2816a69d494..bfec4d73ff79 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceTop.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceTop.java
@@ -133,11 +133,12 @@ public class CamelSourceTop extends ActionWatchCommand {
row.put("id", r.id);
row.put("routeId", r.routeId);
row.put("location", r.location);
- row.put("total", r.total);
- row.put("mean", r.mean);
- row.put("min", r.min);
- row.put("max", r.max);
- row.put("last", r.last);
+ // the counters are numbers, as in the JSON of the other
listings
+ row.put("total", number(r.total));
+ row.put("mean", number(r.mean));
+ row.put("min", number(r.min));
+ row.put("max", number(r.max));
+ row.put("last", number(r.last));
row.put("code", r.code.stream().map(c -> {
JsonObject line = new JsonObject();
line.put("line", c.line);
@@ -176,6 +177,10 @@ public class CamelSourceTop extends ActionWatchCommand {
}
}
+ private static Long number(String value) {
+ return value != null ? Long.valueOf(value) : null;
+ }
+
protected int sortRow(Row o1, Row o2) {
// sort for highest mean value as we want the slowest in the top
long m1 = o1.mean != null ? Long.parseLong(o1.mean) : 0;
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/RouteControllerAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/RouteControllerAction.java
index 535591b3eeb3..22375e42107b 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/RouteControllerAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/RouteControllerAction.java
@@ -164,14 +164,15 @@ public class RouteControllerAction extends
ActionWatchCommand {
printer().printf("\tInitial Starting Routes: %b%n",
jo.getBoolean("startingRoutes"));
printer().printf("\tUnhealthy Routes: %b%n",
jo.getBoolean("unhealthyRoutes"));
printer().printf("\tRoutes Total: %s%n",
jo.getInteger("totalRoutes"));
- printer().printf("\tRoutes Started: %d%n",
jo.getInteger("startedRoutes"));
+ printer().printf("\tRoutes Started: %d%n",
jo.getLong("startedRoutes"));
printer().printf("\tRoutes Restarting: %d%n",
jo.getInteger("restartingRoutes"));
printer().printf("\tRoutes Exhausted: %d%n",
jo.getInteger("exhaustedRoutes"));
- printer().printf("\tInitial Delay: %d%n",
jo.getInteger("initialDelay"));
- printer().printf("\tBackoff Delay: %d%n",
jo.getInteger("backoffDelay"));
- printer().printf("\tBackoff Max Delay: %d%n",
jo.getInteger("backoffMaxDelay"));
- printer().printf("\tBackoff Max Elapsed Time: %d%n",
jo.getInteger("backoffMaxElapsedTime"));
- printer().printf("\tBackoff Max Attempts: %d%n",
jo.getInteger("backoffMaxAttempts"));
+ // the delays and times in millis, and the attempts, are
longs
+ printer().printf("\tInitial Delay: %d%n",
jo.getLong("initialDelay"));
+ printer().printf("\tBackoff Delay: %d%n",
jo.getLong("backoffDelay"));
+ printer().printf("\tBackoff Max Delay: %d%n",
jo.getLong("backoffMaxDelay"));
+ printer().printf("\tBackoff Max Elapsed Time: %d%n",
jo.getLong("backoffMaxElapsedTime"));
+ printer().printf("\tBackoff Max Attempts: %d%n",
jo.getLong("backoffMaxAttempts"));
printer().printf("\tThread Pool Size: %d%n",
jo.getInteger("threadPoolSize"));
printer().printf("\tUnhealthy On Restarting: %b%n",
jo.getBoolean("unhealthyOnRestarting"));
printer().printf("\tUnhealthy On Exhaust: %b%n",
jo.getBoolean("unhealthyOnExhausted"));
@@ -181,7 +182,10 @@ public class RouteControllerAction extends
ActionWatchCommand {
} else {
if (header) {
printer().println("Default Route Controller");
- printer().printf("\tStarting Routes: %b%n",
jo.getBoolean("startingRoutes"));
+ // only the supervising route controller tells
+ if (jo.containsKey("startingRoutes")) {
+ printer().printf("\tStarting Routes: %b%n",
jo.getBoolean("startingRoutes"));
+ }
printer().printf("\tRoutes Total: %s%n",
jo.getInteger("totalRoutes"));
printer().println("\n");
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelBeanDumpTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelBeanDumpTest.java
index ba23696ab8b6..8f40c1036f76 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelBeanDumpTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelBeanDumpTest.java
@@ -16,20 +16,29 @@
*/
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.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 get bean: the beans of an integration with their properties, as the
bean dev console has them, and the beans of
+ * the YAML or XML DSL with their configuration, as the bean-model dev console
has them.
+ */
@ExtendWith(MockitoExtension.class)
class CamelBeanDumpTest extends ActionCommandTestSupport {
@@ -37,62 +46,207 @@ class CamelBeanDumpTest extends ActionCommandTestSupport {
void testRequestsBeanDumpAndRendersBeans() throws Exception {
writeStatusFile(TEST_PID, "myApp");
- CamelBeanDump command = new CamelBeanDump(new
CamelJBangMain().withPrinter(printer));
- command.name = "myApp";
- // options are normally defaulted by picocli; set them as we construct
the command directly
- command.filter = "all";
- command.sort = "name";
- command.scope = "all";
- command.properties = true;
- command.nulls = true;
+ assertThat(callWithResponse(command(), beans(bean("myBean",
"com.foo.MyBean",
+ property("greeting", "java.lang.String",
"helloWorld"))))).isZero();
+
+ JsonObject action = readActionFile(TEST_PID);
+ assertThat(action.getString("action")).isEqualTo("bean");
+ assertThat(action.containsKey("filter")).isFalse();
+ assertThat(action.getBoolean("properties")).isTrue();
+ assertThat(action.getBoolean("nulls")).isTrue();
+ assertThat(action.getBoolean("internal")).isFalse();
+ assertThat(printer.getOutput()).contains("BEAN: myBean
(com.foo.MyBean):", "PROPERTY", "TYPE", "VALUE", "greeting",
+ "java.lang.String", "helloWorld");
+ }
+
+ @Test
+ void aNullValue() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+
+ assertThat(callWithResponse(command(), beans(bean("myBean",
"com.foo.MyBean",
+ property("greeting", "java.lang.String", null))))).isZero();
+
+ assertThat(lineOf("greeting")).contains("null");
+ }
+
+ @Test
+ void withoutPropertiesATableOfTheBeans() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+
+ assertThat(callWithResponse(command("--properties=false"),
someBeans())).isZero();
+
+ String[] lines = printer.getOutput().split(System.lineSeparator());
+ assertThat(lines[0]).contains("NAME", "TYPE");
+ assertThat(printer.getOutput()).doesNotContain("BEAN:", "PROPERTY");
+ // by name
+ assertThat(lines[1]).contains("camel-tracer");
+ assertThat(lines[2]).contains("executor",
"java.util.concurrent.ThreadPoolExecutor");
+ }
+
+ @Test
+ void sortByType() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+
+ assertThat(callWithResponse(command("--properties=false",
"--sort=type"), someBeans())).isZero();
+
+ String[] lines = printer.getOutput().split(System.lineSeparator());
+ assertThat(lines[1]).contains("com.foo.OrderService");
+ assertThat(lines[5]).contains("org.springframework");
+ }
+
+ @Test
+ void userScopeLeavesOutTheBeansOfTheFrameworksAndTheJdk() throws Exception
{
+ writeStatusFile(TEST_PID, "myApp");
+
+ assertThat(callWithResponse(command("--properties=false",
"--scope=user"), someBeans())).isZero();
+
+ // the internal beans are asked for, as a scope picks from all of them
+ assertThat(readActionFile(TEST_PID).getBoolean("internal")).isTrue();
+
assertThat(printer.getOutput()).contains("orders").doesNotContain("camel-tracer",
"executor", "quarkus",
+ "spring");
+ }
+
+ @Test
+ void camelScope() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+
+ assertThat(callWithResponse(command("--properties=false",
"--scope=camel"), someBeans())).isZero();
+
+
assertThat(printer.getOutput()).contains("camel-tracer").doesNotContain("orders",
"executor");
+ }
+
+ @Test
+ void unknownScopeIsAUsageError() {
+ CamelBeanDump command = command("--scope=mine");
+
+ assertThatThrownBy(command::doCall)
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE))
+ .hasMessage("--scope must be one of all, user, camel, spring,
quarkus, was: mine");
+ }
- int exit = callWithResponse(command, singleBeanResponse());
+ @Test
+ void theFilterAndOptionsAreSentToTheIntegration() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
- assertEquals(0, exit);
+ assertThat(callWithResponse(command("--filter=order*",
"--nulls=false", "--internal"), someBeans())).isZero();
JsonObject action = readActionFile(TEST_PID);
- assertNotNull(action, "action file should be written for the matched
process");
- assertEquals("bean", action.getString("action"));
+ assertThat(action.getString("filter")).isEqualTo("order*");
+ assertThat(action.getBoolean("nulls")).isFalse();
+ assertThat(action.getBoolean("internal")).isTrue();
+ }
+
+ @Test
+ void dslShowsTheConfigurationOfEachProperty() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+ // without --nulls the configuration has only the properties that are
configured
+ JsonObject bean = bean("orders", "com.foo.OrderService",
+ property("retries", "java.lang.Integer", 3), property("url",
"java.lang.String", "http://x"));
+ bean.put("modelProperties", new JsonArray(List.of(property("url",
null, "{{orders.url}}"))));
+ JsonObject answer = new JsonObject();
+ answer.put("bean-models", new JsonObject(Map.of("orders", bean)));
+
+ assertThat(callWithResponse(command("--dsl", "--nulls=false"),
answer)).isZero();
+
+ assertThat(printer.getOutput()).contains("BEAN: orders
(com.foo.OrderService):", "CONFIGURATION");
+
assertThat(lineOf("retries")).contains("3").doesNotContain("{{orders.url}}");
+ assertThat(lineOf("url")).contains("{{orders.url}}", "http://x");
+ }
+
+ @Test
+ void jsonOfTheBeans() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+ JsonObject bean = bean("orders", "com.foo.OrderService",
property("url", "java.lang.String", "http://x"));
+ bean.put("modelProperties", new JsonArray(List.of(property("url",
null, "{{orders.url}}"))));
+ JsonObject answer = new JsonObject();
+ answer.put("bean-models", new JsonObject(Map.of("orders", bean)));
+
+ assertThat(callWithResponse(command("--dsl", "--json"),
answer)).isZero();
+
+ JsonArray beans = Jsoner.deserialize(printer.getOutput(), (JsonArray)
null);
+ JsonObject orders = beans.getMap(0);
+ assertThat(orders.getString("name")).isEqualTo("orders");
+ assertThat(orders.getString("type")).isEqualTo("com.foo.OrderService");
+ List<JsonObject> properties = orders.getCollection("properties");
+ assertThat(properties.get(0).getString("name")).isEqualTo("url");
+ assertThat(properties.get(0).getString("value")).isEqualTo("http://x");
+
assertThat(properties.get(0).getString("configValue")).isEqualTo("{{orders.url}}");
+ }
+
+ @Test
+ void anAnswerWithoutBeansIsAFailure() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+ CamelBeanDump command = command();
- String out = printer.getOutput();
- assertTrue(out.contains("BEAN: myBean"), "should print the bean name,
was: " + out);
- assertTrue(out.contains("com.foo.MyBean"), "should print the bean
type, was: " + out);
- assertTrue(out.contains("greeting"), "should print the bean property
name, was: " + out);
- assertTrue(out.contains("helloWorld"), "should print the bean property
value, was: " + out);
+ assertThatThrownBy(() -> callWithResponse(command, new JsonObject()))
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.FAILURE))
+ .hasMessage("Bean data not available from the running
integration");
}
@Test
void testReturnsErrorWhenNameDoesNotMatch() throws Exception {
writeStatusFile(TEST_PID, "myApp");
+ CamelBeanDump command = new CamelBeanDump(new
CamelJBangMain().withPrinter(printer));
+ CommandLine.populateCommand(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()).isEmpty();
+ }
+ private CamelBeanDump command(String... args) {
CamelBeanDump command = new CamelBeanDump(new
CamelJBangMain().withPrinter(printer));
- command.name = "doesNotExist";
+ CommandLine.populateCommand(command, Stream.concat(Stream.of("myApp"),
Arrays.stream(args)).toArray(String[]::new));
+ return command;
+ }
- CliException e = assertThrows(CliException.class, () ->
callWithSingleProcess(command));
+ private String lineOf(String text) {
+ return Arrays.stream(printer.getOutput().split(System.lineSeparator()))
+ .filter(l -> l.strip().startsWith(text + "
")).findFirst().orElseThrow();
+ }
- 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 beans of a user, of Camel, of the JDK, of Spring and of Quarkus
+ private static JsonObject someBeans() {
+ return beans(
+ bean("orders", "com.foo.OrderService"),
+ bean("camel-tracer",
"org.apache.camel.impl.engine.DefaultTracer"),
+ bean("executor", "java.util.concurrent.ThreadPoolExecutor"),
+ bean("spring", "org.springframework.core.env.Environment"),
+ bean("quarkus", "io.quarkus.runtime.Application"));
}
- private static JsonObject singleBeanResponse() {
- JsonObject property = new JsonObject();
- property.put("name", "greeting");
- property.put("type", "java.lang.String");
- property.put("value", "helloWorld");
- JsonArray properties = new JsonArray();
- properties.add(property);
+ // as the bean dev console answers
+ private static JsonObject beans(JsonObject... beans) {
+ JsonObject map = new JsonObject();
+ for (JsonObject bean : beans) {
+ map.put(bean.getString("name"), bean);
+ }
+ JsonObject answer = new JsonObject();
+ answer.put("beans", map);
+ return answer;
+ }
+ private static JsonObject bean(String name, String type, JsonObject...
properties) {
JsonObject bean = new JsonObject();
- bean.put("name", "myBean");
- bean.put("type", "com.foo.MyBean");
- bean.put("properties", properties);
-
- JsonObject beans = new JsonObject();
- beans.put("myBean", bean);
+ bean.put("name", name);
+ bean.put("type", type);
+ if (properties.length > 0) {
+ bean.put("properties", new JsonArray(Arrays.asList(properties)));
+ }
+ return bean;
+ }
- JsonObject response = new JsonObject();
- response.put("beans", beans);
- return response;
+ private static JsonObject property(String name, String type, Object value)
{
+ JsonObject property = new JsonObject();
+ property.put("name", name);
+ if (type != null) {
+ property.put("type", type);
+ }
+ property.put("value", value);
+ return property;
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceTopTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceTopTest.java
index 39a30f6bb341..7f380c8abb6e 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceTopTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceTopTest.java
@@ -16,20 +16,28 @@
*/
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.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 top source: the slowest processors of an integration with their
source code, as the top dev console has them.
+ */
@ExtendWith(MockitoExtension.class)
class CamelSourceTopTest extends ActionCommandTestSupport {
@@ -37,55 +45,164 @@ class CamelSourceTopTest extends ActionCommandTestSupport {
void testRequestsTopProcessorsAndRendersRows() throws Exception {
writeStatusFile(TEST_PID, "myApp");
- CamelSourceTop command = new CamelSourceTop(new
CamelJBangMain().withPrinter(printer));
- command.name = "myApp";
+ assertThat(callWithResponse(command(), processors(log(),
to()))).isZero();
- int exit = callWithResponse(command, singleProcessorResponse());
+
assertThat(readActionFile(TEST_PID).getString("action")).isEqualTo("top-processors");
+ assertThat(printer.getOutput())
+ .contains("Route: orders\tSource: file:orders.yaml:12 Total: 5
Mean: 2 Max: 3 Min: 1 Last: 2");
+ }
- assertEquals(0, exit);
+ @Test
+ void theSlowestFirstWithTheirCode() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
- JsonObject action = readActionFile(TEST_PID);
- assertNotNull(action, "action file should be written for the matched
process");
- assertEquals("top-processors", action.getString("action"));
+ assertThat(callWithResponse(command(), processors(log(),
to()))).isZero();
String out = printer.getOutput();
- assertTrue(out.contains("Route: myRoute"), "should print the route id,
was: " + out);
- assertTrue(out.contains("Total: 5"), "should print the processor
statistics, was: " + out);
+ assertThat(out.indexOf("Mean: 30")).isLessThan(out.indexOf("Mean: 2
"));
+ // the line of the processor is marked, and the code is unescaped
+ assertThat(out).contains(" 13:" + " ".repeat(9) + "- to:", " 14:
-->" + " ".repeat(9) + "uri: \"kafka:out\"",
+ " 15:" + " ".repeat(13) + "id: to1");
+ }
+
+ @Test
+ void filterMeanShowsTheSlowerProcessors() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+
+ assertThat(callWithResponse(command("--filter-mean=10"),
processors(log(), to()))).isZero();
+
+ assertThat(printer.getOutput()).contains("Mean:
30").doesNotContain("Mean: 2 ");
+ }
+
+ @Test
+ void limitShowsTheSlowest() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+
+ // the integration lists the slowest first
+ assertThat(callWithResponse(command("--limit=1"), processors(to(),
log()))).isZero();
+
+ assertThat(printer.getOutput()).contains("Mean:
30").doesNotContain("Mean: 2 ");
+ }
+
+ @Test
+ void aProcessorWithoutExchangesYet() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+ JsonObject idle = processor("idle1", "file:orders.yaml:20", -1, -1,
List.of());
+
+ assertThat(callWithResponse(command(), processors(idle))).isZero();
+
+ assertThat(printer.getOutput()).contains("Total: 0 Mean: Max: 0 Min:
0 Last:");
+ }
+
+ @Test
+ void jsonHasTheNumbersAndTheCode() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+
+ assertThat(callWithResponse(command("--json"), processors(log(),
to()))).isZero();
+
+ JsonArray rows = Jsoner.deserialize(printer.getOutput(), (JsonArray)
null);
+ assertThat(rows).hasSize(2);
+ JsonObject to = rows.getMap(0);
+ assertThat(to.getString("id")).isEqualTo("to1");
+ assertThat(to.getString("routeId")).isEqualTo("orders");
+ assertThat(to.getString("location")).isEqualTo("file:orders.yaml:14");
+ // numbers, not text
+ assertThat(to.get("total")).isInstanceOf(Number.class);
+ assertThat(to.get("mean")).isInstanceOf(Number.class);
+ assertThat(to.getLong("total")).isEqualTo(5L);
+ assertThat(to.getLong("mean")).isEqualTo(30L);
+ assertThat(to.getLong("max")).isEqualTo(59L);
+ List<JsonObject> code = to.getCollection("code");
+ assertThat(code).hasSize(3);
+ assertThat(code.get(1).getBoolean("match")).isTrue();
+ assertThat(code.get(0).containsKey("match")).isFalse();
+ }
+
+ @Test
+ void jsonOfAProcessorWithoutExchangesYet() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+ JsonObject idle = processor("idle1", "file:orders.yaml:20", -1, -1,
List.of());
+
+ assertThat(callWithResponse(command("--json"),
processors(idle))).isZero();
+
+ JsonObject row = Jsoner.deserialize(printer.getOutput(), (JsonArray)
null).getMap(0);
+ assertThat(row.get("mean")).isNull();
+ assertThat(row.get("last")).isNull();
+ assertThat(row.getLong("total")).isZero();
+ }
+
+ @Test
+ void anAnswerWithoutProcessorsIsAFailure() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+ CamelSourceTop command = command();
+
+ assertThatThrownBy(() -> callWithResponse(command, new JsonObject()))
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.FAILURE))
+ .hasMessage("Source top data not available from the running
integration");
}
@Test
void testReturnsErrorWhenNameDoesNotMatch() throws Exception {
writeStatusFile(TEST_PID, "myApp");
+ CamelSourceTop command = new CamelSourceTop(new
CamelJBangMain().withPrinter(printer));
+ CommandLine.populateCommand(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()).isEmpty();
+ }
+
+ private CamelSourceTop command(String... args) {
CamelSourceTop command = new CamelSourceTop(new
CamelJBangMain().withPrinter(printer));
- command.name = "doesNotExist";
+ CommandLine.populateCommand(command, Stream.concat(Stream.of("myApp"),
Arrays.stream(args)).toArray(String[]::new));
+ return command;
+ }
- CliException e = assertThrows(CliException.class, () ->
callWithSingleProcess(command));
+ // as the top dev console answers
+ private static JsonObject processors(JsonObject... processors) {
+ JsonObject answer = new JsonObject();
+ answer.put("processors", new JsonArray(Arrays.asList(processors)));
+ 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 log() {
+ return processor("log1", "file:orders.yaml:12", 2, 2, List.of());
}
- private static JsonObject singleProcessorResponse() {
- JsonObject stats = new JsonObject();
- stats.put("exchangesTotal", 5);
- stats.put("meanProcessingTime", 2);
- stats.put("maxProcessingTime", 3);
- stats.put("minProcessingTime", 1);
- stats.put("lastProcessingTime", 2);
+ private static JsonObject to() {
+ return processor("to1", "file:orders.yaml:14", 30, 25, List.of(
+ line(13, " - to:", false),
+ line(14, " uri: \\\"kafka:out\\\"", true),
+ line(15, " id: to1", false)));
+ }
+ private static JsonObject processor(String id, String location, long mean,
long last, List<JsonObject> code) {
+ JsonObject stats = new JsonObject();
+ stats.put("exchangesTotal", mean < 0 ? 0 : 5);
+ stats.put("meanProcessingTime", mean);
+ stats.put("maxProcessingTime", Math.max(0, mean * 2 - 1));
+ stats.put("minProcessingTime", mean < 0 ? 0 : 1);
+ stats.put("lastProcessingTime", last);
JsonObject processor = new JsonObject();
- processor.put("processorId", "myProcessor");
- processor.put("routeId", "myRoute");
- processor.put("location", "myroute.yaml");
+ processor.put("routeId", "orders");
+ processor.put("processorId", id);
+ processor.put("location", location);
processor.put("statistics", stats);
+ if (!code.isEmpty()) {
+ processor.put("code", new JsonArray(code));
+ }
+ return processor;
+ }
- JsonArray processors = new JsonArray();
- processors.add(processor);
-
- JsonObject response = new JsonObject();
- response.put("processors", processors);
- return response;
+ // the code is escaped as JSON text
+ private static JsonObject line(int line, String code, boolean match) {
+ JsonObject jo = new JsonObject(Map.of("line", line, "code", code));
+ if (match) {
+ jo.put("match", true);
+ }
+ return jo;
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteControllerActionTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteControllerActionTest.java
index be05d8fcd2c5..ec84d0aefc10 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteControllerActionTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteControllerActionTest.java
@@ -16,20 +16,28 @@
*/
package org.apache.camel.dsl.jbang.core.commands.action;
+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.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 get route-controller: the route controller of an integration and its
routes, as the route-controller dev
+ * console has them: the default controller, or the supervising controller
with the routes it restarts.
+ */
@ExtendWith(MockitoExtension.class)
class RouteControllerActionTest extends ActionCommandTestSupport {
@@ -37,52 +45,213 @@ class RouteControllerActionTest extends
ActionCommandTestSupport {
void testRequestsRouteControllerAndRendersRoutes() throws Exception {
writeStatusFile(TEST_PID, "myApp");
- RouteControllerAction command = new RouteControllerAction(new
CamelJBangMain().withPrinter(printer));
- command.name = "myApp";
- // options are normally defaulted by picocli; set them as we construct
the command directly
- command.sort = "id";
- command.header = true;
+ assertThat(callWithResponse(command(), defaultController())).isZero();
- int exit = callWithResponse(command, defaultControllerResponse());
+ JsonObject action = readActionFile(TEST_PID);
+ assertThat(action.getString("action")).isEqualTo("route-controller");
+ // stack traces by default
+ assertThat(action.getString("stacktrace")).isEqualTo("true");
+ String out = printer.getOutput();
+ assertThat(out).contains("Default Route Controller", "Routes Total:
1", "myRoute", "timer://foo", "Started")
+ // the default controller does not tell whether its routes are
starting
+ .doesNotContain("Starting Routes", "ATTEMPT");
+ }
- assertEquals(0, exit);
+ @Test
+ void supervisingControllerWithTheRoutesItRestarts() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
- JsonObject action = readActionFile(TEST_PID);
- assertNotNull(action, "action file should be written for the matched
process");
- assertEquals("route-controller", action.getString("action"));
+ assertThat(callWithResponse(command(),
supervisingController())).isZero();
String out = printer.getOutput();
- assertTrue(out.contains("Default Route Controller"), "should print the
controller header, was: " + out);
- assertTrue(out.contains("myRoute"), "should print the route id, was: "
+ out);
+ assertThat(out).contains("Supervising Route Controller", "Initial
Starting Routes: false",
+ "Unhealthy Routes: true", "Routes Total: 4", "Routes Started:
1", "Routes Restarting: 2",
+ "Routes Exhausted: 1", "Backoff Delay: 2000", "Thread Pool
Size: 1");
+ String[] lines = table(out);
+ assertThat(lines[0]).contains("ID", "URI", "STATE", "ATTEMPT",
"ELAPSED", "LAST-AGO", "ERROR-MESSAGE");
+ // the routes by id; an active route is starting at its first attempt,
and restarting after
+ assertThat(lines[1]).contains("ftp", "Starting", " 1 ", "Cannot
login");
+ assertThat(lines[2]).contains("kafka", "Restarting", " 3 ", "Broker
down");
+ assertThat(lines[3]).contains("mail", "Exhausted", " 10 ", "No host");
+ assertThat(lines[4]).contains("timer", "Started");
}
@Test
- void testReturnsErrorWhenNameDoesNotMatch() throws Exception {
+ void stackTracesOfTheRoutesThatFail() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+
+ assertThat(callWithResponse(command(),
supervisingController())).isZero();
+
+ String out = printer.getOutput();
+ assertThat(out).contains("STACK-TRACE", "ID: kafka", "URI:
kafka://orders", "STATE: Restarting",
+ "java.io.IOException: Broker down", "at
org.example.Kafka.connect(Kafka.java:10)",
+ "at org.example.Kafka.start(Kafka.java:20)");
+ }
+
+ @Test
+ void depthLimitsTheStackTraces() throws Exception {
writeStatusFile(TEST_PID, "myApp");
+ assertThat(callWithResponse(command("--depth=2"),
supervisingController())).isZero();
+
+ assertThat(printer.getOutput()).contains("at
org.example.Kafka.connect(Kafka.java:10)")
+ .doesNotContain("at org.example.Kafka.start(Kafka.java:20)");
+ }
+
+ @Test
+ void withoutTraceTheStackTracesAreNotAskedFor() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+
+ assertThat(callWithResponse(command("--trace=false"),
supervisingController())).isZero();
+
+
assertThat(readActionFile(TEST_PID).getString("stacktrace")).isEqualTo("false");
+ assertThat(printer.getOutput()).contains("Broker
down").doesNotContain("STACK-TRACE");
+ }
+
+ @Test
+ void withoutHeader() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+
+ assertThat(callWithResponse(command("--header=false"),
supervisingController())).isZero();
+
+ assertThat(printer.getOutput()).doesNotContain("Supervising Route
Controller", "Backoff Delay")
+ .contains("kafka", "Restarting");
+ }
+
+ @Test
+ void backoffTimesLongerThanAnInt() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+ JsonObject answer = supervisingController();
+ // 30 days
+ answer.put("backoffMaxElapsedTime", 2_592_000_000L);
+ answer.put("backoffMaxAttempts", Long.MAX_VALUE);
+
+ assertThat(callWithResponse(command(), answer)).isZero();
+
+ assertThat(printer.getOutput()).contains("Backoff Max Elapsed Time:
2592000000",
+ "Backoff Max Attempts: " + Long.MAX_VALUE);
+ }
+
+ @Test
+ void sortByState() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+
+ assertThat(callWithResponse(command("--sort=state", "--trace=false"),
supervisingController())).isZero();
+
+ // by the status of the routes
+ String[] lines = table(printer.getOutput());
+ assertThat(lines[1]).contains("timer");
+ }
+
+ @Test
+ void jsonIsTheAnswerOfTheIntegration() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+
+ assertThat(callWithResponse(command("--json"),
supervisingController())).isZero();
+
+ JsonObject json = Jsoner.deserialize(printer.getOutput(), (JsonObject)
null);
+
assertThat(json.getString("controller")).isEqualTo("SupervisingRouteController");
+ JsonArray routes = (JsonArray) json.get("routes");
+ assertThat(routes).hasSize(4);
+ }
+
+ @Test
+ void anAnswerWithoutRoutesIsAFailure() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+ RouteControllerAction command = command();
+
+ assertThatThrownBy(() -> callWithResponse(command, new JsonObject()))
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.FAILURE))
+ .hasMessage("Route controller data not available from the
running integration");
+ }
+
+ @Test
+ void testReturnsErrorWhenNameDoesNotMatch() throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
RouteControllerAction command = new RouteControllerAction(new
CamelJBangMain().withPrinter(printer));
- command.name = "doesNotExist";
+ CommandLine.populateCommand(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()).isEmpty();
+ }
- CliException e = assertThrows(CliException.class, () ->
callWithSingleProcess(command));
+ private RouteControllerAction command(String... args) {
+ RouteControllerAction command = new RouteControllerAction(new
CamelJBangMain().withPrinter(printer));
+ CommandLine.populateCommand(command, Stream.concat(Stream.of("myApp"),
Arrays.stream(args)).toArray(String[]::new));
+ return command;
+ }
- 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 lines of the table of the routes, from its header
+ private static String[] table(String out) {
+ String table = out.substring(out.indexOf("ID "));
+ int end = table.indexOf("---");
+ return (end > 0 ? table.substring(0, end) :
table).strip().split(System.lineSeparator());
}
- private static JsonObject defaultControllerResponse() {
+ // as the route-controller dev console answers
+ private static JsonObject defaultController() {
JsonObject route = new JsonObject();
route.put("routeId", "myRoute");
route.put("uri", "timer://foo");
route.put("status", "Started");
- JsonArray routes = new JsonArray();
- routes.add(route);
-
- JsonObject response = new JsonObject();
- response.put("controller", "DefaultRouteController");
- response.put("startingRoutes", false);
- response.put("totalRoutes", 1);
- response.put("routes", routes);
- return response;
+ JsonObject answer = new JsonObject();
+ answer.put("controller", "DefaultRouteController");
+ answer.put("totalRoutes", 1);
+ answer.put("routes", new JsonArray(List.of(route)));
+ return answer;
+ }
+
+ private static JsonObject supervisingController() {
+ long now = System.currentTimeMillis();
+ JsonObject answer = new JsonObject();
+ answer.put("controller", "SupervisingRouteController");
+ answer.put("startingRoutes", false);
+ answer.put("unhealthyRoutes", true);
+ answer.put("totalRoutes", 4);
+ answer.put("startedRoutes", 1L);
+ answer.put("restartingRoutes", 2);
+ answer.put("exhaustedRoutes", 1);
+ answer.put("initialDelay", 0L);
+ answer.put("backoffDelay", 2000L);
+ answer.put("backoffMaxDelay", 0L);
+ answer.put("backoffMaxElapsedTime", 0L);
+ answer.put("backoffMaxAttempts", 10L);
+ answer.put("threadPoolSize", 1);
+ answer.put("unhealthyOnRestarting", true);
+ answer.put("unhealthyOnExhausted", true);
+ answer.put("routes", new JsonArray(
+ List.of(
+ route("timer", "timer://tick", "Started", null, 0, 0,
null),
+ route("kafka", "kafka://orders", "Stopped", "Active",
3, now - 5000, "Broker down"),
+ route("ftp", "ftp://host", "Stopped", "Active", 1, now
- 1000, "Cannot login"),
+ route("mail", "smtp://host", "Stopped", "Exhausted",
10, 0, "No host"))));
+ return answer;
+ }
+
+ private static JsonObject route(
+ String routeId, String uri, String status, String supervising,
long attempts, long lastAttempt,
+ String error) {
+ JsonObject route = new JsonObject();
+ route.put("routeId", routeId);
+ route.put("uri", uri);
+ route.put("status", status);
+ route.put("attempts", attempts);
+ route.put("lastAttempt", lastAttempt);
+ route.put("nextAttempt", 0L);
+ route.put("elapsed", lastAttempt > 0 ? 7000L : 0L);
+ if (supervising != null) {
+ route.put("supervising", supervising);
+ }
+ if (error != null) {
+ route.put("error", error);
+ route.put("stackTrace", new JsonArray(
+ List.of("java.io.IOException: " + error,
+ "\tat org.example.Kafka.connect(Kafka.java:10)",
"\tat org.example.Kafka.start(Kafka.java:20)")));
+ }
+ return route;
}
}