blob: 7ae7ada1dbeab5c98887793451e7ce3b11a4d2cf [file] [log] [blame]
// © 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;
}
}
}