This is an automated email from the ASF dual-hosted git repository.

jamesnetherton pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git


The following commit(s) were added to refs/heads/main by this push:
     new ce88cc3987 Add route diagram dumping to @CamelQuarkusTest
ce88cc3987 is described below

commit ce88cc3987c1c89535b96b24badf6d69fc1e99e1
Author: James Netherton <[email protected]>
AuthorDate: Thu Oct 22 07:16:33 2026 +0100

    Add route diagram dumping to @CamelQuarkusTest
    
    Ref #8640
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../user-guide/testing/camel-quarkus-test.adoc     | 18 ++++++-
 .../camel-quarkus-junit-annotation-tests/pom.xml   |  4 ++
 .../test/annotation/DumpRouteDiagramTest.java      | 62 ++++++++++++++++++++++
 .../camel/quarkus/test/CamelQuarkusTest.java       | 16 ++++++
 .../test/internal/CamelQuarkusTestCallbacks.java   | 42 +++++++++++++--
 .../test/internal/CamelQuarkusTestInspector.java   | 13 +++++
 .../internal/CamelQuarkusTestInspectorTest.java    | 10 ++--
 7 files changed, 155 insertions(+), 10 deletions(-)

diff --git a/docs/modules/ROOT/pages/user-guide/testing/camel-quarkus-test.adoc 
b/docs/modules/ROOT/pages/user-guide/testing/camel-quarkus-test.adoc
index 5ad74966b0..1d18b9c4c0 100644
--- a/docs/modules/ROOT/pages/user-guide/testing/camel-quarkus-test.adoc
+++ b/docs/modules/ROOT/pages/user-guide/testing/camel-quarkus-test.adoc
@@ -151,14 +151,14 @@ Using a feature that is only available in isolated mode 
fails the test build.
 | No
 | Yes
 
-| `dumpRouteCoverage`, `dumpRoute`, `shutdownTimeout`
+| `dumpRouteCoverage`, `dumpRoute`, `dumpRouteDiagramFolder`, `shutdownTimeout`
 | Yes
 | Yes
 |===
 
 In isolated mode, `advices` and `replaceRouteFromWith` are applied before 
routes are created.
 
-== Debugging, route coverage and route dumps
+== Debugging, route coverage, route dumps and route diagrams
 
 A `public static` no-arg method annotated with `@ProvidesBreakpoint` and 
returning `org.apache.camel.spi.Breakpoint` enables the Camel debugger with 
that breakpoint.
 
@@ -167,6 +167,20 @@ A `public static` no-arg method annotated with 
`@ProvidesBreakpoint` and returni
 `dumpRoute` writes the routes to `target/camel-route-dump` after each test 
method, in `xml` or `yaml` format.
 It requires `camel-quarkus-xml-io-dsl` for `xml`, and `camel-quarkus-yaml-io` 
for `yaml`.
 
+`dumpRouteDiagramFolder` writes PNG route diagrams to the given folder before 
the first test method. The diagrams include the routes and advice declared by 
the test class.
+The route topology diagram is also written, unless `dumpRouteDiagramTopology = 
false`. `dumpRouteDiagramTopologyExternal = false` leaves external systems out 
of it.
+It requires `camel-quarkus-diagram`.
+
+[source,java]
+----
+@CamelQuarkusTest(dumpRouteDiagramFolder = "src/docs/diagrams")
+class RouteDiagramTest {
+    @Test
+    void dumpRouteDiagrams() {
+    }
+}
+----
+
 == Continuous testing
 
 Continuous testing only detects test classes annotated with `@QuarkusTest`, or 
with annotations declared in the test sources that are meta-annotated with 
`@QuarkusTest`.
diff --git a/test-framework/camel-quarkus-junit-annotation-tests/pom.xml 
b/test-framework/camel-quarkus-junit-annotation-tests/pom.xml
index e53064b0f0..a8de0a5b1b 100644
--- a/test-framework/camel-quarkus-junit-annotation-tests/pom.xml
+++ b/test-framework/camel-quarkus-junit-annotation-tests/pom.xml
@@ -30,6 +30,10 @@
     <name>Camel Quarkus :: Test Framework :: Junit :: Annotation Tests</name>
 
     <dependencies>
+        <dependency>
+            <groupId>org.apache.camel.quarkus</groupId>
+            <artifactId>camel-quarkus-diagram</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.apache.camel.quarkus</groupId>
             <artifactId>camel-quarkus-direct</artifactId>
