17757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch"""XML reporting for coverage.py"""
27757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
37757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochimport os, sys, time
47757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochimport xml.dom.minidom
57757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
67757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochfrom coverage import __url__, __version__
77757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochfrom coverage.backward import sorted            # pylint: disable=W0622
87757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochfrom coverage.report import Reporter
97757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
107757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochdef rate(hit, num):
117757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    """Return the fraction of `hit`/`num`, as a string."""
127757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    return "%.4g" % (float(hit) / (num or 1.0))
137757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
147757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
157757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochclass XmlReporter(Reporter):
167757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    """A reporter for writing Cobertura-style XML coverage results."""
177757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
187757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def __init__(self, coverage, ignore_errors=False):
197757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        super(XmlReporter, self).__init__(coverage, ignore_errors)
207757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
217757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.packages = None
227757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.xml_out = None
237757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.arcs = coverage.data.has_arcs()
247757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
257757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def report(self, morfs, outfile=None, config=None):
267757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Generate a Cobertura-compatible XML report for `morfs`.
277757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
287757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        `morfs` is a list of modules or filenames.
297757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
307757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        `outfile` is a file object to write the XML to.  `config` is a
317757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        CoverageConfig instance.
327757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
337757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """
347757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Initial setup.
357757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        outfile = outfile or sys.stdout
367757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
377757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Create the DOM that will store the data.
387757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        impl = xml.dom.minidom.getDOMImplementation()
397757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        docType = impl.createDocumentType(
407757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            "coverage", None,
417757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            "http://cobertura.sourceforge.net/xml/coverage-03.dtd"
427757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            )
437757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.xml_out = impl.createDocument(None, "coverage", docType)
447757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
457757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Write header stuff.
467757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        xcoverage = self.xml_out.documentElement
477757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        xcoverage.setAttribute("version", __version__)
487757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        xcoverage.setAttribute("timestamp", str(int(time.time()*1000)))
497757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        xcoverage.appendChild(self.xml_out.createComment(
507757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            " Generated by coverage.py: %s " % __url__
517757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            ))
527757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        xpackages = self.xml_out.createElement("packages")
537757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        xcoverage.appendChild(xpackages)
547757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
557757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Call xml_file for each file in the data.
567757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.packages = {}
577757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.report_files(self.xml_file, morfs, config)
587757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
597757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        lnum_tot, lhits_tot = 0, 0
607757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        bnum_tot, bhits_tot = 0, 0
617757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
627757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Populate the XML DOM with the package info.
637757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        for pkg_name in sorted(self.packages.keys()):
647757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            pkg_data = self.packages[pkg_name]
657757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            class_elts, lhits, lnum, bhits, bnum = pkg_data
667757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            xpackage = self.xml_out.createElement("package")
677757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            xpackages.appendChild(xpackage)
687757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            xclasses = self.xml_out.createElement("classes")
697757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            xpackage.appendChild(xclasses)
707757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            for class_name in sorted(class_elts.keys()):
717757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                xclasses.appendChild(class_elts[class_name])
727757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            xpackage.setAttribute("name", pkg_name.replace(os.sep, '.'))
737757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            xpackage.setAttribute("line-rate", rate(lhits, lnum))
747757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            xpackage.setAttribute("branch-rate", rate(bhits, bnum))
757757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            xpackage.setAttribute("complexity", "0")
767757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
777757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            lnum_tot += lnum
787757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            lhits_tot += lhits
797757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            bnum_tot += bnum
807757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            bhits_tot += bhits
817757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
827757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        xcoverage.setAttribute("line-rate", rate(lhits_tot, lnum_tot))
837757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        xcoverage.setAttribute("branch-rate", rate(bhits_tot, bnum_tot))
847757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
857757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Use the DOM to write the output file.
867757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        outfile.write(self.xml_out.toprettyxml())
877757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
887757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def xml_file(self, cu, analysis):
897757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Add to the XML report for a single file."""
907757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
917757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Create the 'lines' and 'package' XML elements, which
927757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # are populated later.  Note that a package == a directory.
937757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        dirname, fname = os.path.split(cu.name)
947757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        dirname = dirname or '.'
957757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        package = self.packages.setdefault(dirname, [ {}, 0, 0, 0, 0 ])
967757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
977757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        xclass = self.xml_out.createElement("class")
987757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
997757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        xclass.appendChild(self.xml_out.createElement("methods"))
1007757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1017757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        xlines = self.xml_out.createElement("lines")
1027757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        xclass.appendChild(xlines)
1037757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        className = fname.replace('.', '_')
1047757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        xclass.setAttribute("name", className)
1057757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        ext = os.path.splitext(cu.filename)[1]
1067757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        xclass.setAttribute("filename", cu.name + ext)
1077757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        xclass.setAttribute("complexity", "0")
1087757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1097757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        branch_stats = analysis.branch_stats()
1107757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1117757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # For each statement, create an XML 'line' element.
1127757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        for line in analysis.statements:
1137757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            xline = self.xml_out.createElement("line")
1147757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            xline.setAttribute("number", str(line))
1157757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1167757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            # Q: can we get info about the number of times a statement is
1177757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            # executed?  If so, that should be recorded here.
1187757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            xline.setAttribute("hits", str(int(not line in analysis.missing)))
1197757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1207757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            if self.arcs:
1217757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                if line in branch_stats:
1227757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    total, taken = branch_stats[line]
1237757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    xline.setAttribute("branch", "true")
1247757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    xline.setAttribute("condition-coverage",
1257757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                        "%d%% (%d/%d)" % (100*taken/total, taken, total)
1267757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                        )
1277757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            xlines.appendChild(xline)
1287757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1297757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        class_lines = len(analysis.statements)
1307757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        class_hits = class_lines - len(analysis.missing)
1317757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1327757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        if self.arcs:
1337757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            class_branches = sum([t for t,k in branch_stats.values()])
1347757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            missing_branches = sum([t-k for t,k in branch_stats.values()])
1357757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            class_br_hits = class_branches - missing_branches
1367757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        else:
1377757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            class_branches = 0.0
1387757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            class_br_hits = 0.0
1397757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1407757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Finalize the statistics that are collected in the XML DOM.
1417757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        xclass.setAttribute("line-rate", rate(class_hits, class_lines))
1427757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        xclass.setAttribute("branch-rate", rate(class_br_hits, class_branches))
1437757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        package[0][className] = xclass
1447757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        package[1] += class_hits
1457757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        package[2] += class_lines
1467757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        package[3] += class_br_hits
1477757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        package[4] += class_branches
148