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 307bfe3fc6e9 CAMEL-25750: camel-report-maven-plugin - the route
coverage report shows the body types each step has been reached with, and can
fail on mixed kinds (#28182)
307bfe3fc6e9 is described below
commit 307bfe3fc6e9088a251b21a571ba3a28a7c68169
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 20 22:12:12 2026 +0200
CAMEL-25750: camel-report-maven-plugin - the route coverage report shows
the body types each step has been reached with, and can fail on mixed kinds
(#28182)
* CAMEL-25750: camel-report-maven-plugin - the route coverage report shows
the body types each step has been reached with
The route coverage dump has the body types each step has been reached with
when the tests ran with
the message history on. camel-route-parser reads them into CoverageData,
merged across the dump files
of the tests, and the route-coverage goal shows them in a Body types column
with -Dcamel.bodyTypes=true,
with the kinds when a step was reached with bodies of more than one kind.
Without body types in the
dump, the goal says how to get them.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-25750: camel-report-maven-plugin - failOnMixedBodyKinds fails the
route coverage when a step was reached with bodies of more than one kind
A step that some messages reach with data and others with a payload fails
for some of them when it writes
the body or reads its fields. -Dcamel.failOnMixedBodyKinds=true lists those
steps, where they are and
their types, and fails the build: a check for a CI build that runs the
tests with the messages of the
expected workload.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-25750: the body kinds Javadoc says collection, not iterator
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../src/main/docs/camel-report-maven-plugin.adoc | 45 ++++++++
.../org/apache/camel/maven/RouteCoverageMojo.java | 117 ++++++++++++++++++++-
.../camel/maven/model/RouteCoverageNode.java | 26 +++++
.../camel/maven/RouteCoverageBodyTypesTest.java | 106 +++++++++++++++++++
.../camel/parser/helper/RouteCoverageHelper.java | 28 +++++
.../apache/camel/parser/model/CoverageData.java | 28 +++++
.../parser/helper/RouteCoverageHelperTest.java | 84 +++++++++++++++
.../ROOT/pages/camel-report-maven-plugin.adoc | 45 ++++++++
8 files changed, 475 insertions(+), 4 deletions(-)
diff --git
a/catalog/camel-report-maven-plugin/src/main/docs/camel-report-maven-plugin.adoc
b/catalog/camel-report-maven-plugin/src/main/docs/camel-report-maven-plugin.adoc
index 488086d31933..59bdf4bfc4c2 100644
---
a/catalog/camel-report-maven-plugin/src/main/docs/camel-report-maven-plugin.adoc
+++
b/catalog/camel-report-maven-plugin/src/main/docs/camel-report-maven-plugin.adoc
@@ -382,6 +382,46 @@ Coverage: 4 out of 5 (80.0%)
Here we can see that the 2nd last line with `to` has `0` in the count column,
and therefore is not covered.
We can also see that this is one line 34 in the source code file, which is in
the `my-camel.xml` XML file.
+=== Showing the body types
+
+When the tests run with the message history on (such as
`camel.main.messageHistory=true`), the route coverage dump also has
+the body types each step has been reached with, across the messages of the
tests (from Camel 4.24). Run the goal with
+`bodyTypes` to show them:
+
+[source,bash]
+----
+mvn camel-report:route-coverage -Dcamel.bodyTypes=true
+----
+
+[source,text]
+----
+ Line # Count Route Body types
+ ------ ----- ----- ----------
+ 3 2 from
+ 4 2 unmarshal String
+ 7 2 to LinkedHashMap | String (data and
payload)
+----
+
+A step reached with more than one type is a part of the route that is dynamic.
When the types are of more than one kind
+(`null`, `payload`, `data` or `collection`), as for the `to` here, the step
needs attention: some messages reach it with
+data, others with a payload, so a step that writes the body, or reads its
fields, fails for some of them. See
+xref:components:eips:message-history.adoc[Message History] for the body types
and their kinds.
+
+To fail the build when a step has been reached with bodies of more than one
kind, such as in a CI build that runs the
+tests with the messages of the expected workload, use `failOnMixedBodyKinds`:
+
+[source,bash]
+----
+mvn camel-report:route-coverage -Dcamel.failOnMixedBodyKinds=true
+----
+
+[source,text]
+----
+[WARNING] 1 step(s) have been reached with bodies of more than one kind: some
messages bring data and others a payload, so a step that writes the body, or
reads its fields, fails for some of them:
+ routes/orders.xml:7 to (route orders): LinkedHashMap | String (data and
payload)
+[ERROR] Failed to execute goal
org.apache.camel:camel-report-maven-plugin:route-coverage (default-cli): There
are 1 step(s) reached with bodies of more than one kind!
+----
+
=== Options
The maven plugin *coverage* goal supports the following options which can be
configured from the command line (use `-D` syntax),
@@ -401,5 +441,10 @@ The maven plugin *coverage* goal supports the following
options which can be con
that was tested corresponds to which of the routes from the source code.
| generateJacocoXmlReport | false | Whether to generate a coverage-report in
Jacoco XML format. When enabled it will generate the file:
`target/site/jacoco/xmlJacoco.xml`
| generateHtmlReport | false | Whether to generate a coverage-report in HTML
format. When enabled it will generate the file:
`target/site/route-coverage/html/index.html`
+| bodyTypes | false | Whether to show the body types each step has been
reached with, which the route coverage dump has when the
+ tests ran with the message history on (Camel 4.24 or newer).
+| failOnMixedBodyKinds | false | Whether to fail when a step has been reached
with bodies of more than one kind (null,
+ payload, data or collection) in the tests: some messages reach it with data
and others with a payload, so a step that
+ writes the body, or reads its fields, fails for some of them.
|===
diff --git
a/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/RouteCoverageMojo.java
b/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/RouteCoverageMojo.java
index 0c385a303a5f..6dd62ee8ee6e 100644
---
a/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/RouteCoverageMojo.java
+++
b/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/RouteCoverageMojo.java
@@ -32,6 +32,7 @@ import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import java.util.StringJoiner;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -156,6 +157,28 @@ public class RouteCoverageMojo extends AbstractMojo {
@Parameter(property = "camel.generateHtmlReport", defaultValue = "false")
private boolean generateHtmlReport;
+ /**
+ * Whether to show the body types each step has been reached with, which
the route coverage dump has when the tests
+ * ran with the message history on (Camel 4.24 or newer).
+ */
+ @Parameter(property = "camel.bodyTypes", defaultValue = "false")
+ private boolean bodyTypes;
+
+ /**
+ * Whether to fail when a step has been reached with bodies of more than
one kind (null, payload, data or
+ * collection) in the tests: some messages reach it with data and others
with a payload, so a step that writes the
+ * body, or reads its fields, fails for some of them. The route coverage
dump has the body types when the tests ran
+ * with the message history on (Camel 4.24 or newer).
+ */
+ @Parameter(property = "camel.failOnMixedBodyKinds", defaultValue = "false")
+ private boolean failOnMixedBodyKinds;
+
+ // whether a route coverage dump had body types
+ private boolean bodyTypesSeen;
+
+ // the steps reached with bodies of more than one kind
+ private final List<String> mixedBodyKinds = new ArrayList<>();
+
private File createJacocoDir() {
final File file = new File(project.getBasedir() +
"/target/site/jacoco");
if (!file.exists()) {
@@ -268,6 +291,15 @@ public class RouteCoverageMojo extends AbstractMojo {
if (generateHtmlReport) {
doGenerateHtmlReport();
}
+ if ((bodyTypes || failOnMixedBodyKinds) && !bodyTypesSeen) {
+ getLog().info("The route coverage dump has no body types: the
tests record them while the message history is"
+ + " on (such as camel.main.messageHistory=true),
with Camel 4.24 or newer");
+ }
+ if ((bodyTypes || failOnMixedBodyKinds) && !mixedBodyKinds.isEmpty()) {
+ getLog().warn(mixedBodyKinds.size() + " step(s) have been reached
with bodies of more than one kind: some"
+ + " messages bring data and others a payload, so a
step that writes the body, or reads its"
+ + " fields, fails for some of them:\n " +
String.join("\n ", mixedBodyKinds));
+ }
// compute and log overall coverage across routes
AtomicBoolean overallCoverageAboveThreshold = new AtomicBoolean();
@@ -276,6 +308,10 @@ public class RouteCoverageMojo extends AbstractMojo {
getLog().info("");
evalFailingConditions(notCovered, overallCoverageAboveThreshold);
+ if (failOnMixedBodyKinds && !mixedBodyKinds.isEmpty()) {
+ throw new MojoExecutionException(
+ "There are " + mixedBodyKinds.size() + " step(s) reached
with bodies of more than one kind!");
+ }
}
private void evalFailingConditions(AtomicInteger notCovered, AtomicBoolean
overallCoverageAboveThreshold)
@@ -516,6 +552,8 @@ public class RouteCoverageMojo extends AbstractMojo {
if (existing != null) {
int count = existing.getCount() + node.getCount();
existing.setCount(count);
+ existing.setBodyTypes(union(existing.getBodyTypes(),
node.getBodyTypes()));
+ existing.setBodyKinds(union(existing.getBodyKinds(),
node.getBodyKinds()));
} else {
// its a new node
toBeAdded.add(node);
@@ -559,16 +597,16 @@ public class RouteCoverageMojo extends AbstractMojo {
sw.println("Route:\t" + routeId);
}
sw.println();
- sw.printf(MAIN_FORMAT_PATTERN, "Line #", "Count", "Route");
- sw.printf(MAIN_FORMAT_PATTERN, "------", "-----", "-----");
+ boolean hasBodyTypes = model.stream().anyMatch(node ->
!node.getBodyTypes().isEmpty());
+ bodyTypesSeen |= hasBodyTypes;
+ mixedBodyKinds.addAll(mixedBodyKinds(fileName, routeId, model));
+ printTable(sw, model, bodyTypes && hasBodyTypes);
int covered = 0;
for (RouteCoverageNode node : model) {
if (node.getCount() > 0) {
covered++;
}
- String pad = padString(node.getLevel());
- sw.printf(MAIN_FORMAT_PATTERN, node.getLineNumber(),
node.getCount(), pad + node.getName());
}
coveredNodes.addAndGet(covered);
@@ -592,6 +630,75 @@ public class RouteCoverageMojo extends AbstractMojo {
return bos.toString();
}
+ /**
+ * The table of a route: the line, the count and the step, and the body
types each step has been reached with when
+ * they are shown.
+ */
+ static void printTable(PrintStream sw, List<RouteCoverageNode> model,
boolean showBodyTypes) {
+ int width = "Route".length();
+ if (showBodyTypes) {
+ for (RouteCoverageNode node : model) {
+ width = Math.max(width, padString(node.getLevel()).length() +
node.getName().length());
+ }
+ }
+ String withBodyTypes = "%-" + width + "s %s";
+ if (showBodyTypes) {
+ sw.printf(MAIN_FORMAT_PATTERN, "Line #", "Count",
String.format(withBodyTypes, "Route", "Body types"));
+ sw.printf(MAIN_FORMAT_PATTERN, "------", "-----",
String.format(withBodyTypes, "-----", "----------"));
+ } else {
+ sw.printf(MAIN_FORMAT_PATTERN, "Line #", "Count", "Route");
+ sw.printf(MAIN_FORMAT_PATTERN, "------", "-----", "-----");
+ }
+ for (RouteCoverageNode node : model) {
+ String route = padString(node.getLevel()) + node.getName();
+ if (showBodyTypes && !node.getBodyTypes().isEmpty()) {
+ route = String.format(withBodyTypes, route,
bodyTypesText(node.getBodyTypes(), node.getBodyKinds()));
+ }
+ sw.printf(MAIN_FORMAT_PATTERN, node.getLineNumber(),
node.getCount(), route);
+ }
+ }
+
+ /**
+ * The steps of a route that have been reached with bodies of more than
one kind: where they are, and their types.
+ */
+ static List<String> mixedBodyKinds(String fileName, String routeId,
List<RouteCoverageNode> model) {
+ List<String> answer = new ArrayList<>();
+ for (RouteCoverageNode node : model) {
+ if (node.getBodyKinds().size() > 1) {
+ answer.add(fileName + ":" + node.getLineNumber() + " " +
node.getName()
+ + (routeId != null ? " (route " + routeId + ")" :
"") + ": "
+ + bodyTypesText(node.getBodyTypes(),
node.getBodyKinds()));
+ }
+ }
+ return answer;
+ }
+
+ /**
+ * The body types by their simple class names, such as {@code String |
LinkedHashMap}, followed by the kinds when
+ * there is more than one: {@code String | LinkedHashMap (payload and
data)}, a step that needs attention.
+ */
+ static String bodyTypesText(List<String> types, List<String> kinds) {
+ StringJoiner sj = new StringJoiner(" | ");
+ for (String type : types) {
+ sj.add(type.substring(type.lastIndexOf('.') + 1));
+ }
+ String answer = sj.toString();
+ if (kinds != null && kinds.size() > 1) {
+ answer += " (" + String.join(", ", kinds.subList(0, kinds.size() -
1)) + " and " + kinds.get(kinds.size() - 1)
+ + ")";
+ }
+ return answer;
+ }
+
+ /**
+ * The values of both lists, each once, in the order first seen.
+ */
+ private static List<String> union(List<String> first, List<String> second)
{
+ Set<String> answer = new LinkedHashSet<>(first);
+ answer.addAll(second);
+ return List.copyOf(answer);
+ }
+
private String templateOverallCoverageData(
int coveredNodes, int totalNumberOfNodes, AtomicBoolean
overallCoverageAboveThreshold) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
@@ -655,6 +762,8 @@ public class RouteCoverageMojo extends AbstractMojo {
|| (holder.getNode().equals(node.getName()));
if (found) {
data.setCount(holder.getCount());
+ data.setBodyTypes(holder.getBodyTypes());
+ data.setBodyKinds(holder.getBodyKinds());
}
}
diff --git
a/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/model/RouteCoverageNode.java
b/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/model/RouteCoverageNode.java
index 166691792673..aa7c7b2bcabe 100644
---
a/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/model/RouteCoverageNode.java
+++
b/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/model/RouteCoverageNode.java
@@ -16,6 +16,8 @@
*/
package org.apache.camel.maven.model;
+import java.util.List;
+
public final class RouteCoverageNode {
private String className;
@@ -25,6 +27,8 @@ public final class RouteCoverageNode {
private int lineNumber;
private int count;
private int level;
+ private List<String> bodyTypes = List.of();
+ private List<String> bodyKinds = List.of();
public String getClassName() {
return className;
@@ -74,6 +78,28 @@ public final class RouteCoverageNode {
this.level = level;
}
+ /**
+ * The body types the node has been reached with, from the route coverage
dump.
+ */
+ public List<String> getBodyTypes() {
+ return bodyTypes;
+ }
+
+ public void setBodyTypes(List<String> bodyTypes) {
+ this.bodyTypes = bodyTypes != null ? bodyTypes : List.of();
+ }
+
+ /**
+ * The kinds of the bodies the node has been reached with: null, payload,
data or collection.
+ */
+ public List<String> getBodyKinds() {
+ return bodyKinds;
+ }
+
+ public void setBodyKinds(List<String> bodyKinds) {
+ this.bodyKinds = bodyKinds != null ? bodyKinds : List.of();
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
diff --git
a/catalog/camel-report-maven-plugin/src/test/java/org/apache/camel/maven/RouteCoverageBodyTypesTest.java
b/catalog/camel-report-maven-plugin/src/test/java/org/apache/camel/maven/RouteCoverageBodyTypesTest.java
new file mode 100644
index 000000000000..15f6d8507418
--- /dev/null
+++
b/catalog/camel-report-maven-plugin/src/test/java/org/apache/camel/maven/RouteCoverageBodyTypesTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.maven;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import org.apache.camel.maven.model.RouteCoverageNode;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The route coverage table with the body types each step has been reached
with (CAMEL-25750).
+ */
+public class RouteCoverageBodyTypesTest {
+
+ @Test
+ public void testTheBodyTypesColumn() {
+ String table = table(true);
+
+ List<String> lines = table.lines().toList();
+ assertTrue(lines.get(0).contains("Route") &&
lines.get(0).contains("Body types"), table);
+ // the widest step is " unmarshal", and the column starts four spaces
after it
+ assertTrue(table.contains(" unmarshal GenericFile"), table);
+ // a step reached with data by some messages and with a payload by
others needs attention
+ assertTrue(table.contains(" to" + " ".repeat(9) + "LinkedHashMap |
String (data and payload)"), table);
+ // the column lines up
+ assertEquals(1, lines.stream().filter(l -> l.contains("GenericFile")
|| l.contains("LinkedHashMap"))
+ .map(l -> l.indexOf(l.contains("GenericFile") ? "GenericFile"
: "LinkedHashMap")).distinct().count(),
+ table);
+ }
+
+ @Test
+ public void testWithoutTheOption() {
+ String table = table(false);
+
+ assertFalse(table.contains("Body types"), table);
+ assertFalse(table.contains("GenericFile"), table);
+ assertTrue(table.contains(" unmarshal"), table);
+ }
+
+ @Test
+ public void testTheStepsReachedWithBodiesOfMoreThanOneKind() {
+ List<String> mixed =
RouteCoverageMojo.mixedBodyKinds("routes/orders.xml", "orders", List.of(
+ node("from", 3, 2, 0, List.of(), List.of()),
+ // two types of one kind: dynamic, but not a failure
+ node("unmarshal", 4, 2, 1, List.of("java.lang.String",
"byte[]"), List.of("payload")),
+ node("to", 7, 2, 1, List.of("java.util.LinkedHashMap",
"java.lang.String"), List.of("data", "payload"))));
+
+ assertEquals(List.of("routes/orders.xml:7 to (route orders):
LinkedHashMap | String (data and payload)"), mixed);
+ assertTrue(RouteCoverageMojo.mixedBodyKinds("routes/orders.xml",
"orders",
+ List.of(node("from", 3, 2, 0, List.of(),
List.of()))).isEmpty());
+ }
+
+ @Test
+ public void testBodyTypesText() {
+ assertEquals("String | byte[] | String[]",
+ RouteCoverageMojo.bodyTypesText(List.of("java.lang.String",
"byte[]", "java.lang.String[]"),
+ List.of("payload")));
+ assertEquals("null | LinkedHashMap (null and data)",
+ RouteCoverageMojo.bodyTypesText(List.of("null",
"java.util.LinkedHashMap"), List.of("null", "data")));
+ }
+
+ private static String table(boolean bodyTypes) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ PrintStream sw = new PrintStream(bos, true, StandardCharsets.UTF_8);
+ RouteCoverageMojo.printTable(sw, List.of(
+ node("from", 12, 2, 0, List.of(), List.of()),
+ node("unmarshal", 13, 2, 1,
List.of("org.apache.camel.component.file.GenericFile"), List.of("payload")),
+ node("choice", 14, 2, 1, List.of(), List.of()),
+ node("to", 16, 2, 2, List.of("java.util.LinkedHashMap",
"java.lang.String"), List.of("data", "payload"))),
+ bodyTypes);
+ return bos.toString(StandardCharsets.UTF_8);
+ }
+
+ private static RouteCoverageNode node(
+ String name, int line, int count, int level, List<String>
bodyTypes, List<String> bodyKinds) {
+ RouteCoverageNode node = new RouteCoverageNode();
+ node.setName(name);
+ node.setLineNumber(line);
+ node.setCount(count);
+ node.setLevel(level);
+ node.setBodyTypes(bodyTypes);
+ node.setBodyKinds(bodyKinds);
+ return node;
+ }
+}
diff --git
a/catalog/camel-route-parser/src/main/java/org/apache/camel/parser/helper/RouteCoverageHelper.java
b/catalog/camel-route-parser/src/main/java/org/apache/camel/parser/helper/RouteCoverageHelper.java
index 6746380aa97a..5b6d9cb50f5a 100644
---
a/catalog/camel-route-parser/src/main/java/org/apache/camel/parser/helper/RouteCoverageHelper.java
+++
b/catalog/camel-route-parser/src/main/java/org/apache/camel/parser/helper/RouteCoverageHelper.java
@@ -20,8 +20,10 @@ import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.w3c.dom.Document;
@@ -213,6 +215,15 @@ public final class RouteCoverageHelper {
CoverageData cd = new CoverageData(id, key, count, routeId);
cd.setLineNumber(lineNumber);
+ // the body types the node was reached with, from every dump file
(each test writes its own)
+ List<String> bodyTypes = commaSeparated(node, "bodyTypes");
+ List<String> bodyKinds = commaSeparated(node, "bodyKinds");
+ if (holder != null && holder.getNode().equals(key)) {
+ bodyTypes = union(holder.getBodyTypes(), bodyTypes);
+ bodyKinds = union(holder.getBodyKinds(), bodyKinds);
+ }
+ cd.setBodyTypes(bodyTypes);
+ cd.setBodyKinds(bodyKinds);
if (holder == null) {
// add new
data.add(counter.get(), cd);
@@ -234,4 +245,21 @@ public final class RouteCoverageHelper {
}
}
+ private static List<String> commaSeparated(Node node, String attribute) {
+ Node value = node.getAttributes().getNamedItem(attribute);
+ if (value == null || value.getNodeValue().isBlank()) {
+ return List.of();
+ }
+ return List.of(value.getNodeValue().split(","));
+ }
+
+ /**
+ * The values of both lists, each once, in the order first seen.
+ */
+ private static List<String> union(List<String> first, List<String> second)
{
+ Set<String> answer = new LinkedHashSet<>(first);
+ answer.addAll(second);
+ return List.copyOf(answer);
+ }
+
}
diff --git
a/catalog/camel-route-parser/src/main/java/org/apache/camel/parser/model/CoverageData.java
b/catalog/camel-route-parser/src/main/java/org/apache/camel/parser/model/CoverageData.java
index 371e13436b3f..eab97e87becc 100644
---
a/catalog/camel-route-parser/src/main/java/org/apache/camel/parser/model/CoverageData.java
+++
b/catalog/camel-route-parser/src/main/java/org/apache/camel/parser/model/CoverageData.java
@@ -16,6 +16,8 @@
*/
package org.apache.camel.parser.model;
+import java.util.List;
+
/**
* Represents coverage data of a given node in the Camel route.
*/
@@ -26,6 +28,8 @@ public class CoverageData {
private final int count;
private String routeId;
private int lineNumber;
+ private List<String> bodyTypes = List.of();
+ private List<String> bodyKinds = List.of();
public CoverageData(String nodeId, String node, int count, String routeId)
{
this.nodeId = nodeId;
@@ -61,4 +65,28 @@ public class CoverageData {
public void setLineNumber(int lineNumber) {
this.lineNumber = lineNumber;
}
+
+ /**
+ * The body types the node has been reached with, in the order first seen
(empty when the route coverage dump has
+ * none, as when the tests ran without the message history).
+ */
+ public List<String> getBodyTypes() {
+ return bodyTypes;
+ }
+
+ public void setBodyTypes(List<String> bodyTypes) {
+ this.bodyTypes = bodyTypes != null ? bodyTypes : List.of();
+ }
+
+ /**
+ * The kinds of the bodies the node has been reached with: null, payload,
data or collection. More than one means
+ * the node needs attention.
+ */
+ public List<String> getBodyKinds() {
+ return bodyKinds;
+ }
+
+ public void setBodyKinds(List<String> bodyKinds) {
+ this.bodyKinds = bodyKinds != null ? bodyKinds : List.of();
+ }
}
diff --git
a/catalog/camel-route-parser/src/test/java/org/apache/camel/parser/helper/RouteCoverageHelperTest.java
b/catalog/camel-route-parser/src/test/java/org/apache/camel/parser/helper/RouteCoverageHelperTest.java
new file mode 100644
index 000000000000..ac92fccc55d5
--- /dev/null
+++
b/catalog/camel-route-parser/src/test/java/org/apache/camel/parser/helper/RouteCoverageHelperTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.parser.helper;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.camel.parser.model.CoverageData;
+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.assertTrue;
+
+/**
+ * The route coverage dump: the counts, and the body types each step has been
reached with (CAMEL-25750).
+ */
+public class RouteCoverageHelperTest {
+
+ @TempDir
+ Path dir;
+
+ @Test
+ public void testTheBodyTypesOfEveryDumpFile() throws Exception {
+ // each test writes its own dump file: one sent text, the other data
+ Files.writeString(dir.resolve("OrdersTextTest-testText.xml"), dump(2,
"java.lang.String", "payload"));
+ Files.writeString(dir.resolve("OrdersDataTest-testData.xml"), dump(1,
"java.util.LinkedHashMap", "data"));
+
+ List<CoverageData> data =
RouteCoverageHelper.parseDumpRouteCoverageByRouteId(dir.toString(), "orders");
+
+ assertEquals(2, data.size());
+ assertEquals("from", data.get(0).getNode());
+ assertEquals(3, data.get(0).getCount());
+ assertTrue(data.get(0).getBodyTypes().isEmpty());
+
+ CoverageData to = data.get(1);
+ assertEquals("to", to.getNode());
+ assertEquals(3, to.getCount());
+ // the files are read in no particular order
+ assertEquals(Set.of("java.lang.String", "java.util.LinkedHashMap"),
Set.copyOf(to.getBodyTypes()));
+ assertEquals(Set.of("payload", "data"), Set.copyOf(to.getBodyKinds()));
+ }
+
+ @Test
+ public void testADumpWithoutBodyTypes() throws Exception {
+ Files.writeString(dir.resolve("OrdersTest-test.xml"), dump(1, null,
null));
+
+ List<CoverageData> data =
RouteCoverageHelper.parseDumpRouteCoverageByRouteId(dir.toString(), "orders");
+
+ assertEquals(1, data.get(1).getCount());
+ assertTrue(data.get(1).getBodyTypes().isEmpty());
+ assertTrue(data.get(1).getBodyKinds().isEmpty());
+ }
+
+ private static String dump(int count, String bodyTypes, String bodyKinds) {
+ String body = bodyTypes != null ? " bodyTypes=\"" + bodyTypes + "\"
bodyKinds=\"" + bodyKinds + "\"" : "";
+ return """
+ <camelContextRouteCoverage id="camel-1" exchangesTotal="%d"
totalProcessingTime="0">
+ <routes>
+ <route id="orders" exchangesTotal="%d"
totalProcessingTime="0">
+ <from uri="direct:start" exchangesTotal="%d"
totalProcessingTime="0" index="0"/>
+ <to id="result" uri="mock:result" exchangesTotal="%d"
totalProcessingTime="0" index="1"%s/>
+ </route>
+ </routes>
+ </camelContextRouteCoverage>
+ """.formatted(count, count, count, count, body);
+ }
+}
diff --git a/docs/user-manual/modules/ROOT/pages/camel-report-maven-plugin.adoc
b/docs/user-manual/modules/ROOT/pages/camel-report-maven-plugin.adoc
index 488086d31933..59bdf4bfc4c2 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-report-maven-plugin.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-report-maven-plugin.adoc
@@ -382,6 +382,46 @@ Coverage: 4 out of 5 (80.0%)
Here we can see that the 2nd last line with `to` has `0` in the count column,
and therefore is not covered.
We can also see that this is one line 34 in the source code file, which is in
the `my-camel.xml` XML file.
+=== Showing the body types
+
+When the tests run with the message history on (such as
`camel.main.messageHistory=true`), the route coverage dump also has
+the body types each step has been reached with, across the messages of the
tests (from Camel 4.24). Run the goal with
+`bodyTypes` to show them:
+
+[source,bash]
+----
+mvn camel-report:route-coverage -Dcamel.bodyTypes=true
+----
+
+[source,text]
+----
+ Line # Count Route Body types
+ ------ ----- ----- ----------
+ 3 2 from
+ 4 2 unmarshal String
+ 7 2 to LinkedHashMap | String (data and
payload)
+----
+
+A step reached with more than one type is a part of the route that is dynamic.
When the types are of more than one kind
+(`null`, `payload`, `data` or `collection`), as for the `to` here, the step
needs attention: some messages reach it with
+data, others with a payload, so a step that writes the body, or reads its
fields, fails for some of them. See
+xref:components:eips:message-history.adoc[Message History] for the body types
and their kinds.
+
+To fail the build when a step has been reached with bodies of more than one
kind, such as in a CI build that runs the
+tests with the messages of the expected workload, use `failOnMixedBodyKinds`:
+
+[source,bash]
+----
+mvn camel-report:route-coverage -Dcamel.failOnMixedBodyKinds=true
+----
+
+[source,text]
+----
+[WARNING] 1 step(s) have been reached with bodies of more than one kind: some
messages bring data and others a payload, so a step that writes the body, or
reads its fields, fails for some of them:
+ routes/orders.xml:7 to (route orders): LinkedHashMap | String (data and
payload)
+[ERROR] Failed to execute goal
org.apache.camel:camel-report-maven-plugin:route-coverage (default-cli): There
are 1 step(s) reached with bodies of more than one kind!
+----
+
=== Options
The maven plugin *coverage* goal supports the following options which can be
configured from the command line (use `-D` syntax),
@@ -401,5 +441,10 @@ The maven plugin *coverage* goal supports the following
options which can be con
that was tested corresponds to which of the routes from the source code.
| generateJacocoXmlReport | false | Whether to generate a coverage-report in
Jacoco XML format. When enabled it will generate the file:
`target/site/jacoco/xmlJacoco.xml`
| generateHtmlReport | false | Whether to generate a coverage-report in HTML
format. When enabled it will generate the file:
`target/site/route-coverage/html/index.html`
+| bodyTypes | false | Whether to show the body types each step has been
reached with, which the route coverage dump has when the
+ tests ran with the message history on (Camel 4.24 or newer).
+| failOnMixedBodyKinds | false | Whether to fail when a step has been reached
with bodies of more than one kind (null,
+ payload, data or collection) in the tests: some messages reach it with data
and others with a payload, so a step that
+ writes the body, or reads its fields, fails for some of them.
|===