diff --git 
a/test-framework/camel-quarkus-junit-annotation-tests/src/test/java/org/apache/camel/quarkus/test/annotation/DumpRouteDiagramTest.java
 
b/test-framework/camel-quarkus-junit-annotation-tests/src/test/java/org/apache/camel/quarkus/test/annotation/DumpRouteDiagramTest.java
new file mode 100644
index 0000000000..5d67bd9853
--- /dev/null
+++ 
b/test-framework/camel-quarkus-junit-annotation-tests/src/test/java/org/apache/camel/quarkus/test/annotation/DumpRouteDiagramTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.test.annotation;
+
+import java.io.File;
+import java.nio.file.Path;
+
+import jakarta.inject.Inject;
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.quarkus.test.CamelQuarkusTest;
+import org.apache.camel.util.FileUtil;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@CamelQuarkusTest(routes = DumpRouteDiagramTest.Routes.class, 
dumpRouteDiagramFolder = DumpRouteDiagramTest.FOLDER)
+class DumpRouteDiagramTest {
+    static final String FOLDER = "target/camel-route-diagram-annotation";
+    static final String ROUTE_ID = "annotation-diagram";
+
+    @Inject
+    CamelContext context;
+
+    @BeforeAll
+    static void deleteDiagramsOfPreviousRuns() {
+        // Runs before the test instance is created, which is when the 
diagrams are dumped
+        FileUtil.removeDir(new File(FOLDER));
+    }
+
+    @Test
+    void diagramsAreDumped() {
+        Path folder = Path.of(FOLDER);
+        assertThat(folder.resolve(AnnotationApplicationRoutes.ROUTE_ID + 
".png")).isNotEmptyFile();
+        assertThat(folder.resolve(ROUTE_ID + ".png")).isNotEmptyFile();
+        assertThat(folder.resolve(context.getName() + 
"-topology.png")).isNotEmptyFile();
+    }
+
+    public static class Routes extends RouteBuilder {
+        @Override
+        public void configure() {
+            from("direct:annotationDiagram")
+                    .routeId(ROUTE_ID)
+                    .to("direct:annotationApp");
+        }
+    }
+}
diff --git 
a/test-framework/camel-quarkus-junit/src/main/java/org/apache/camel/quarkus/test/CamelQuarkusTest.java
 
