Camel route coverage maven plugin to support anonymous routes.

Project: http://git-wip-us.apache.org/repos/asf/camel/repo
Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/4393783e
Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/4393783e
Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/4393783e

Branch: refs/heads/master
Commit: 4393783e59a00d3a64b95cd7a42db0487ef6595f
Parents: 9bd1e29
Author: Claus Ibsen <davscl...@apache.org>
Authored: Sat Oct 14 13:47:20 2017 +0200
Committer: Claus Ibsen <davscl...@apache.org>
Committed: Sat Oct 14 14:09:42 2017 +0200

----------------------------------------------------------------------
 .../velocity/VelocityContentCacheTest.java      |   3 +-
 .../VelocitySomeValuesNotInExchangeTest.java    |   4 +-
 .../parser/helper/RouteCoverageHelper.java      |  53 +++++++-
 .../apache/camel/maven/RouteCoverageMojo.java   | 129 +++++++++++++++++--
 .../camel/maven/model/RouteCoverageNode.java    |  42 ++++++
 5 files changed, 219 insertions(+), 12 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/camel/blob/4393783e/components/camel-velocity/src/test/java/org/apache/camel/component/velocity/VelocityContentCacheTest.java
----------------------------------------------------------------------
diff --git 
a/components/camel-velocity/src/test/java/org/apache/camel/component/velocity/VelocityContentCacheTest.java
 
b/components/camel-velocity/src/test/java/org/apache/camel/component/velocity/VelocityContentCacheTest.java
index 060571a..5fb55ce 100644
--- 
a/components/camel-velocity/src/test/java/org/apache/camel/component/velocity/VelocityContentCacheTest.java
+++ 
b/components/camel-velocity/src/test/java/org/apache/camel/component/velocity/VelocityContentCacheTest.java
@@ -59,9 +59,10 @@ public class VelocityContentCacheTest extends 
CamelTestSupport {
         
template.sendBodyAndHeader("file://target/test-classes/org/apache/camel/component/velocity?fileExist=Override",
 "Bye $headers.name", Exchange.FILE_NAME, "hello.vm");
 
         mock.reset();
-        mock.expectedBodiesReceived("Bye Paris");
+        mock.expectedBodiesReceived("Bye Paris", "Bye World");
 
         template.sendBodyAndHeader("direct:a", "Body", "name", "Paris");
+        template.sendBodyAndHeader("direct:a", "Body", "name", "World");
         mock.assertIsSatisfied();
     }
 

http://git-wip-us.apache.org/repos/asf/camel/blob/4393783e/components/camel-velocity/src/test/java/org/apache/camel/component/velocity/VelocitySomeValuesNotInExchangeTest.java
----------------------------------------------------------------------
diff --git 
a/components/camel-velocity/src/test/java/org/apache/camel/component/velocity/VelocitySomeValuesNotInExchangeTest.java
 
