| // © 2016 and later: Unicode, Inc. and others. |
| // License & terms of use: http://www.unicode.org/copyright.html#License |
| /* |
| ******************************************************************************* |
| * Copyright (C) 2016, International Business Machines Corporation and * |
| * others. All Rights Reserved. * |
| ******************************************************************************* |
| */ |
| package com.ibm.icu.dev.tool.coverage; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.StringReader; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.TreeSet; |
| |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import javax.xml.parsers.ParserConfigurationException; |
| |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| import org.xml.sax.EntityResolver; |
| import org.xml.sax.InputSource; |
| import org.xml.sax.SAXException; |
| |
| /** |
| * A tool used for scanning JaCoCo report.xml and detect methods not covered by the |
| * ICU4J unit tests. This tool is called from ICU4J ant target: coverageJaCoCo, and |
| * signals failure if there are any methods with no test coverage (and not included |
| * in 'coverage-exclusion.txt'). |
| */ |
| public class JacocoReportCheck { |
| public static void main(String... args) { |
| if (args.length < 1) { |
| System.err.println("Missing jacoco report.xml"); |
| System.exit(1); |
| } |
| |
| System.out.println("Checking method coverage in " + args[0]); |
| if (args.length > 1) { |
| System.out.println("Coverage check exclusion file: " + args[1]); |
| } |
| |
| File reportXml = new File(args[0]); |
| Map<String, ReportEntry> entries = parseReport(reportXml); |
| if (entries == null) { |
| System.err.println("Failed to parse jacoco report.xml"); |
| System.exit(2); |
| } |
| |
| Set<String> excludedSet = new HashSet<String>(); |
| if (args.length > 1) { |
| File exclusionTxt = new File(args[1]); |
| BufferedReader reader = null; |
| try { |
| reader = new BufferedReader(new InputStreamReader(new FileInputStream(exclusionTxt))); |
| while (true) { |
| String line = reader.readLine(); |
| if (line == null) { |
| break; |
| } |
| line = line.trim(); |
| if (line.startsWith("//") || line.length() == 0) { |
| // comment or blank line |
| continue; |
| } |
| boolean added = excludedSet.add(line); |
| if (!added) { |
| System.err.println("Warning: Duplicated exclusion entry - " + line); |
| } |
| } |
| } catch (IOException e) { |
| e.printStackTrace(); |
| } finally { |
| if (reader != null) { |
| try { |
| reader.close(); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| // ignore |
| } |
| } |
| } |
| } |
| |
| |
| Set<String> noCoverageSet = new TreeSet<String>(); |
| Set<String> coveredButExcludedSet = new TreeSet<String>(); |
| |
| for (ReportEntry reportEntry : entries.values()) { |
| String key = reportEntry.key(); |
| Counter methodCnt = reportEntry.method().methodCounter(); |
| int methodMissed = methodCnt == null ? 1 : methodCnt.missed(); |
| if (methodMissed > 0) { |
| // no test coverage |
| if (!excludedSet.contains(key)) { |
| noCoverageSet.add(key); |
| } |
| } else { |
| // covered |
| if (excludedSet.contains(key)) { |
| coveredButExcludedSet.add(key); |
| } |
| } |
| } |
| |
| if (noCoverageSet.size() > 0) { |
| System.out.println("//"); |
| System.out.println("// Methods with no test coverage, not included in the exclusion set"); |
| System.out.println("//"); |
| for (String key : noCoverageSet) { |
| System.out.println(key); |
| } |
| } |
| |
| if (coveredButExcludedSet.size() > 0) { |
| System.out.println("//"); |
| System.out.println("// Methods covered by tests, but included in the exclusion set"); |
| System.out.println("//"); |
| for (String key : coveredButExcludedSet) { |
| System.out.println(key); |
| } |
| } |
| |
| System.out.println("Method coverage check finished"); |
| |
| if (noCoverageSet.size() > 0) { |
| System.err.println("Error: Found method(s) with no test coverage"); |
| System.exit(-1); |
| } |
| } |
| |
| private static Map<String, ReportEntry> parseReport(File reportXmlFile) { |
| try { |
| Map<String, ReportEntry> entries = new TreeMap<String, ReportEntry>(); |
| DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); |
| docBuilder.setEntityResolver(new EntityResolver() { |
| // Ignores JaCoCo report DTD |
| public InputSource resolveEntity(String publicId, String systemId) { |
| return new InputSource(new StringReader("")); |
| } |
| }); |
| Document doc = docBuilder.parse(reportXmlFile); |
| NodeList nodes = doc.getElementsByTagName("report"); |
| for (int idx = 0; idx < nodes.getLength(); idx++) { |
| Node node = nodes.item(idx); |
| if (node.getNodeType() != Node.ELEMENT_NODE) { |
| continue; |
| } |
| Element reportElement = (Element)node; |
| NodeList packages = reportElement.getElementsByTagName("package"); |
| for (int pidx = 0 ; pidx < packages.getLength(); pidx++) { |
| Node pkgNode = packages.item(pidx); |
| if (pkgNode.getNodeType() != Node.ELEMENT_NODE) { |
| continue; |
| } |
| Element pkgElement = (Element)pkgNode; |
| NodeList classes = pkgElement.getChildNodes(); |
| if (classes == null) { |
| continue; |
| } |
| |
| // Iterate through classes |
| for (int cidx = 0; cidx < classes.getLength(); cidx++) { |
| Node clsNode = classes.item(cidx); |
| if (clsNode.getNodeType() != Node.ELEMENT_NODE || !"class".equals(clsNode.getNodeName())) { |
| continue; |
| } |
| Element clsElement = (Element)clsNode; |
| String cls = clsElement.getAttribute("name"); |
| |
| NodeList methods = clsNode.getChildNodes(); |
| if (methods == null) { |
| continue; |
| } |
| |
| // Iterate through method elements |
| for (int midx = 0; midx < methods.getLength(); midx++) { |
| Node mtdNode = methods.item(midx); |
| if (mtdNode.getNodeType() != Node.ELEMENT_NODE || !"method".equals(mtdNode.getNodeName())) { |
| continue; |
| } |
| Element mtdElement = (Element)mtdNode; |
| String mtdName = mtdElement.getAttribute("name"); |
| String mtdDesc = mtdElement.getAttribute("desc"); |
| String mtdLineStr = mtdElement.getAttribute("line"); |
| assert mtdName != null; |
| assert mtdDesc != null; |
| assert mtdLineStr != null; |
| |
| int mtdLine = -1; |
| try { |
| mtdLine = Integer.parseInt(mtdLineStr); |
| } catch (NumberFormatException e) { |
| // Ignore line # parse failure |
| e.printStackTrace(); |
| } |
| |
| // Iterate through counter elements and add report entries |
| |
| Counter instructionCnt = null; |
| Counter branchCnt = null; |
| Counter lineCnt = null; |
| Counter complexityCnt = null; |
| Counter methodCnt = null; |
| |
| NodeList counters = mtdNode.getChildNodes(); |
| if (counters == null) { |
| continue; |
| } |
| for (int i = 0; i < counters.getLength(); i++) { |
| Node cntNode = counters.item(i); |
| if (cntNode.getNodeType() != Node.ELEMENT_NODE) { |
| continue; |
| } |
| Element cntElement = (Element)cntNode; |
| String type = cntElement.getAttribute("type"); |
| String missedStr = cntElement.getAttribute("missed"); |
| String coveredStr = cntElement.getAttribute("covered"); |
| assert type != null; |
| assert missedStr != null; |
| assert coveredStr != null; |
| |
| int missed = -1; |
| int covered = -1; |
| try { |
| missed = Integer.parseInt(missedStr); |
| } catch (NumberFormatException e) { |
| // Ignore missed # parse failure |
| e.printStackTrace(); |
| } |
| try { |
| covered = Integer.parseInt(coveredStr); |
| } catch (NumberFormatException e) { |
| // Ignore covered # parse failure |
| e.printStackTrace(); |
| } |
| |
| if (type.equals("INSTRUCTION")) { |
| instructionCnt = new Counter(missed, covered); |
| } else if (type.equals("BRANCH")) { |
| branchCnt = new Counter(missed, covered); |
| } else if (type.equals("LINE")) { |
| lineCnt = new Counter(missed, covered); |
| } else if (type.equals("COMPLEXITY")) { |
| complexityCnt = new Counter(missed, covered); |
| } else if (type.equals("METHOD")) { |
| methodCnt = new Counter(missed, covered); |
| } else { |
| System.err.println("Unknown counter type: " + type); |
| // Ignore |
| } |
| } |
| // Add the entry |
| Method method = new Method(mtdName, mtdDesc, mtdLine, |
| instructionCnt, branchCnt, lineCnt, complexityCnt, methodCnt); |
| |
| ReportEntry entry = new ReportEntry(cls, method); |
| ReportEntry prev = entries.put(entry.key(), entry); |
| if (prev != null) { |
| System.out.println("oh"); |
| } |
| } |
| } |
| } |
| } |
| return entries; |
| } catch (IOException e) { |
| e.printStackTrace(); |
| return null; |
| } catch (ParserConfigurationException e) { |
| e.printStackTrace(); |
| return null; |
| } catch (SAXException e) { |
| e.printStackTrace(); |
| return null; |
| } |
| } |
| |
| private static class Counter { |
| final int missed; |
| final int covered; |
| |
| Counter(int missed, int covered) { |
| this.missed = missed; |
| this.covered = covered; |
| } |
| |
| int missed() { |
| return missed; |
| } |
| |
| int covered() { |
| return covered; |
| } |
| } |
| |
| private static class Method { |
| final String name; |
| final String desc; |
| final int line; |
| |
| final Counter instructionCnt; |
| final Counter branchCnt; |
| final Counter lineCnt; |
| final Counter complexityCnt; |
| final Counter methodCnt; |
| |
| Method(String name, String desc, int line, |
| Counter instructionCnt, Counter branchCnt, Counter lineCnt, |
| Counter complexityCnt, Counter methodCnt) { |
| this.name = name; |
| this.desc = desc; |
| this.line = line; |
| this.instructionCnt = instructionCnt; |
| this.branchCnt = branchCnt; |
| this.lineCnt = lineCnt; |
| this.complexityCnt = complexityCnt; |
| this.methodCnt = methodCnt; |
| } |
| |
| String name() { |
| return name; |
| } |
| |
| String desc() { |
| return desc; |
| } |
| |
| int line() { |
| return line; |
| } |
| |
| Counter instructionCounter() { |
| return instructionCnt; |
| } |
| |
| Counter branchCounter() { |
| return branchCnt; |
| } |
| |
| Counter lineCounter() { |
| return lineCnt; |
| } |
| |
| Counter complexityCounter() { |
| return complexityCnt; |
| } |
| |
| Counter methodCounter() { |
| return methodCnt; |
| } |
| } |
| |
| private static class ReportEntry { |
| final String cls; |
| final Method method; |
| final String key; |
| |
| ReportEntry(String cls, Method method) { |
| this.cls = cls; |
| this.method = method; |
| this.key = cls + "#" + method.name() + ":" + method.desc(); |
| } |
| |
| String key() { |
| return key; |
| } |
| |
| String cls() { |
| return cls; |
| } |
| |
| Method method() { |
| return method; |
| } |
| } |
| |
| } |