b/test-framework/camel-quarkus-junit/src/main/java/org/apache/camel/quarkus/test/CamelQuarkusTest.java
index b4421daa6b..8febcd602c 100644
--- 
a/test-framework/camel-quarkus-junit/src/main/java/org/apache/camel/quarkus/test/CamelQuarkusTest.java
+++ 
b/test-framework/camel-quarkus-junit/src/main/java/org/apache/camel/quarkus/test/CamelQuarkusTest.java
@@ -96,6 +96,22 @@ public @interface CamelQuarkusTest {
      */
     String dumpRoute() default "";
 
+    /**
+     * Folder to dump PNG route diagrams to before the first test method, 
including the routes and advice of the test
+     * class. The diagrams are not dumped when the folder is empty. Requires 
{@code camel-quarkus-diagram}.
+     */
+    String dumpRouteDiagramFolder() default "";
+
+    /**
+     * Whether to also dump the route topology diagram, showing how routes 
connect to each other.
+     */
+    boolean dumpRouteDiagramTopology() default true;
+
+    /**
+     * Whether to include external systems, such as Kafka or HTTP endpoints, 
in the route topology diagram.
+     */
+    boolean dumpRouteDiagramTopologyExternal() default true;
+
     /**
      * Timeout in seconds for graceful shutdown of routes.
      */
diff --git 
a/test-framework/camel-quarkus-junit/src/main/java/org/apache/camel/quarkus/test/internal/CamelQuarkusTestCallbacks.java
 
b/test-framework/camel-quarkus-junit/src/main/java/org/apache/camel/quarkus/test/internal/CamelQuarkusTestCallbacks.java
index 90beda1424..a1d1800c98 100644
--- 
a/test-framework/camel-quarkus-junit/src/main/java/org/apache/camel/quarkus/test/internal/CamelQuarkusTestCallbacks.java
+++ 
b/test-framework/camel-quarkus-junit/src/main/java/org/apache/camel/quarkus/test/internal/CamelQuarkusTestCallbacks.java
@@ -16,6 +16,9 @@
  */
 package org.apache.camel.quarkus.test.internal;
 
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -35,15 +38,20 @@ import org.apache.camel.component.mock.MockEndpoint;
 import org.apache.camel.model.ModelCamelContext;
 import org.apache.camel.quarkus.test.CamelQuarkusTest;
 import org.apache.camel.quarkus.test.CamelTestHelper;
+import org.apache.camel.spi.RouteDiagramDumper;
+import org.apache.camel.support.PluginHelper;
 import org.apache.camel.test.junit6.util.ExtensionHelper;
 import org.apache.camel.test.junit6.util.RouteCoverageDumperExtension;
 import org.apache.camel.test.junit6.util.RouteDumperExtension;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Applies the {@link CamelQuarkusTest} plan around the test classes and 
methods that use it.
  */
 public class CamelQuarkusTestCallbacks implements 
QuarkusTestAfterConstructCallback, QuarkusTestBeforeEachCallback,
         QuarkusTestAfterEachCallback, QuarkusTestAfterAllCallback {
+    private static final Logger LOG = 
LoggerFactory.getLogger(CamelQuarkusTestCallbacks.class);
 
     @Override
     public void afterConstruct(Object testInstance) {
@@ -58,8 +66,9 @@ public class CamelQuarkusTestCallbacks implements 
QuarkusTestAfterConstructCallb
             // The plan is applied once per class, so every test of the class 
fails for the same reason
             throw state.classPlanFailure;
         }
-        boolean applyClassPlan = !state.classPlanApplied && !state.isolated;
-        if (!state.classPlanApplied) {
+        boolean firstInstance = !state.classPlanApplied;
+        boolean applyClassPlan = firstInstance && !state.isolated;
+        if (firstInstance) {
             state.classPlanApplied = true;
             state.existingComponents = new 
HashSet<>(context.getComponentNames());
             if (!state.isolated) {
@@ -70,9 +79,13 @@ public class CamelQuarkusTestCallbacks implements 
QuarkusTestAfterConstructCallb
         }
         // Bound before the routes of the test class are added, so that they 
can refer to the beans
         TestPlanSupport.replaceInRegistry(context, state, testInstance);
-        if (applyClassPlan) {
+        if (firstInstance) {
             try {
-                TestPlanSupport.unchecked(() -> 
TestPlanSupport.applyRoutesAndAdvice(context, state));
+                if (applyClassPlan) {
+                    TestPlanSupport.unchecked(() -> 
TestPlanSupport.applyRoutesAndAdvice(context, state));
+                }
+                // After the routes and advice of the test class are applied, 
so that the diagrams include them
+                dumpRouteDiagrams(context, state.annotation);
             } catch (RuntimeException e) {
                 state.classPlanFailure = e;
                 try {
@@ -168,6 +181,27 @@ public class CamelQuarkusTestCallbacks implements 
QuarkusTestAfterConstructCallb
         }
     }
 
+    private static void dumpRouteDiagrams(CamelContext context, 
CamelQuarkusTest annotation) {
+        String folder = annotation.dumpRouteDiagramFolder();
+        if (folder.isBlank()) {
+            return;
+        }
+
+        // Not via DumpRoutesStrategy, whose output options are shared with 
the application and the other test classes
+        RouteDiagramDumper dumper = 
PluginHelper.getRouteDiagramDumper(context);
+        File output = new File(folder);
+        LOG.info("Dumping route diagrams to: {}", output);
+        try {
+            dumper.dumpRoutesToFolder("*", RouteDiagramDumper.Theme.LIGHT, 
output);
+            if (annotation.dumpRouteDiagramTopology()) {
+                dumper.dumpTopologyToFolder(RouteDiagramDumper.Theme.LIGHT, 
annotation.dumpRouteDiagramTopologyExternal(),
+                        output);
+            }
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
     private static Class<?> findPlanOwner(Class<?> testClass) {
         if (Arc.container() == null) {
             // The application does not run in the JVM of the test, which is 
the case for @QuarkusIntegrationTest
diff --git 
a/test-framework/camel-quarkus-junit/src/main/java/org/apache/camel/quarkus/test/internal/CamelQuarkusTestInspector.java
 
b/test-framework/camel-quarkus-junit/src/main/java/org/apache/camel/quarkus/test/internal/CamelQuarkusTestInspector.java
index 12eb0a07b1..f1011593a4 100644
--- 
a/test-framework/camel-quarkus-junit/src/main/java/org/apache/camel/quarkus/test/internal/CamelQuarkusTestInspector.java
+++ 
b/test-framework/camel-quarkus-junit/src/main/java/org/apache/camel/quarkus/test/internal/CamelQuarkusTestInspector.java
@@ -62,6 +62,7 @@ public final class CamelQuarkusTestInspector {
     static final String XML_IO_DSL_MARKER = 
"org.apache.camel.quarkus.dsl.xml.XmlIoDslRecorder";
     static final String XML_JAXB_MARKER = 
"org.apache.camel.quarkus.component.xml.jaxb.XmlJaxbRecorder";
     static final String YAML_IO_MARKER = 
"org.apache.camel.quarkus.yaml.io.YamlIORecorder";
+    static final String DIAGRAM_MARKER = 
"org.apache.camel.quarkus.component.diagram.CamelDiagramRecorder";
 
     private static final List<String> ISOLATED_ONLY_ATTRIBUTES = 
List.of("mockEndpoints", "mockEndpointsAndSkip",
             "stubEndpoints", "autoStartupExclude");
@@ -232,6 +233,18 @@ public final class CamelQuarkusTestInspector {
                     dumpRoute.asString()));
             }
         }
+        AnnotationValue dumpRouteDiagramFolder = 
annotation.value("dumpRouteDiagramFolder");
+        if (dumpRouteDiagramFolder != null && 
!dumpRouteDiagramFolder.asString().isBlank()) {
+            if (!classPresent.test(DIAGRAM_MARKER)) {
+                errors.add(name + ": dumpRouteDiagramFolder requires the 
camel-quarkus-diagram dependency");
+            }
+        } else {
+            for (String option : List.of("dumpRouteDiagramTopology", 
"dumpRouteDiagramTopologyExternal")) {
+                if (annotation.value(option) != null) {
+                    errors.add(name + ": " + option + " requires 
dumpRouteDiagramFolder");
+                }
+            }
+        }
     }
 
     /**
diff --git 
a/test-framework/camel-quarkus-junit/src/test/java/org/apache/camel/quarkus/test/internal/CamelQuarkusTestInspectorTest.java
 
b/test-framework/camel-quarkus-junit/src/test/java/org/apache/camel/quarkus/test/internal/CamelQuarkusTestInspectorTest.java
index 7b8d807c3b..af73cbd7bb 100644
--- 
a/test-framework/camel-quarkus-junit/src/test/java/org/apache/camel/quarkus/test/internal/CamelQuarkusTestInspectorTest.java
+++ 
b/test-framework/camel-quarkus-junit/src/test/java/org/apache/camel/quarkus/test/internal/CamelQuarkusTestInspectorTest.java
@@ -99,7 +99,8 @@ class CamelQuarkusTestInspectorTest {
                 "replaceRouteFromWith entry 'route=' must have the format 
routeId=uri",
                 "routes class " + NoDefaultConstructorRoutes.class.getName() + 
" must have a no-arg constructor",
                 "routes class " + AbstractRoutes.class.getName() + " must not 
be abstract",
-                "dumpRoute must be \"xml\" or \"yaml\", not \"json\"");
+                "dumpRoute must be \"xml\" or \"yaml\", not \"json\"",
+                "dumpRouteDiagramTopology requires dumpRouteDiagramFolder");
     }
 
     @Test
@@ -116,7 +117,8 @@ class CamelQuarkusTestInspectorTest {
     void missingDependencies() throws IOException {
         assertErrors(inspect(DumpTest.class, name -> false),
                 "dumpRouteCoverage requires the camel-quarkus-management 
dependency",
-                "dumpRoute = \"yaml\" requires the camel-quarkus-yaml-io 
dependency");
+                "dumpRoute = \"yaml\" requires the camel-quarkus-yaml-io 
dependency",
+                "dumpRouteDiagramFolder requires the camel-quarkus-diagram 
dependency");
         assertEquals(List.of(), inspect(DumpTest.class, name -> 
true).errors());
     }
 
@@ -234,7 +236,7 @@ class CamelQuarkusTestInspectorTest {
     }
 
     @CamelQuarkusTest(replaceRouteFromWith = { "no-separator", "route=" }, 
routes = { NoDefaultConstructorRoutes.class,
-            AbstractRoutes.class }, dumpRoute = "json")
+            AbstractRoutes.class }, dumpRoute = "json", 
dumpRouteDiagramTopology = false)
     static class InvalidAttributesTest {
     }
 
@@ -254,7 +256,7 @@ class CamelQuarkusTestInspectorTest {
         }
     }
 
-    @CamelQuarkusTest(dumpRouteCoverage = true, dumpRoute = "yaml")
+    @CamelQuarkusTest(dumpRouteCoverage = true, dumpRoute = "yaml", 
dumpRouteDiagramFolder = "target/diagrams")
     static class DumpTest {
     }
 

Reply via email to