b/components/camel-velocity/src/test/java/org/apache/camel/component/velocity/VelocitySomeValuesNotInExchangeTest.java
index 7ff09bd..3ba0819 100644
--- 
a/components/camel-velocity/src/test/java/org/apache/camel/component/velocity/VelocitySomeValuesNotInExchangeTest.java
+++ 
b/components/camel-velocity/src/test/java/org/apache/camel/component/velocity/VelocitySomeValuesNotInExchangeTest.java
@@ -61,7 +61,9 @@ public class VelocitySomeValuesNotInExchangeTest extends 
CamelTestSupport {
     protected RouteBuilder createRouteBuilder() throws Exception {
         return new RouteBuilder() {
             public void configure() throws Exception {
-                
from("direct:a").to("velocity:org/apache/camel/component/velocity/someValuesNotInExchange.vm").to("mock:result");
+                from("direct:a")
+                    
.to("velocity:org/apache/camel/component/velocity/someValuesNotInExchange.vm")
+                    .to("mock:result");
             }
         };
     }

http://git-wip-us.apache.org/repos/asf/camel/blob/4393783e/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/helper/RouteCoverageHelper.java
----------------------------------------------------------------------
diff --git 
a/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/helper/RouteCoverageHelper.java
 
b/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/helper/RouteCoverageHelper.java
index 2bc4bf8..45c61ed 100644
--- 
a/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/helper/RouteCoverageHelper.java
+++ 
b/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/helper/RouteCoverageHelper.java
@@ -19,7 +19,9 @@ package org.apache.camel.parser.helper;
 import java.io.File;
 import java.io.FileInputStream;
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.w3c.dom.Document;
@@ -39,9 +41,19 @@ public final class RouteCoverageHelper {
     private RouteCoverageHelper() {
     }
 
+    /**
+     * Parses the dumped route coverage data and creates a line by line 
coverage data
+     *
+     * @param directory  the directory with the dumped route coverage data
+     * @param routeId    the route id to gather, must not be null.
+     * @return line by line coverage data
+     */
     public static List<CoverageData> parseDumpRouteCoverageByRouteId(String 
directory, String routeId) throws Exception {
         List<CoverageData> answer = new ArrayList<>();
 
+        if (routeId == null) {
+            return answer;
+        }
         File[] files = new File(directory).listFiles(f -> 
f.getName().endsWith(".xml"));
         if (files == null) {
             return answer;
@@ -58,8 +70,7 @@ public final class RouteCoverageHelper {
                     String id = 
route.getAttributes().getNamedItem("id").getNodeValue();
                     // must be the target route
                     if (routeId.equals(id)) {
-                        // parse each route and build a Map<String, Integer> 
with the no of messages processed
-                        // where String is the EIP name
+                        // parse each route and build a List<CoverageData> for 
line by line coverage data
                         AtomicInteger counter = new AtomicInteger();
                         parseRouteData(catalog, route, answer, counter);
                     }
@@ -70,6 +81,44 @@ public final class RouteCoverageHelper {
         return answer;
     }
 
+    public static Map<String, List<CoverageData>> 
parseDumpRouteCoverageByClassAndTestMethod(String directory) throws Exception {
+        Map<String, List<CoverageData>> answer = new LinkedHashMap();
+
+        File[] files = new File(directory).listFiles(f -> 
f.getName().endsWith(".xml"));
+        if (files == null) {
+            return answer;
+        }
+
+        CamelCatalog catalog = new DefaultCamelCatalog(true);
+
+        for (File file : files) {
+            try (FileInputStream fis = new FileInputStream(file)) {
+                Document dom = XmlLineNumberParser.parseXml(fis);
+                NodeList routes = dom.getElementsByTagName("route");
+                for (int i = 0; i < routes.getLength(); i++) {
+                    Node route = routes.item(i);
+                    // parse each route and build a List<CoverageData> for 
line by line coverage data
+                    AtomicInteger counter = new AtomicInteger();
+                    List<CoverageData> data = new ArrayList<>();
+                    parseRouteData(catalog, route, data, counter);
+                    // create a key which is based on the file name without 
extension
+                    String key = file.getName();
+                    // strip .xml extension
+                    key = key.substring(0, key.length() - 4);
+                    // is there existing data
+                    List<CoverageData> existing = answer.get(key);
+                    if (existing != null) {
+                        existing.addAll(data);
+                    } else {
+                        answer.put(key, data);
+                    }
+                }
+            }
+        }
+
+        return answer;
+    }
+
     private static void parseRouteData(CamelCatalog catalog, Node node, 
List<CoverageData> data, AtomicInteger counter) {
         // must be a known EIP model
         String key = node.getNodeName();

http://git-wip-us.apache.org/repos/asf/camel/blob/4393783e/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/RouteCoverageMojo.java
----------------------------------------------------------------------
diff --git 
a/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/RouteCoverageMojo.java
 
b/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/RouteCoverageMojo.java
index 6f89638..8168ea7 100644
--- 
a/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/RouteCoverageMojo.java
+++ 
b/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/RouteCoverageMojo.java
@@ -23,12 +23,16 @@ import java.io.InputStream;
 import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 
+import edu.emory.mathcs.backport.java.util.Collections;
 import org.apache.camel.maven.helper.EndpointHelper;
 import org.apache.camel.maven.model.RouteCoverageNode;
 import org.apache.camel.parser.RouteBuilderParser;
@@ -36,6 +40,7 @@ import org.apache.camel.parser.XmlRouteParser;
 import org.apache.camel.parser.helper.RouteCoverageHelper;
 import org.apache.camel.parser.model.CamelNodeDetails;
 import org.apache.camel.parser.model.CoverageData;
+import org.apache.camel.util.FileUtil;
 import org.apache.maven.model.Resource;
 import org.apache.maven.plugin.MojoExecutionException;
 import org.apache.maven.plugin.MojoFailureException;
@@ -94,6 +99,17 @@ public class RouteCoverageMojo extends AbstractExecMojo {
      */
     private String excludes;
 
+    /**
+     * Whether to allow anonymous routes (routes without any route id 
assigned).
+     * By using route id's then its safer to match the route cover data with 
the route source code.
+     * Anonymous routes are less safe to use for route coverage as its harder 
to know
+     * exactly which route that was tested corresponds to which of the routes 
from the source code.
+     *
+     * @parameter property="camel.anonymousRoutes"
+     *            default-value="false"
+     */
+    private boolean anonymousRoutes = false;
+
     // CHECKSTYLE:OFF
     @Override
     public void execute() throws MojoExecutionException, MojoFailureException {
@@ -168,14 +184,17 @@ public class RouteCoverageMojo extends AbstractExecMojo {
         // skip any routes which has no route id assigned
 
         long anonymous = routeTrees.stream().filter(t -> t.getRouteId() == 
null).count();
-        if (anonymous > 0) {
+        if (!anonymousRoutes && anonymous > 0) {
             getLog().warn("Discovered " + anonymous + " anonymous routes. Add 
route ids to these routes for route coverage support");
         }
 
         final AtomicInteger notCovered = new AtomicInteger();
 
-        routeTrees = routeTrees.stream().filter(t -> t.getRouteId() != 
null).collect(Collectors.toList());
-        for (CamelNodeDetails t : routeTrees) {
+        List<CamelNodeDetails> routeIdTrees = routeTrees.stream().filter(t -> 
t.getRouteId() != null).collect(Collectors.toList());
+        List<CamelNodeDetails> anonymousRouteTrees = 
routeTrees.stream().filter(t -> t.getRouteId() == 
null).collect(Collectors.toList());
+
+        // favor strict matching on route ids
+        for (CamelNodeDetails t : routeIdTrees) {
             String routeId = t.getRouteId();
             String fileName = asRelativeFile(t.getFileName());
 
@@ -186,7 +205,7 @@ public class RouteCoverageMojo extends AbstractExecMojo {
                     getLog().warn("No route coverage data found for route: " + 
routeId
                         + ". Make sure to enable route coverage in your unit 
tests and assign unique route ids to your routes. Also remember to run unit 
tests first.");
                 } else {
-                    List<RouteCoverageNode> coverage = 
gatherRouteCoverageSummary(t, coverageData);
+                    List<RouteCoverageNode> coverage = 
gatherRouteCoverageSummary(Collections.singletonList(t), coverageData);
                     String out = templateCoverageData(fileName, routeId, 
coverage, notCovered);
                     getLog().info("Route coverage summary:\n\n" + out);
                     getLog().info("");
@@ -197,10 +216,100 @@ public class RouteCoverageMojo extends AbstractExecMojo {
             }
         }
 
+        if (anonymousRoutes && !anonymousRouteTrees.isEmpty()) {
+            // grab dump data for the route
+            try {
+                Map<String, List<CoverageData>> datas = 
RouteCoverageHelper.parseDumpRouteCoverageByClassAndTestMethod("target/camel-route-coverage");
+                if (datas.isEmpty()) {
+                    getLog().warn("No route coverage data found"
+                        + ". Make sure to enable route coverage in your unit 
tests. Also remember to run unit tests first.");
+                } else {
+                    Map<String, List<CamelNodeDetails>> routes = 
groupAnonymousRoutesByClassName(anonymousRouteTrees);
+                    // attempt to match anonymous routes via the unit test 
class
+                    for (Map.Entry<String, List<CamelNodeDetails>> t : 
routes.entrySet()) {
+                        List<RouteCoverageNode> coverage = new ArrayList<>();
+                        String className = t.getKey();
+
+                        // we may have multiple tests in the same test class 
that tests different parts of the same
+                        // routes so merge their coverage reports into a 
single coverage
+                        for (Map.Entry<String, List<CoverageData>> entry : 
datas.entrySet()) {
+                            String key = entry.getKey();
+                            String dataClassName = key.substring(0, 
key.indexOf('-'));
+                            if (dataClassName.equals(className)) {
+                                List<RouteCoverageNode> result = 
gatherRouteCoverageSummary(t.getValue(), entry.getValue());
+                                // merge them together
+                                mergeCoverageData(coverage, result);
+                            }
+                        }
+
+                        if (!coverage.isEmpty()) {
+                            String out = templateCoverageData(className, null, 
coverage, notCovered);
+                            getLog().info("Route coverage summary:\n\n" + out);
+                            getLog().info("");
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                throw new MojoExecutionException("Error during gathering route 
coverage data", e);
+            }
+        }
+
         if (failOnError && notCovered.get() > 0) {
             throw new MojoExecutionException("There are " + notCovered.get() + 
" route(s) not fully covered!");
         }
     }
+
+    private Map<String, List<CamelNodeDetails>> 
groupAnonymousRoutesByClassName(List<CamelNodeDetails> anonymousRouteTrees) {
+        Map<String, List<CamelNodeDetails>> answer = new LinkedHashMap<>();
+
+        for (CamelNodeDetails t : anonymousRouteTrees) {
+            String fileName = asRelativeFile(t.getFileName());
+            String className = FileUtil.stripExt(FileUtil.stripPath(fileName));
+            List<CamelNodeDetails> list = answer.computeIfAbsent(className, k 
-> new ArrayList<>());
+            list.add(t);
+        }
+
+        return answer;
+    }
+
+    private void mergeCoverageData(List<RouteCoverageNode> coverage, 
List<RouteCoverageNode> result) {
+        List<RouteCoverageNode> toBeAdded = new ArrayList<>();
+
+        ListIterator<RouteCoverageNode> it = null;
+        for (RouteCoverageNode node : result) {
+            // do we have an existing
+            it = positionToLineNumber(it, coverage, node.getLineNumber());
+            RouteCoverageNode existing = it.hasNext() ? it.next() : null;
+            if (existing != null) {
+                int count = existing.getCount() + node.getCount();
+                existing.setCount(count);
+            } else {
+                // its a new node
+                toBeAdded.add(node);
+            }
+        }
+
+        if (!toBeAdded.isEmpty()) {
+            coverage.addAll(toBeAdded);
+        }
+    }
+
+    private ListIterator<RouteCoverageNode> 
positionToLineNumber(ListIterator<RouteCoverageNode> it, 
List<RouteCoverageNode> coverage, int lineNumber) {
+        // restart
+        if (it == null || !it.hasNext()) {
+            it = coverage.listIterator();
+        }
+        while (it.hasNext()) {
+            RouteCoverageNode node = it.next();
+            if (node.getLineNumber() == lineNumber) {
+                // go back
+                it.previous();
+                return it;
+            }
+        }
+        return it;
+    }
+
     // CHECKSTYLE:ON
 
     @SuppressWarnings("unchecked")
@@ -213,7 +322,9 @@ public class RouteCoverageMojo extends AbstractExecMojo {
         } else {
             sw.println("File:\t" + fileName);
         }
-        sw.println("RouteId:\t" + routeId);
+        if (routeId != null) {
+            sw.println("RouteId:\t" + routeId);
+        }
         sw.println();
         sw.println(String.format("%8s   %8s   %s", "Line #", "Count", 
"Route"));
         sw.println(String.format("%8s   %8s   %s", "------", "-----", 
"-----"));
@@ -241,12 +352,14 @@ public class RouteCoverageMojo extends AbstractExecMojo {
         return bos.toString();
     }
 
-    private static List<RouteCoverageNode> 
gatherRouteCoverageSummary(CamelNodeDetails route, List<CoverageData> 
coverageData) {
+    private static List<RouteCoverageNode> 
gatherRouteCoverageSummary(List<CamelNodeDetails> route, List<CoverageData> 
coverageData) {
         List<RouteCoverageNode> answer = new ArrayList<>();
 
         Iterator<CoverageData> it = coverageData.iterator();
-        AtomicInteger level = new AtomicInteger();
-        gatherRouteCoverageSummary(route, it, level, answer);
+        for (CamelNodeDetails r : route) {
+            AtomicInteger level = new AtomicInteger();
+            gatherRouteCoverageSummary(r, it, level, answer);
+        }
 
         return answer;
     }

http://git-wip-us.apache.org/repos/asf/camel/blob/4393783e/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/model/RouteCoverageNode.java
----------------------------------------------------------------------
diff --git 
a/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/model/RouteCoverageNode.java
 
b/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/model/RouteCoverageNode.java
index cb6aba6..9656c56 100644
--- 
a/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/model/RouteCoverageNode.java
+++ 
b/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/model/RouteCoverageNode.java
@@ -74,4 +74,46 @@ public final class RouteCoverageNode {
         this.level = level;
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        RouteCoverageNode that = (RouteCoverageNode) o;
+
+        if (lineNumber != that.lineNumber) {
+            return false;
+        }
+        if (level != that.level) {
+            return false;
+        }
+        if (!className.equals(that.className)) {
+            return false;
+        }
+        return name.equals(that.name);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = className.hashCode();
+        result = 31 * result + name.hashCode();
+        result = 31 * result + lineNumber;
+        result = 31 * result + level;
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "RouteCoverageNode["
+            + "lineNumber=" + lineNumber
+            + ", count=" + count
+            + ", name='" + name + '\''
+            + ", level=" + level
+            + ", className='" + className + '\''
+            + ']';
+    }
 }

Reply via email to