xmlreport.py 7.7 KB

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