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 {
}