Home | History | Annotate | Download | only in coverage
      1 """XML reporting for coverage.py"""
      2 
      3 import os, sys, time
      4 import xml.dom.minidom
      5 
      6 from coverage import __url__, __version__
      7 from coverage.backward import sorted            # pylint: disable=W0622
      8 from coverage.report import Reporter
      9 
     10 def rate(hit, num):
     11     """Return the fraction of `hit`/`num`, as a string."""
     12     return "%.4g" % (float(hit) / (num or 1.0))
     13 
     14 
     15 class XmlReporter(Reporter):
     16     """A reporter for writing Cobertura-style XML coverage results."""
     17 
     18     def __init__(self, coverage, ignore_errors=False):
     19         super(XmlReporter, self).__init__(coverage, ignore_errors)
     20 
     21         self.packages = None
     22         self.xml_out = None
     23         self.arcs = coverage.data.has_arcs()
     24 
     25     def report(self, morfs, outfile=None, config=None):
     26         """Generate a Cobertura-compatible XML report for `morfs`.
     27 
     28         `morfs` is a list of modules or filenames.
     29 
     30         `outfile` is a file object to write the XML to.  `config` is a
     31         CoverageConfig instance.
     32 
     33         """
     34         # Initial setup.
     35         outfile = outfile or sys.stdout
     36 
     37         # Create the DOM that will store the data.
     38         impl = xml.dom.minidom.getDOMImplementation()
     39         docType = impl.createDocumentType(
     40             "coverage", None,
     41             "http://cobertura.sourceforge.net/xml/coverage-03.dtd"
     42             )
     43         self.xml_out = impl.createDocument(None, "coverage", docType)
     44 
     45         # Write header stuff.
     46         xcoverage = self.xml_out.documentElement
     47         xcoverage.setAttribute("version", __version__)
     48         xcoverage.setAttribute("timestamp", str(int(time.time()*1000)))
     49         xcoverage.appendChild(self.xml_out.createComment(
     50             " Generated by coverage.py: %s " % __url__
     51             ))
     52         xpackages = self.xml_out.createElement("packages")
     53         xcoverage.appendChild(xpackages)
     54 
     55         # Call xml_file for each file in the data.
     56         self.packages = {}
     57         self.report_files(self.xml_file, morfs, config)
     58 
     59         lnum_tot, lhits_tot = 0, 0
     60         bnum_tot, bhits_tot = 0, 0
     61 
     62         # Populate the XML DOM with the package info.
     63         for pkg_name in sorted(self.packages.keys()):
     64             pkg_data = self.packages[pkg_name]
     65             class_elts, lhits, lnum, bhits, bnum = pkg_data
     66             xpackage = self.xml_out.createElement("package")
     67             xpackages.appendChild(xpackage)
     68             xclasses = self.xml_out.createElement("classes")
     69             xpackage.appendChild(xclasses)
     70             for class_name in sorted(class_elts.keys()):
     71                 xclasses.appendChild(class_elts[class_name])
     72             xpackage.setAttribute("name", pkg_name.replace(os.sep, '.'))
     73             xpackage.setAttribute("line-rate", rate(lhits, lnum))
     74             xpackage.setAttribute("branch-rate", rate(bhits, bnum))
     75             xpackage.setAttribute("complexity", "0")
     76 
     77             lnum_tot += lnum
     78             lhits_tot += lhits
     79             bnum_tot += bnum
     80             bhits_tot += bhits
     81 
     82         xcoverage.setAttribute("line-rate", rate(lhits_tot, lnum_tot))
     83         xcoverage.setAttribute("branch-rate", rate(bhits_tot, bnum_tot))
     84 
     85         # Use the DOM to write the output file.
     86         outfile.write(self.xml_out.toprettyxml())
     87 
     88     def xml_file(self, cu, analysis):
     89         """Add to the XML report for a single file."""
     90 
     91         # Create the 'lines' and 'package' XML elements, which
     92         # are populated later.  Note that a package == a directory.
     93         dirname, fname = os.path.split(cu.name)
     94         dirname = dirname or '.'
     95         package = self.packages.setdefault(dirname, [ {}, 0, 0, 0, 0 ])
     96 
     97         xclass = self.xml_out.createElement("class")
     98 
     99         xclass.appendChild(self.xml_out.createElement("methods"))
    100 
    101         xlines = self.xml_out.createElement("lines")
    102         xclass.appendChild(xlines)
    103         className = fname.replace('.', '_')
    104         xclass.setAttribute("name", className)
    105         ext = os.path.splitext(cu.filename)[1]
    106         xclass.setAttribute("filename", cu.name + ext)
    107         xclass.setAttribute("complexity", "0")
    108 
    109         branch_stats = analysis.branch_stats()
    110 
    111         # For each statement, create an XML 'line' element.
    112         for line in analysis.statements:
    113             xline = self.xml_out.createElement("line")
    114             xline.setAttribute("number", str(line))
    115 
    116             # Q: can we get info about the number of times a statement is
    117             # executed?  If so, that should be recorded here.
    118             xline.setAttribute("hits", str(int(not line in analysis.missing)))
    119 
    120             if self.arcs:
    121                 if line in branch_stats:
    122                     total, taken = branch_stats[line]
    123                     xline.setAttribute("branch", "true")
    124                     xline.setAttribute("condition-coverage",
    125                         "%d%% (%d/%d)" % (100*taken/total, taken, total)
    126                         )
    127             xlines.appendChild(xline)
    128 
    129         class_lines = len(analysis.statements)
    130         class_hits = class_lines - len(analysis.missing)
    131 
    132         if self.arcs:
    133             class_branches = sum([t for t,k in branch_stats.values()])
    134             missing_branches = sum([t-k for t,k in branch_stats.values()])
    135             class_br_hits = class_branches - missing_branches
    136         else:
    137             class_branches = 0.0
    138             class_br_hits = 0.0
    139 
    140         # Finalize the statistics that are collected in the XML DOM.
    141         xclass.setAttribute("line-rate", rate(class_hits, class_lines))
    142         xclass.setAttribute("branch-rate", rate(class_br_hits, class_branches))
    143         package[0][className] = xclass
    144         package[1] += class_hits
    145         package[2] += class_lines
    146         package[3] += class_br_hits
    147         package[4] += class_branches
    148