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.
 |===
 

Reply via email to