1 // © 2016 and later: Unicode, Inc. and others. 2 // License & terms of use: http://www.unicode.org/copyright.html 3 /* 4 ******************************************************************************* 5 * Copyright (C) 2016, International Business Machines Corporation and * 6 * others. All Rights Reserved. * 7 ******************************************************************************* 8 */ 9 package com.ibm.icu.dev.tool.coverage; 10 11 import java.io.BufferedReader; 12 import java.io.File; 13 import java.io.FileInputStream; 14 import java.io.IOException; 15 import java.io.InputStreamReader; 16 import java.io.StringReader; 17 import java.util.HashSet; 18 import java.util.Map; 19 import java.util.Set; 20 import java.util.TreeMap; 21 import java.util.TreeSet; 22 23 import javax.xml.parsers.DocumentBuilder; 24 import javax.xml.parsers.DocumentBuilderFactory; 25 import javax.xml.parsers.ParserConfigurationException; 26 27 import org.w3c.dom.Document; 28 import org.w3c.dom.Element; 29 import org.w3c.dom.Node; 30 import org.w3c.dom.NodeList; 31 import org.xml.sax.EntityResolver; 32 import org.xml.sax.InputSource; 33 import org.xml.sax.SAXException; 34 35 /** 36 * A tool used for scanning JaCoCo report.xml and detect methods not covered by the 37 * ICU4J unit tests. This tool is called from ICU4J ant target: coverageJaCoCo, and 38 * signals failure if there are any methods with no test coverage (and not included 39 * in 'coverage-exclusion.txt'). 40 */ 41 public class JacocoReportCheck { main(String... args)42 public static void main(String... args) { 43 if (args.length < 1) { 44 System.err.println("Missing jacoco report.xml"); 45 System.exit(1); 46 } 47 48 System.out.println("Checking method coverage in " + args[0]); 49 if (args.length > 1) { 50 System.out.println("Coverage check exclusion file: " + args[1]); 51 } 52 53 File reportXml = new File(args[0]); 54 Map<String, ReportEntry> entries = parseReport(reportXml); 55 if (entries == null) { 56 System.err.println("Failed to parse jacoco report.xml"); 57 System.exit(2); 58 } 59 60 Set<String> excludedSet = new HashSet<String>(); 61 if (args.length > 1) { 62 File exclusionTxt = new File(args[1]); 63 BufferedReader reader = null; 64 try { 65 reader = new BufferedReader(new InputStreamReader(new FileInputStream(exclusionTxt))); 66 while (true) { 67 String line = reader.readLine(); 68 if (line == null) { 69 break; 70 } 71 line = line.trim(); 72 if (line.startsWith("//") || line.length() == 0) { 73 // comment or blank line 74 continue; 75 } 76 boolean added = excludedSet.add(line); 77 if (!added) { 78 System.err.println("Warning: Duplicated exclusion entry - " + line); 79 } 80 } 81 } catch (IOException e) { 82 e.printStackTrace(); 83 } finally { 84 if (reader != null) { 85 try { 86 reader.close(); 87 } catch (IOException e) { 88 e.printStackTrace(); 89 // ignore 90 } 91 } 92 } 93 } 94 95 96 Set<String> noCoverageSet = new TreeSet<String>(); 97 Set<String> coveredButExcludedSet = new TreeSet<String>(); 98 99 for (ReportEntry reportEntry : entries.values()) { 100 String key = reportEntry.key(); 101 Counter methodCnt = reportEntry.method().methodCounter(); 102 int methodMissed = methodCnt == null ? 1 : methodCnt.missed(); 103 if (methodMissed > 0) { 104 // no test coverage 105 if (!excludedSet.contains(key)) { 106 noCoverageSet.add(key); 107 } 108 } else { 109 // covered 110 if (excludedSet.contains(key)) { 111 coveredButExcludedSet.add(key); 112 } 113 } 114 } 115 116 if (noCoverageSet.size() > 0) { 117 System.out.println("//"); 118 System.out.println("// Methods with no test coverage, not included in the exclusion set"); 119 System.out.println("//"); 120 for (String key : noCoverageSet) { 121 System.out.println(key); 122 } 123 } 124 125 if (coveredButExcludedSet.size() > 0) { 126 System.out.println("//"); 127 System.out.println("// Methods covered by tests, but included in the exclusion set"); 128 System.out.println("//"); 129 for (String key : coveredButExcludedSet) { 130 System.out.println(key); 131 } 132 } 133 134 System.out.println("Method coverage check finished"); 135 136 if (noCoverageSet.size() > 0) { 137 System.err.println("Error: Found method(s) with no test coverage"); 138 System.exit(-1); 139 } 140 } 141 parseReport(File reportXmlFile)142 private static Map<String, ReportEntry> parseReport(File reportXmlFile) { 143 try { 144 Map<String, ReportEntry> entries = new TreeMap<String, ReportEntry>(); 145 DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 146 docBuilder.setEntityResolver(new EntityResolver() { 147 // Ignores JaCoCo report DTD 148 public InputSource resolveEntity(String publicId, String systemId) { 149 return new InputSource(new StringReader("")); 150 } 151 }); 152 Document doc = docBuilder.parse(reportXmlFile); 153 NodeList nodes = doc.getElementsByTagName("report"); 154 for (int idx = 0; idx < nodes.getLength(); idx++) { 155 Node node = nodes.item(idx); 156 if (node.getNodeType() != Node.ELEMENT_NODE) { 157 continue; 158 } 159 Element reportElement = (Element)node; 160 NodeList packages = reportElement.getElementsByTagName("package"); 161 for (int pidx = 0 ; pidx < packages.getLength(); pidx++) { 162 Node pkgNode = packages.item(pidx); 163 if (pkgNode.getNodeType() != Node.ELEMENT_NODE) { 164 continue; 165 } 166 Element pkgElement = (Element)pkgNode; 167 NodeList classes = pkgElement.getChildNodes(); 168 if (classes == null) { 169 continue; 170 } 171 172 // Iterate through classes 173 for (int cidx = 0; cidx < classes.getLength(); cidx++) { 174 Node clsNode = classes.item(cidx); 175 if (clsNode.getNodeType() != Node.ELEMENT_NODE || !"class".equals(clsNode.getNodeName())) { 176 continue; 177 } 178 Element clsElement = (Element)clsNode; 179 String cls = clsElement.getAttribute("name"); 180 181 NodeList methods = clsNode.getChildNodes(); 182 if (methods == null) { 183 continue; 184 } 185 186 // Iterate through method elements 187 for (int midx = 0; midx < methods.getLength(); midx++) { 188 Node mtdNode = methods.item(midx); 189 if (mtdNode.getNodeType() != Node.ELEMENT_NODE || !"method".equals(mtdNode.getNodeName())) { 190 continue; 191 } 192 Element mtdElement = (Element)mtdNode; 193 String mtdName = mtdElement.getAttribute("name"); 194 String mtdDesc = mtdElement.getAttribute("desc"); 195 String mtdLineStr = mtdElement.getAttribute("line"); 196 assert mtdName != null; 197 assert mtdDesc != null; 198 assert mtdLineStr != null; 199 200 int mtdLine = -1; 201 try { 202 mtdLine = Integer.parseInt(mtdLineStr); 203 } catch (NumberFormatException e) { 204 // Ignore line # parse failure 205 e.printStackTrace(); 206 } 207 208 // Iterate through counter elements and add report entries 209 210 Counter instructionCnt = null; 211 Counter branchCnt = null; 212 Counter lineCnt = null; 213 Counter complexityCnt = null; 214 Counter methodCnt = null; 215 216 NodeList counters = mtdNode.getChildNodes(); 217 if (counters == null) { 218 continue; 219 } 220 for (int i = 0; i < counters.getLength(); i++) { 221 Node cntNode = counters.item(i); 222 if (cntNode.getNodeType() != Node.ELEMENT_NODE) { 223 continue; 224 } 225 Element cntElement = (Element)cntNode; 226 String type = cntElement.getAttribute("type"); 227 String missedStr = cntElement.getAttribute("missed"); 228 String coveredStr = cntElement.getAttribute("covered"); 229 assert type != null; 230 assert missedStr != null; 231 assert coveredStr != null; 232 233 int missed = -1; 234 int covered = -1; 235 try { 236 missed = Integer.parseInt(missedStr); 237 } catch (NumberFormatException e) { 238 // Ignore missed # parse failure 239 e.printStackTrace(); 240 } 241 try { 242 covered = Integer.parseInt(coveredStr); 243 } catch (NumberFormatException e) { 244 // Ignore covered # parse failure 245 e.printStackTrace(); 246 } 247 248 if (type.equals("INSTRUCTION")) { 249 instructionCnt = new Counter(missed, covered); 250 } else if (type.equals("BRANCH")) { 251 branchCnt = new Counter(missed, covered); 252 } else if (type.equals("LINE")) { 253 lineCnt = new Counter(missed, covered); 254 } else if (type.equals("COMPLEXITY")) { 255 complexityCnt = new Counter(missed, covered); 256 } else if (type.equals("METHOD")) { 257 methodCnt = new Counter(missed, covered); 258 } else { 259 System.err.println("Unknown counter type: " + type); 260 // Ignore 261 } 262 } 263 // Add the entry 264 Method method = new Method(mtdName, mtdDesc, mtdLine, 265 instructionCnt, branchCnt, lineCnt, complexityCnt, methodCnt); 266 267 ReportEntry entry = new ReportEntry(cls, method); 268 ReportEntry prev = entries.put(entry.key(), entry); 269 if (prev != null) { 270 System.out.println("oh"); 271 } 272 } 273 } 274 } 275 } 276 return entries; 277 } catch (IOException e) { 278 e.printStackTrace(); 279 return null; 280 } catch (ParserConfigurationException e) { 281 e.printStackTrace(); 282 return null; 283 } catch (SAXException e) { 284 e.printStackTrace(); 285 return null; 286 } 287 } 288 289 private static class Counter { 290 final int missed; 291 final int covered; 292 Counter(int missed, int covered)293 Counter(int missed, int covered) { 294 this.missed = missed; 295 this.covered = covered; 296 } 297 missed()298 int missed() { 299 return missed; 300 } 301 covered()302 int covered() { 303 return covered; 304 } 305 } 306 307 private static class Method { 308 final String name; 309 final String desc; 310 final int line; 311 312 final Counter instructionCnt; 313 final Counter branchCnt; 314 final Counter lineCnt; 315 final Counter complexityCnt; 316 final Counter methodCnt; 317 Method(String name, String desc, int line, Counter instructionCnt, Counter branchCnt, Counter lineCnt, Counter complexityCnt, Counter methodCnt)318 Method(String name, String desc, int line, 319 Counter instructionCnt, Counter branchCnt, Counter lineCnt, 320 Counter complexityCnt, Counter methodCnt) { 321 this.name = name; 322 this.desc = desc; 323 this.line = line; 324 this.instructionCnt = instructionCnt; 325 this.branchCnt = branchCnt; 326 this.lineCnt = lineCnt; 327 this.complexityCnt = complexityCnt; 328 this.methodCnt = methodCnt; 329 } 330 name()331 String name() { 332 return name; 333 } 334 desc()335 String desc() { 336 return desc; 337 } 338 line()339 int line() { 340 return line; 341 } 342 instructionCounter()343 Counter instructionCounter() { 344 return instructionCnt; 345 } 346 branchCounter()347 Counter branchCounter() { 348 return branchCnt; 349 } 350 lineCounter()351 Counter lineCounter() { 352 return lineCnt; 353 } 354 complexityCounter()355 Counter complexityCounter() { 356 return complexityCnt; 357 } 358 methodCounter()359 Counter methodCounter() { 360 return methodCnt; 361 } 362 } 363 364 private static class ReportEntry { 365 final String cls; 366 final Method method; 367 final String key; 368 ReportEntry(String cls, Method method)369 ReportEntry(String cls, Method method) { 370 this.cls = cls; 371 this.method = method; 372 this.key = cls + "#" + method.name() + ":" + method.desc(); 373 } 374 key()375 String key() { 376 return key; 377 } 378 cls()379 String cls() { 380 return cls; 381 } 382 method()383 Method method() { 384 return method; 385 } 386 } 387 388 } 389