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 + '\'' + + ']'; + } }