package hudson.plugins.cobertura; import hudson.plugins.cobertura.targets.CoverageElement; import hudson.plugins.cobertura.targets.CoverageMetric; import hudson.plugins.cobertura.targets.CoverageResult; import hudson.util.IOException2; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.SAXNotRecognizedException; import org.xml.sax.SAXNotSupportedException; import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Created by IntelliJ IDEA. * * @author connollys * @since 03-Jul-2007 09:03:30 */ public class CoberturaCoverageParser { /** * Do not instantiate CoberturaCoverageParser. */ private CoberturaCoverageParser() { } public static CoverageResult parse(File inFile, CoverageResult cumulative) throws IOException { return parse(inFile, cumulative, null); } public static CoverageResult parse(File inFile, CoverageResult cumulative, Set<String> sourcePaths) throws IOException { FileInputStream fileInputStream = null; BufferedInputStream bufferedInputStream = null; try { fileInputStream = new FileInputStream(inFile); bufferedInputStream = new BufferedInputStream(fileInputStream); return parse(bufferedInputStream, cumulative, sourcePaths); } finally { try { if (bufferedInputStream != null) bufferedInputStream.close(); if (fileInputStream != null) fileInputStream.close(); } catch (IOException e) { } } } public static CoverageResult parse(InputStream in, CoverageResult cumulative) throws IOException { return parse(in, cumulative, null); } public static CoverageResult parse(InputStream in, CoverageResult cumulative, Set<String> sourcePaths) throws IOException { if (in == null) throw new NullPointerException(); SAXParserFactory factory = SAXParserFactory.newInstance(); factory.setValidating(false); try { factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); } catch (ParserConfigurationException e) { } catch (SAXNotRecognizedException e) { } catch (SAXNotSupportedException e) { } try { SAXParser parser = factory.newSAXParser(); CoberturaXmlHandler handler = new CoberturaXmlHandler(cumulative); parser.parse(in, handler); if (sourcePaths != null) { sourcePaths.addAll(handler.getSourcePaths()); } return handler.getRootCoverage(); } catch (ParserConfigurationException e) { throw new IOException2("Cannot parse coverage results", e); } catch (SAXException e) { throw new IOException2("Cannot parse coverage results", e); } } } /** * Parses coverage XML data. */ class CoberturaXmlHandler extends DefaultHandler { private CoverageResult rootCoverage; private Stack<CoverageResult> stack = new Stack<CoverageResult>(); private static final String DEFAULT_PACKAGE = "<default>"; private Set<String> sourcePaths = new HashSet<String>(); private boolean inSources = false; private boolean inSource = false; private StringBuilder sourceDir = new StringBuilder(); public CoberturaXmlHandler(CoverageResult rootCoverage) { this.rootCoverage = rootCoverage; } /** * {@inheritDoc} */ public void startDocument() throws SAXException { super.startDocument(); if (this.rootCoverage == null) { this.rootCoverage = new CoverageResult(CoverageElement.PROJECT, null, Messages.CoberturaCoverageParser_name()); } stack.clear(); inSource = false; inSources = false; } /** * {@inheritDoc} */ public void endDocument() throws SAXException { if (!stack.empty() || inSource || inSources) { throw new SAXException("Unbalanced parse of cobertura coverage results."); } super.endDocument(); //To change body of overridden methods use File | Settings | File Templates. } private void descend(CoverageElement childType, String childName) { CoverageResult child = rootCoverage.getChild(childName); stack.push(rootCoverage); if (child == null) { rootCoverage = new CoverageResult(childType, rootCoverage, childName); } else { rootCoverage = child; } } private void ascend(CoverageElement element) { while (rootCoverage != null && rootCoverage.getElement() != element) { rootCoverage = stack.pop(); } if (rootCoverage != null) { rootCoverage = stack.pop(); } } /** * {@inheritDoc} */ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { super.startElement(uri, localName, qName, attributes); String name = attributes.getValue("name"); if ("sources".equals(qName)) { inSources = true; } else if ("source".equals(qName)) { sourceDir = new StringBuilder(); inSource = true; } else if ("coverage".equals(qName)) { } else if ("package".equals(qName)) { if ("".equals(name) || null == name) { name = DEFAULT_PACKAGE; } descend(CoverageElement.JAVA_PACKAGE, name); } else if ("class".equals(qName)) { assert rootCoverage.getElement() == CoverageElement.JAVA_PACKAGE; // cobertura combines file and class final String filename = attributes.getValue("filename").replace('\\', '/'); String relativeFilename = filename; final String packageName = rootCoverage.getName(); final String packagePath = packageName.replace('.', '/') + "/"; if (!DEFAULT_PACKAGE.equals(packageName)) { if (relativeFilename.startsWith(packagePath)) { relativeFilename = filename.substring(packagePath.length()); } } if (name.startsWith(packageName + ".")) { name = name.substring(packageName.length() + 1); } descend(CoverageElement.JAVA_FILE, relativeFilename); rootCoverage.setRelativeSourcePath(filename); descend(CoverageElement.JAVA_CLASS, name); } else if ("method".equals(qName)) { String methodName = buildMethodName(name, attributes.getValue("signature")); descend(CoverageElement.JAVA_METHOD, methodName); } else if ("line".equals(qName)) { String hitsString = attributes.getValue("hits"); String lineNumber = attributes.getValue("number"); int denominator = 0; int numerator = 0; if (Boolean.parseBoolean(attributes.getValue("branch"))) { final String conditionCoverage = attributes.getValue("condition-coverage"); if (conditionCoverage != null) { // some cases in the wild have branch = true but no condition-coverage attribute // should be of the format xxx% (yyy/zzz) Matcher matcher = Pattern.compile("(\\d*)\\%\\s*\\((\\d*)/(\\d*)\\)").matcher(conditionCoverage); if (matcher.matches()) { assert matcher.groupCount() == 3; final String numeratorStr = matcher.group(2); final String denominatorStr = matcher.group(3); try { numerator = Integer.parseInt(numeratorStr); denominator = Integer.parseInt(denominatorStr); rootCoverage.updateMetric(CoverageMetric.CONDITIONAL, Ratio.create(numerator, denominator)); } catch (NumberFormatException e) { // ignore } } } } try { int hits = Integer.parseInt(hitsString); int number = Integer.parseInt(lineNumber); if (denominator == 0) { rootCoverage.paint(number, hits); } else { rootCoverage.paint(number, hits, numerator, denominator); } rootCoverage.updateMetric(CoverageMetric.LINE, Ratio.create((hits == 0) ? 0 : 1, 1)); } catch (NumberFormatException e) { // ignore } } } private String buildMethodName(String name, String signature) { Matcher signatureMatcher = Pattern.compile("\\((.*)\\)(.*)").matcher(signature); StringBuilder methodName = new StringBuilder(); if (signatureMatcher.matches()) { Pattern argMatcher = Pattern.compile("\\[*([TL][^\\;]*\\;)|([ZCBSIFJDV])"); String returnType = signatureMatcher.group(2); Matcher matcher = argMatcher.matcher(returnType); if (matcher.matches()) { methodName.append(parseMethodArg(matcher.group())); methodName.append(' '); } methodName.append(name); String args = signatureMatcher.group(1); matcher = argMatcher.matcher(args); methodName.append('('); boolean first = true; while (matcher.find()) { if (!first) { methodName.append(','); } methodName.append(parseMethodArg(matcher.group())); first = false; } methodName.append(')'); } else { methodName.append(name); } return methodName.toString(); } private String parseMethodArg(String s) { char c = s.charAt(0); int end; switch (c) { case'Z': return "boolean"; case'C': return "char"; case'B': return "byte"; case'S': return "short"; case'I': return "int"; case'F': return "float"; case'J': return ""; case'D': return "double"; case'V': return "void"; case'[': return parseMethodArg(s.substring(1)) + "[]"; case'T': case'L': end = s.indexOf(';'); return s.substring(1, end).replace('/', '.'); } return s; } /** * {@inheritDoc} */ public void endElement(String uri, String localName, String qName) throws SAXException { if ("sources".equals(qName)) { inSources = false; } else if ("source".equals(qName)) { if (inSources && inSource) { sourcePaths.add(sourceDir.toString().trim()); } inSource = false; } else if ("coverage".equals(qName)) { } else if ("package".equals(qName)) { ascend(CoverageElement.JAVA_PACKAGE); } else if ("class".equals(qName)) { ascend(CoverageElement.JAVA_CLASS); ascend(CoverageElement.JAVA_FILE); } else if ("method".equals(qName)) { ascend(CoverageElement.JAVA_METHOD); } super.endElement(uri, localName, qName); //To change body of overridden methods use File | Settings | File Templates. } /** * {@inheritDoc} */ public void characters(char[] ch, int start, int length) throws SAXException { sourceDir.append(new String(ch, start, length)); } /** * Getter for property 'rootCoverage'. * * @return Value for property 'rootCoverage'. */ public CoverageResult getRootCoverage() { return rootCoverage; } /** * Getter for property 'sourcePaths'. * * @return Value for property 'sourcePaths'. */ public Set<String> getSourcePaths() { return Collections.unmodifiableSet(sourcePaths); } } class CoberturaXmlHandlerStackItem { private CoverageResult metric; private CoberturaCoverageTotals totals; public CoberturaXmlHandlerStackItem(CoverageResult metric) { this.metric = metric; totals = new CoberturaCoverageTotals(); } /** * Getter for property 'metric'. * * @return Value for property 'metric'. */ public CoverageResult getMetric() { return metric; } /** * Getter for property 'totals'. * * @return Value for property 'totals'. */ public CoberturaCoverageTotals getTotals() { return totals; } } class CoberturaCoverageTotals { private long totalLineCount; private long coverLineCount; private long totalConditionCount; private long coverConditionCount; private long totalMethodCount; private long coverMethodCount; /** * Constructs a new CoberturaCoverageTotals. */ public CoberturaCoverageTotals() { totalLineCount = 0; coverLineCount = 0; totalConditionCount = 0; coverConditionCount = 0; totalMethodCount = 0; coverMethodCount = 0; } public void addLine(boolean covered) { totalLineCount++; if (covered) { coverLineCount++; } } public void addMethod(boolean covered) { totalMethodCount++; if (covered) { coverMethodCount++; } } public void addLine(boolean covered, int condCoverCount, int condTotalCount) { addLine(covered); totalConditionCount += condTotalCount; coverConditionCount += condCoverCount; } public void addTotal(CoberturaCoverageTotals sub) { totalLineCount += sub.totalLineCount; coverLineCount += sub.coverLineCount; totalConditionCount += sub.totalConditionCount; coverConditionCount += sub.coverConditionCount; totalMethodCount += sub.totalMethodCount; coverMethodCount += sub.coverMethodCount; } /** * Getter for property 'lineCoverage'. * * @return Value for property 'lineCoverage'. */ public Ratio getLineCoverage() { return Ratio.create(coverLineCount, totalLineCount); } /** * Getter for property 'conditionalCoverage'. * * @return Value for property 'conditionalCoverage'. */ public Ratio getConditionalCoverage() { return Ratio.create(coverLineCount, totalLineCount); } /** * Getter for property 'methodCoverage'. * * @return Value for property 'methodCoverage'. */ public Ratio getMethodCoverage() { return Ratio.create(coverMethodCount, totalMethodCount); } }