xmlreport.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
  2. # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
  3. """XML reporting for coverage.py"""
  4. import os
  5. import os.path
  6. import sys
  7. import time
  8. import xml.dom.minidom
  9. from coverage import env
  10. from coverage import __url__, __version__, files
  11. from coverage.backward import iitems
  12. from coverage.misc import isolate_module
  13. from coverage.report import Reporter
  14. os = isolate_module(os)
  15. DTD_URL = 'https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd'
  16. def rate(hit, num):
  17. """Return the fraction of `hit`/`num`, as a string."""
  18. if num == 0:
  19. return "1"
  20. else:
  21. return "%.4g" % (float(hit) / num)
  22. class XmlReporter(Reporter):
  23. """A reporter for writing Cobertura-style XML coverage results."""
  24. def __init__(self, coverage, config):
  25. super(XmlReporter, self).__init__(coverage, config)
  26. self.source_paths = set()
  27. if config.source:
  28. for src in config.source:
  29. if os.path.exists(src):
  30. self.source_paths.add(files.canonical_filename(src))
  31. self.packages = {}
  32. self.xml_out = None
  33. self.has_arcs = coverage.data.has_arcs()
  34. def report(self, morfs, outfile=None):
  35. """Generate a Cobertura-compatible XML report for `morfs`.
  36. `morfs` is a list of modules or file names.
  37. `outfile` is a file object to write the XML to.
  38. """
  39. # Initial setup.
  40. outfile = outfile or sys.stdout
  41. # Create the DOM that will store the data.
  42. impl = xml.dom.minidom.getDOMImplementation()
  43. self.xml_out = impl.createDocument(None, "coverage", None)
  44. # Write header stuff.
  45. xcoverage = self.xml_out.documentElement
  46. xcoverage.setAttribute("version", __version__)
  47. xcoverage.setAttribute("timestamp", str(int(time.time()*1000)))
  48. xcoverage.appendChild(self.xml_out.createComment(
  49. " Generated by coverage.py: %s " % __url__
  50. ))
  51. xcoverage.appendChild(self.xml_out.createComment(" Based on %s " % DTD_URL))
  52. # Call xml_file for each file in the data.
  53. self.report_files(self.xml_file, morfs)
  54. xsources = self.xml_out.createElement("sources")
  55. xcoverage.appendChild(xsources)
  56. # Populate the XML DOM with the source info.
  57. for path in sorted(self.source_paths):
  58. xsource = self.xml_out.createElement("source")
  59. xsources.appendChild(xsource)
  60. txt = self.xml_out.createTextNode(path)
  61. xsource.appendChild(txt)
  62. lnum_tot, lhits_tot = 0, 0
  63. bnum_tot, bhits_tot = 0, 0
  64. xpackages = self.xml_out.createElement("packages")
  65. xcoverage.appendChild(xpackages)
  66. # Populate the XML DOM with the package info.
  67. for pkg_name, pkg_data in sorted(iitems(self.packages)):
  68. class_elts, lhits, lnum, bhits, bnum = pkg_data
  69. xpackage = self.xml_out.createElement("package")
  70. xpackages.appendChild(xpackage)
  71. xclasses = self.xml_out.createElement("classes")
  72. xpackage.appendChild(xclasses)
  73. for _, class_elt in sorted(iitems(class_elts)):
  74. xclasses.appendChild(class_elt)
  75. xpackage.setAttribute("name", pkg_name.replace(os.sep, '.'))
  76. xpackage.setAttribute("line-rate", rate(lhits, lnum))
  77. if self.has_arcs:
  78. branch_rate = rate(bhits, bnum)
  79. else:
  80. branch_rate = "0"
  81. xpackage.setAttribute("branch-rate", branch_rate)
  82. xpackage.setAttribute("complexity", "0")
  83. lnum_tot += lnum
  84. lhits_tot += lhits
  85. bnum_tot += bnum
  86. bhits_tot += bhits
  87. xcoverage.setAttribute("lines-valid", str(lnum_tot))
  88. xcoverage.setAttribute("lines-covered", str(lhits_tot))
  89. xcoverage.setAttribute("line-rate", rate(lhits_tot, lnum_tot))
  90. if self.has_arcs:
  91. xcoverage.setAttribute("branches-valid", str(bnum_tot))
  92. xcoverage.setAttribute("branches-covered", str(bhits_tot))
  93. xcoverage.setAttribute("branch-rate", rate(bhits_tot, bnum_tot))
  94. else:
  95. xcoverage.setAttribute("branches-covered", "0")
  96. xcoverage.setAttribute("branches-valid", "0")
  97. xcoverage.setAttribute("branch-rate", "0")
  98. xcoverage.setAttribute("complexity", "0")
  99. # Use the DOM to write the output file.
  100. out = self.xml_out.toprettyxml()
  101. if env.PY2:
  102. out = out.encode("utf8")
  103. outfile.write(out)
  104. # Return the total percentage.
  105. denom = lnum_tot + bnum_tot
  106. if denom == 0:
  107. pct = 0.0
  108. else:
  109. pct = 100.0 * (lhits_tot + bhits_tot) / denom
  110. return pct
  111. def xml_file(self, fr, analysis):
  112. """Add to the XML report for a single file."""
  113. # Create the 'lines' and 'package' XML elements, which
  114. # are populated later. Note that a package == a directory.
  115. filename = fr.filename.replace("\\", "/")
  116. for source_path in self.source_paths:
  117. if filename.startswith(source_path.replace("\\", "/") + "/"):
  118. rel_name = filename[len(source_path)+1:]
  119. break
  120. else:
  121. rel_name = fr.relative_filename()
  122. dirname = os.path.dirname(rel_name) or u"."
  123. dirname = "/".join(dirname.split("/")[:self.config.xml_package_depth])
  124. package_name = dirname.replace("/", ".")
  125. if rel_name != fr.filename:
  126. self.source_paths.add(fr.filename[:-len(rel_name)].rstrip(r"\/"))
  127. package = self.packages.setdefault(package_name, [{}, 0, 0, 0, 0])
  128. xclass = self.xml_out.createElement("class")
  129. xclass.appendChild(self.xml_out.createElement("methods"))
  130. xlines = self.xml_out.createElement("lines")
  131. xclass.appendChild(xlines)
  132. xclass.setAttribute("name", os.path.relpath(rel_name, dirname))
  133. xclass.setAttribute("filename", rel_name.replace("\\", "/"))
  134. xclass.setAttribute("complexity", "0")
  135. branch_stats = analysis.branch_stats()
  136. missing_branch_arcs = analysis.missing_branch_arcs()
  137. # For each statement, create an XML 'line' element.
  138. for line in sorted(analysis.statements):
  139. xline = self.xml_out.createElement("line")
  140. xline.setAttribute("number", str(line))
  141. # Q: can we get info about the number of times a statement is
  142. # executed? If so, that should be recorded here.
  143. xline.setAttribute("hits", str(int(line not in analysis.missing)))
  144. if self.has_arcs:
  145. if line in branch_stats:
  146. total, taken = branch_stats[line]
  147. xline.setAttribute("branch", "true")
  148. xline.setAttribute(
  149. "condition-coverage",
  150. "%d%% (%d/%d)" % (100*taken//total, taken, total)
  151. )
  152. if line in missing_branch_arcs:
  153. annlines = ["exit" if b < 0 else str(b) for b in missing_branch_arcs[line]]
  154. xline.setAttribute("missing-branches", ",".join(annlines))
  155. xlines.appendChild(xline)
  156. class_lines = len(analysis.statements)
  157. class_hits = class_lines - len(analysis.missing)
  158. if self.has_arcs:
  159. class_branches = sum(t for t, k in branch_stats.values())
  160. missing_branches = sum(t - k for t, k in branch_stats.values())
  161. class_br_hits = class_branches - missing_branches
  162. else:
  163. class_branches = 0.0
  164. class_br_hits = 0.0
  165. # Finalize the statistics that are collected in the XML DOM.
  166. xclass.setAttribute("line-rate", rate(class_hits, class_lines))
  167. if self.has_arcs:
  168. branch_rate = rate(class_br_hits, class_branches)
  169. else:
  170. branch_rate = "0"
  171. xclass.setAttribute("branch-rate", branch_rate)
  172. package[0][rel_name] = xclass
  173. package[1] += class_hits
  174. package[2] += class_lines
  175. package[3] += class_br_hits
  176. package[4] += class_branches