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 { IOUtils.closeQuietly(bufferedInputStream); IOUtils.closeQuietly(fileInputStream); } } 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 static final String DEFAULT_PACKAGE = "<default>"; // patterns static for performance ("Instances of Pattern are immutable and are safe for use by multiple concurrent threads" according to javadoc) private static final Pattern CONDITION_COVERAGE_PATTERN = Pattern.compile("(\\d*)\\s*\\%\\s*\\((\\d*)/(\\d*)\\)"); private static final Pattern METHOD_SIGNATURE_PATTERN = Pattern.compile("\\((.*)\\)(.*)"); private static final Pattern METHOD_ARGS_PATTERN = Pattern.compile("\\[*([TL][^\\;]*\\;)|([ZCBSIFJDV])"); private CoverageResult rootCoverage; private Stack<CoverageResult> stack = new Stack<CoverageResult>(); 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 String filename = attributes.getValue("filename").replace('\\', '/'); // filename should be a relative path. // See https://issues.jenkins-ci.org/browse/JENKINS-16252 if (filename.startsWith("\\") || filename.startsWith("/")) { filename = filename.substring(1); } 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), // or xxx % (yyy/zzz) for French, // because cobertura uses the default locale as said in // http://sourceforge.net/tracker/?func=detail&aid=3296149&group_id=130558&atid=720015 Matcher matcher = CONDITION_COVERAGE_PATTERN.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 = METHOD_SIGNATURE_PATTERN.matcher(signature); StringBuilder methodName = new StringBuilder(); if (signatureMatcher.matches()) { String returnType = signatureMatcher.group(2); Matcher matcher = METHOD_ARGS_PATTERN.matcher(returnType); if (matcher.matches()) { methodName.append(parseMethodArg(matcher.group())); methodName.append(' '); } methodName.append(name); String args = signatureMatcher.group(1); matcher = METHOD_ARGS_PATTERN.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); } }