This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-23688-actions-2 in repository https://gitbox.apache.org/repos/asf/camel.git
commit a8f2e01d36effd7eae01333679be7f4db8bd393b Author: Claus Ibsen <[email protected]> AuthorDate: Fri Oct 16 23:01:00 2026 +0200 CAMEL-23688: camel-jbang - Tests for get bean, get route-controller and top source 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]> Signed-off-by: Claus Ibsen <[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 b332441dfd88..4bcbd57be301 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 @@ -521,6 +521,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; } }
