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 80248a3a9819 CAMEL-25389: camel-jbang - route-diagram, route-structure 
and route-topology print the parser notes on stderr (#27913)
80248a3a9819 is described below

commit 80248a3a9819e8f44b69e4b9c68fae55bf84ebde
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 15 23:26:42 2026 +0200

    CAMEL-25389: camel-jbang - route-diagram, route-structure and 
route-topology print the parser notes on stderr (#27913)
    
    The parts of a Java route the Java DSL parser could not read are a warning 
on
    stderr, also with --json (which still lists them in "unread"), and a source
    file of these commands that does not exist exits with 3.
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  3 +++
 .../core/commands/action/ActionBaseCommand.java    | 26 +++++++++-------------
 .../commands/action/CamelRouteStructureAction.java |  9 ++------
 .../commands/action/CamelRouteTopologyAction.java  |  5 -----
 .../CamelRouteDiagramActionDispatchTest.java       | 20 ++++++++---------
 .../CamelRouteDiagramActionMultiFileTest.java      | 12 ++++++----
 .../action/CamelRouteTopologyActionTest.java       | 19 ++++++++--------
 .../commands/action/RouteActionJavaSourceTest.java | 20 ++++++++++++-----
 8 files changed, 57 insertions(+), 57 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 2cf91c597b2e..da9774f9f335 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
@@ -213,6 +213,9 @@ A script that checks the exit code of a command must take 
these changes into acc
 * `camel transform dataweave` exits with `2` without `--input` or 
`--expression`, and with `3` when the input path does
   not exist or holds no `.dwl` files, instead of `1`.
 * `camel cmd sql` exits with `124` instead of `1` when the query does not 
answer in time.
+* A source file of `camel cmd route-diagram`, `camel cmd route-structure` and 
`camel cmd route-topology` that does not
+  exist exits with `3` instead of `1`. The parts of a Java route the parser 
could not read are printed as a warning on
+  stderr (also with `--json`, which lists them in `unread` as before).
 * A file or a name that does not exist exits with `3` instead of `1`: a source 
file or `--source-dir` of `camel run`, the
   route file of `camel explain` and `camel harden` (checked before the LLM is 
contacted), the `pom.xml` of
   `camel dependency runtime`, the Kamelet of `camel init --from-kamelet`, the 
service of `camel infra run`, the key of
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
index 029e2982f3c6..8adf0526fccd 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
@@ -30,6 +30,7 @@ import 
org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.Run;
 import org.apache.camel.dsl.jbang.core.commands.TransformJavaRoutes;
 import org.apache.camel.dsl.jbang.core.common.CamelJBangConstants;
+import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.PathUtils;
 import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
@@ -205,8 +206,9 @@ abstract class ActionBaseCommand extends CamelCommand {
      * @param  workDirPrefix      prefix for the (pid-suffixed) temporary work 
directory name
      * @param  ignoreLoadingError whether to ignore route loading and 
compilation errors
      * @param  compile            whether to compile Java sources and boot 
them, instead of reading them with the parser
-     * @return                    the dump location and the instant the dump 
started, or {@code null} if a file does not
-     *                            exist or the dump failed (an error is 
already printed to the user in that case)
+     * @return                    the dump location and the instant the dump 
started, or {@code null} if the dump failed
+     *                            (an error is already printed to the user in 
that case)
+     * @throws CliException       when a source file does not exist (exit code 
3)
      */
     protected SourceDump dumpRoutesFromSource(
             List<String> sourceFiles, String workDirPrefix, boolean 
ignoreLoadingError, boolean compile)
@@ -214,8 +216,7 @@ abstract class ActionBaseCommand extends CamelCommand {
         for (String name : sourceFiles) {
             File f = new File(name);
             if (!f.isFile() || !f.exists()) {
-                printer().printErr("File does not exist: " + name);
-                return null;
+                throw CliException.notFound("File does not exist: " + name);
             }
         }
 
@@ -269,22 +270,15 @@ abstract class ActionBaseCommand extends CamelCommand {
             return false;
         }
         unreadSourceParts = result.unread();
-        if (!unreadSourceParts.isEmpty() && printSourceNotes()) {
-            printer().println("Routes read without compiling them; the parser 
could not work out these parts "
-                              + "(use --compile to compile the source 
instead):");
-            unreadSourceParts.forEach(u -> printer().println("  " + u));
+        if (!unreadSourceParts.isEmpty()) {
+            // a warning on stderr: the JSON output stays one document, and 
lists them too (see addUnreadSourceParts)
+            printer().printWarn("Routes read without compiling them; the 
parser could not work out these parts "
+                                + "(use --compile to compile the source 
instead):");
+            unreadSourceParts.forEach(u -> printer().printDiagnostic("  " + 
u));
         }
         return true;
     }
 
-    /**
-     * Whether to print the parts of Java sources the parser could not read: 
not when the output is JSON, which lists
-     * them instead (see {@link #addUnreadSourceParts(JsonObject)}).
-     */
-    protected boolean printSourceNotes() {
-        return true;
-    }
-
     /**
      * Adds the parts of Java sources the parser could not read to the JSON 
output, as an {@code unread} array, so a
      * tool reading the JSON can tell that routes may be missing from it.
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
index 08a5ef1cfad9..ad7f9701ac20 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
@@ -28,6 +28,7 @@ import 
org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.Run;
 import org.apache.camel.dsl.jbang.core.commands.TransformJavaRoutes;
 import org.apache.camel.dsl.jbang.core.common.CamelJBangConstants;
+import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.PathUtils;
 import org.apache.camel.main.KameletMain;
@@ -226,8 +227,7 @@ public class CamelRouteStructureAction extends 
ActionBaseCommand {
     private int doCallSource(String name) throws Exception {
         File f = new File(name);
         if (!f.isFile() || !f.exists()) {
-            printer().printErr("File does not exist: " + name);
-            return 1;
+            throw CliException.notFound("File does not exist: " + name);
         }
 
         final String target = CommandLineHelper.CAMEL_JBANG_WORK_DIR + 
"/structure-output.json";
@@ -348,9 +348,4 @@ public class CamelRouteStructureAction extends 
ActionBaseCommand {
         String code;
         String description;
     }
-
-    @Override
-    protected boolean printSourceNotes() {
-        return !jsonOutput;
-    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyAction.java
index 281e23e578f0..c6c040c2c2d4 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyAction.java
@@ -374,9 +374,4 @@ public class CamelRouteTopologyAction extends 
ActionBaseCommand {
             cmd.files.add(arg);
         }
     }
-
-    @Override
-    protected boolean printSourceNotes() {
-        return !jsonOutput;
-    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramActionDispatchTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramActionDispatchTest.java
index a278b47721a7..f312e8d8bc73 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramActionDispatchTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramActionDispatchTest.java
@@ -20,13 +20,15 @@ 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.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.MockedStatic;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.Mockito.mockStatic;
 
 /**
@@ -46,11 +48,10 @@ class CamelRouteDiagramActionDispatchTest extends 
ActionCommandTestSupport {
         // "doesNotExist" matches no running process, so it is tried as a 
source file next
         command.files = List.of("doesNotExist");
 
-        int exit = callWatchWithSingleProcess(command);
+        CliException e = assertThrows(CliException.class, () -> 
callWatchWithSingleProcess(command));
 
-        assertEquals(1, exit);
-        assertTrue(printer.getOutput().contains("File does not exist: 
doesNotExist"),
-                "should report the missing source file, was: " + 
printer.getOutput());
+        assertEquals(CliExitCode.NOT_FOUND, e.getExitCode());
+        assertEquals("File does not exist: doesNotExist", e.getMessage(), 
"should report the missing source file");
     }
 
     @Test
@@ -63,13 +64,12 @@ class CamelRouteDiagramActionDispatchTest extends 
ActionCommandTestSupport {
         CamelRouteDiagramAction command = new CamelRouteDiagramAction(new 
CamelJBangMain().withPrinter(printer));
         command.files = List.of("route1", "route2");
 
-        int exit = command.doWatchCall();
+        CliException e = assertThrows(CliException.class, 
command::doWatchCall);
 
-        assertEquals(1, exit);
-        assertTrue(printer.getOutput().contains("File does not exist: route1"),
+        assertEquals(CliExitCode.NOT_FOUND, e.getExitCode());
+        assertEquals("File does not exist: route1", e.getMessage(),
                 "with 2+ file args, dispatch must go straight to source-file 
handling and skip the "
-                                                                               
 + "running-integration lookup entirely, was: "
-                                                                               
 + printer.getOutput());
+                                                                    + 
"running-integration lookup entirely");
     }
 
     private static int callWatchWithSingleProcess(CamelRouteDiagramAction 
command) throws Exception {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramActionMultiFileTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramActionMultiFileTest.java
index 73c32e46f1fe..b510c6d381ed 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramActionMultiFileTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramActionMultiFileTest.java
@@ -26,12 +26,15 @@ import java.util.List;
 
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
 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.StringPrinter;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
@@ -52,11 +55,12 @@ class CamelRouteDiagramActionMultiFileTest {
         StringPrinter printer = new StringPrinter();
         CamelRouteDiagramAction command = new CamelRouteDiagramAction(new 
CamelJBangMain().withPrinter(printer));
 
-        List<RouteInfo> routes = 
command.doCallSource(List.of(existing.toString(), "does-not-exist.yaml"));
+        // the missing file ends the command (exit code 3) before Camel is 
booted
+        CliException e = assertThrows(CliException.class,
+                () -> command.doCallSource(List.of(existing.toString(), 
"does-not-exist.yaml")));
 
-        assertNull(routes, "should report an error and not attempt to boot 
Camel when a file is missing");
-        assertTrue(printer.getOutput().contains("File does not exist: 
does-not-exist.yaml"),
-                "should report the missing file, was: " + printer.getOutput());
+        assertEquals(CliExitCode.NOT_FOUND, e.getExitCode());
+        assertEquals("File does not exist: does-not-exist.yaml", 
e.getMessage(), "should report the missing file");
     }
 
     @Test
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyActionTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyActionTest.java
index 85fd4701d1c7..fd8b22c689fc 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyActionTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyActionTest.java
@@ -19,6 +19,8 @@ package org.apache.camel.dsl.jbang.core.commands.action;
 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.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.Test;
@@ -27,6 +29,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
 
 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;
 
 @ExtendWith(MockitoExtension.class)
@@ -74,11 +77,10 @@ class CamelRouteTopologyActionTest extends 
ActionCommandTestSupport {
         // "doesNotExist" matches no running process, so it is tried as a 
source file next
         command.files = List.of("doesNotExist");
 
-        int exit = callWithSingleProcess(command);
+        CliException e = assertThrows(CliException.class, () -> 
callWithSingleProcess(command));
 
-        assertEquals(1, exit);
-        assertTrue(printer.getOutput().contains("File does not exist: 
doesNotExist"),
-                "should report the missing source file, was: " + 
printer.getOutput());
+        assertEquals(CliExitCode.NOT_FOUND, e.getExitCode());
+        assertEquals("File does not exist: doesNotExist", e.getMessage(), 
"should report the missing source file");
     }
 
     @Test
@@ -91,13 +93,12 @@ class CamelRouteTopologyActionTest extends 
ActionCommandTestSupport {
         CamelRouteTopologyAction command = new CamelRouteTopologyAction(new 
CamelJBangMain().withPrinter(printer));
         command.files = List.of("route1", "route2");
 
-        int exit = command.doCall();
+        CliException e = assertThrows(CliException.class, command::doCall);
 
-        assertEquals(1, exit);
-        assertTrue(printer.getOutput().contains("File does not exist: route1"),
+        assertEquals(CliExitCode.NOT_FOUND, e.getExitCode());
+        assertEquals("File does not exist: route1", e.getMessage(),
                 "with 2+ file args, dispatch must go straight to source-file 
handling and skip the "
-                                                                               
 + "running-integration lookup entirely, was: "
-                                                                               
 + printer.getOutput());
+                                                                    + 
"running-integration lookup entirely");
     }
 
     // Note: the actual doCallSource -> Run.runTransform spawn path 
(successfully rendering topology from a real
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteActionJavaSourceTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteActionJavaSourceTest.java
index 40969d254666..fc7d6eb95deb 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteActionJavaSourceTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteActionJavaSourceTest.java
@@ -22,6 +22,8 @@ import java.util.List;
 
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
 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.StringPrinter;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -30,6 +32,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 /**
  * route-diagram, route-topology and route-structure read Java source files 
with the Java DSL parser (CAMEL-25565): the
@@ -93,8 +96,9 @@ class RouteActionJavaSourceTest {
         RouteInfo intake = routes.stream().filter(r -> 
"intake".equals(r.routeId)).findFirst().orElseThrow();
         // the lambda is a placeholder step, so the route keeps all its steps
         assertThat(intake.nodes).extracting(n -> n.type).contains("from", 
"setBody", "process", "to");
-        assertThat(printer.getOutput())
-                .contains("Routes read without compiling them")
+        // a warning on stderr, not part of the output
+        assertThat(printer.getStderr())
+                .contains("WARN: Routes read without compiling them")
                 .contains("Orders.java:")
                 .contains("a lambda or method reference");
     }
@@ -107,8 +111,10 @@ class RouteActionJavaSourceTest {
         command.compile = true;
 
         // the existence check comes first, also when compiling
-        assertThat(command.doCallSource(List.of(missing.toString()))).isNull();
-        assertThat(printer.getOutput()).contains("File does not exist: " + 
missing);
+        assertThatThrownBy(() -> 
command.doCallSource(List.of(missing.toString())))
+                .isInstanceOf(CliException.class)
+                .hasMessage("File does not exist: " + missing)
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
     }
 
     @Test
@@ -142,8 +148,10 @@ class RouteActionJavaSourceTest {
         int exit = command.doCall();
 
         assertThat(exit).isZero();
-        // with --json only the JSON is printed, and the parts the parser 
could not read are listed in it
-        JsonObject topology = (JsonObject) 
Jsoner.deserialize(printer.getOutput());
+        // with --json only the JSON is on stdout, and the parts the parser 
could not read are listed in it (and on
+        // stderr, as a warning)
+        JsonObject topology = (JsonObject) 
Jsoner.deserialize(printer.getStdout());
+        assertThat(printer.getStderr()).contains("WARN: Routes read without 
compiling them");
         JsonArray edges = (JsonArray) topology.get("edges");
         assertThat(edges).hasSize(1);
         JsonObject edge = (JsonObject) edges.get(0);

Reply via email to