1// © 2016 and later: Unicode, Inc. and others. 2// License & terms of use: http://www.unicode.org/copyright.html#License 3/* 4 ******************************************************************************* 5 * Copyright (C) 2016, International Business Machines Corporation and * 6 * others. All Rights Reserved. * 7 ******************************************************************************* 8 */ 9package com.ibm.icu.dev.tool.coverage; 10 11import java.io.BufferedReader; 12import java.io.File; 13import java.io.FileInputStream; 14import java.io.IOException; 15import java.io.InputStreamReader; 16import java.io.StringReader; 17import java.util.HashSet; 18import java.util.Map; 19import java.util.Set; 20import java.util.TreeMap; 21import java.util.TreeSet; 22 23import javax.xml.parsers.DocumentBuilder; 24import javax.xml.parsers.DocumentBuilderFactory; 25import javax.xml.parsers.ParserConfigurationException; 26 27import org.w3c.dom.Document; 28import org.w3c.dom.Element; 29import org.w3c.dom.Node; 30import org.w3c.dom.NodeList; 31import org.xml.sax.EntityResolver; 32import org.xml.sax.InputSource; 33import 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 */ 41public class JacocoReportCheck { 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 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 293 Counter(int missed, int covered) { 294 this.missed = missed; 295 this.covered = covered; 296 } 297 298 int missed() { 299 return missed; 300 } 301 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 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 331 String name() { 332 return name; 333 } 334 335 String desc() { 336 return desc; 337 } 338 339 int line() { 340 return line; 341 } 342 343 Counter instructionCounter() { 344 return instructionCnt; 345 } 346 347 Counter branchCounter() { 348 return branchCnt; 349 } 350 351 Counter lineCounter() { 352 return lineCnt; 353 } 354 355 Counter complexityCounter() { 356 return complexityCnt; 357 } 358 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 369 ReportEntry(String cls, Method method) { 370 this.cls = cls; 371 this.method = method; 372 this.key = cls + "#" + method.name() + ":" + method.desc(); 373 } 374 375 String key() { 376 return key; 377 } 378 379 String cls() { 380 return cls; 381 } 382 383 Method method() { 384 return method; 385 } 386 } 387 388} 389