package fr.tikione.jacocoexec.analyzer; import java.io.File; import java.io.IOException; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; /** * JaCoCo XML reports parser and related utilities. * * @author Jonathan Lermitage */ public class JaCoCoXmlReportParser extends DefaultHandler { /** The coverage data of each Java class. */ private final Map<String, JavaClass> classes = new LinkedHashMap<>(32); /** Used to remember current Java package while XML parsing. */ private String currentPackage = null; /** Used to remember current Java method while XML parsing. */ private JavaMethod currentJavaMethod = null; /** Used to remember if we are in a Java method description while XML parsing. */ private boolean inMethod = false; /** Used to remember current Java class while XML parsing. */ private JavaClass currentJavaClass = null; /** * Extract coverage data from a JaCoCo XML report file. * * @param xml the JaCoCo XML report file. * @return the coverage data of each Java class registered in the JaCoCo XML report. * @throws ParserConfigurationException if an error occurs during the parsing of the JaCoCo XML report. * @throws SAXException if an error occurs during the parsing of the JaCoCo XML report. * @throws IOException if an error occurs during the parsing of the JaCoCo XML report. */ public static Map<String, JavaClass> getCoverageData(File xml) throws ParserConfigurationException, SAXException, IOException { SAXParserFactory factory = SAXParserFactory.newInstance(); factory.setFeature("http://xml.org/sax/features/validation", false); factory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); factory.setFeature("http://xml.org/sax/features/external-general-entities", false); factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); SAXParser saxParser = factory.newSAXParser(); JaCoCoXmlReportParser handler = new JaCoCoXmlReportParser(); saxParser.parse(xml, handler); return handler.getClasses(); } /** * Get the coverage data of each Java class. * * @return coverage data. */ public Map<String, JavaClass> getClasses() { return Collections.unmodifiableMap(classes); } @Override public void startDocument() throws SAXException { } @Override public void endDocument() throws SAXException { } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (qName.equalsIgnoreCase("PACKAGE")) { for (int idx = 0; idx < attributes.getLength(); idx++) { if (attributes.getQName(idx).equalsIgnoreCase("NAME")) { currentPackage = '/' + attributes.getValue(idx) + '/'; break; } } } else if (qName.equalsIgnoreCase("SOURCEFILE")) { for (int idx = 0; idx < attributes.getLength(); idx++) { if (attributes.getQName(idx).equalsIgnoreCase("NAME")) { String classname = attributes.getValue(idx); if (classes.containsKey(currentPackage + classname)) { currentJavaClass = classes.get(currentPackage + classname); } else { currentJavaClass = new JavaClass(currentPackage, classname); classes.put(currentPackage + classname, currentJavaClass); } break; } } } else if (qName.equalsIgnoreCase("CLASS")) { for (int idx = 0; idx < attributes.getLength(); idx++) { if (attributes.getQName(idx).equalsIgnoreCase("NAME")) { String classname = attributes.getValue(idx); classname = classname.substring(classname.lastIndexOf('/') + 1); if (classname.contains("$")) { classname = classname.substring(0, classname.indexOf('$')); } classname += ".java"; if (classes.containsKey(currentPackage + classname)) { currentJavaClass = classes.get(currentPackage + classname); } else { currentJavaClass = new JavaClass(currentPackage, classname); classes.put(currentPackage + classname, currentJavaClass); } break; } } } else if (qName.equalsIgnoreCase("METHOD")) { inMethod = true; currentJavaMethod = new JavaMethod(); for (int idx = 0; idx < attributes.getLength(); idx++) { if (attributes.getQName(idx).equalsIgnoreCase("LINE")) { currentJavaMethod.setLineNumber(Integer.parseInt(attributes.getValue(idx)) - 1); } else if (attributes.getQName(idx).equalsIgnoreCase("NAME")) { currentJavaMethod.setName(attributes.getValue(idx)); } } } else if (qName.equalsIgnoreCase("COUNTER") && inMethod) { String type = null; int missed = 0; int covered = 0; for (int idx = 0; idx < attributes.getLength(); idx++) { if (attributes.getQName(idx).equalsIgnoreCase("TYPE")) { type = attributes.getValue(idx); } else if (attributes.getQName(idx).equalsIgnoreCase("MISSED")) { missed = Integer.parseInt(attributes.getValue(idx)); } else if (attributes.getQName(idx).equalsIgnoreCase("COVERED")) { covered = Integer.parseInt(attributes.getValue(idx)); } } if (type != null) { if (type.equalsIgnoreCase("INSTRUCTION")) { currentJavaMethod.setInstructionsCovered(covered); currentJavaMethod.setInstructionsMissed(missed); } else if (type.equalsIgnoreCase("LINE")) { currentJavaMethod.setLinesCovered(covered); currentJavaMethod.setLinesMissed(missed); } } } else if (qName.equalsIgnoreCase("LINE")) { // Get line's coverage data. int lineNumber = 0; int missedInstructions = 0; int coveredInstructions = 0; int missedBranches = 0; int coveredBranches = 0; for (int idx = 0; idx < attributes.getLength(); idx++) { if (attributes.getQName(idx).equalsIgnoreCase("NR")) { lineNumber = Integer.parseInt(attributes.getValue(idx)) - 1; // NetBeans Editor starting index is 0, not 1. } else if (attributes.getQName(idx).equalsIgnoreCase("MI")) { missedInstructions = Integer.parseInt(attributes.getValue(idx)); } else if (attributes.getQName(idx).equalsIgnoreCase("CI")) { coveredInstructions = Integer.parseInt(attributes.getValue(idx)); } else if (attributes.getQName(idx).equalsIgnoreCase("MB")) { missedBranches = Integer.parseInt(attributes.getValue(idx)); } else if (attributes.getQName(idx).equalsIgnoreCase("CB")) { coveredBranches = Integer.parseInt(attributes.getValue(idx)); } } boolean someMissed = missedInstructions > 0 || missedBranches > 0; boolean someCovered = coveredInstructions > 0 || coveredBranches > 0; // Set coverage state. Will indicate the color of code highlighting. if (someCovered) { if (someMissed) { currentJavaClass.addPartiallyCoveredLine(lineNumber); } else { currentJavaClass.addCoveredLine(lineNumber); } } else { currentJavaClass.addNotCoveredLine(lineNumber); } // Set coverage description when possible (currently: branches coverage). Will enable glyphed annotations. if (missedBranches > 0) { if (coveredBranches > 0) { currentJavaClass.getCoverageDesc().put(lineNumber, missedBranches + " of " + (missedBranches + coveredBranches) + " branches missed."); } else { currentJavaClass.getCoverageDesc().put(lineNumber, "All " + missedBranches + " branches missed."); } } else if (coveredBranches > 0) { currentJavaClass.getCoverageDesc().put(lineNumber, "All " + coveredBranches + " branches covered."); } } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if (qName.equalsIgnoreCase("COUNTER")) { if (!currentJavaMethod.getName().equals("<init>")) { currentJavaMethod.setCoverageDesc(""); // TODO anno desc if needed int totalMissed = currentJavaMethod.getInstructionsMissed() + currentJavaMethod.getLinesMissed(); int totalCovered = currentJavaMethod.getInstructionsCovered() + currentJavaMethod.getLinesCovered(); if (totalMissed > 0) { if (totalCovered > 0) { currentJavaMethod.setCoverageState(CoverageStateEnum.PARTIALLY_COVERED); } else { currentJavaMethod.setCoverageState(CoverageStateEnum.NOT_COVERED); } } else { currentJavaMethod.setCoverageState(CoverageStateEnum.COVERED); } currentJavaClass.addMethodCoverage(currentJavaMethod.getLineNumber(), currentJavaMethod.getCoverageState()); } } else if (qName.equalsIgnoreCase("METHOD")) { inMethod = false; } } @Override public void characters(char[] ch, int start, int length) throws SAXException { } }