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