|
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-msg=W0622 |
|
8 from coverage.report import Reporter |
|
9 |
|
10 def rate(hit, num): |
|
11 """Return the fraction of `hit`/`num`.""" |
|
12 return 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, omit_prefixes=None, outfile=None): |
|
26 """Generate a Cobertura-compatible XML report for `morfs`. |
|
27 |
|
28 `morfs` is a list of modules or filenames. `omit_prefixes` is a list |
|
29 of strings, prefixes of modules to omit from the report. |
|
30 |
|
31 """ |
|
32 # Initial setup. |
|
33 outfile = outfile or sys.stdout |
|
34 |
|
35 # Create the DOM that will store the data. |
|
36 impl = xml.dom.minidom.getDOMImplementation() |
|
37 docType = impl.createDocumentType( |
|
38 "coverage", None, |
|
39 "http://cobertura.sourceforge.net/xml/coverage-03.dtd" |
|
40 ) |
|
41 self.xml_out = impl.createDocument(None, "coverage", docType) |
|
42 |
|
43 # Write header stuff. |
|
44 xcoverage = self.xml_out.documentElement |
|
45 xcoverage.setAttribute("version", __version__) |
|
46 xcoverage.setAttribute("timestamp", str(int(time.time()*1000))) |
|
47 xcoverage.appendChild(self.xml_out.createComment( |
|
48 " Generated by coverage.py: %s " % __url__ |
|
49 )) |
|
50 xpackages = self.xml_out.createElement("packages") |
|
51 xcoverage.appendChild(xpackages) |
|
52 |
|
53 # Call xml_file for each file in the data. |
|
54 self.packages = {} |
|
55 self.report_files(self.xml_file, morfs, omit_prefixes=omit_prefixes) |
|
56 |
|
57 lnum_tot, lhits_tot = 0, 0 |
|
58 bnum_tot, bhits_tot = 0, 0 |
|
59 |
|
60 # Populate the XML DOM with the package info. |
|
61 for pkg_name, pkg_data in self.packages.items(): |
|
62 class_elts, lhits, lnum, bhits, bnum = pkg_data |
|
63 xpackage = self.xml_out.createElement("package") |
|
64 xpackages.appendChild(xpackage) |
|
65 xclasses = self.xml_out.createElement("classes") |
|
66 xpackage.appendChild(xclasses) |
|
67 for className in sorted(class_elts.keys()): |
|
68 xclasses.appendChild(class_elts[className]) |
|
69 xpackage.setAttribute("name", pkg_name.replace(os.sep, '.')) |
|
70 xpackage.setAttribute("line-rate", str(rate(lhits, lnum))) |
|
71 xpackage.setAttribute("branch-rate", str(rate(bhits, bnum))) |
|
72 xpackage.setAttribute("complexity", "0.0") |
|
73 |
|
74 lnum_tot += lnum |
|
75 lhits_tot += lhits |
|
76 bnum_tot += bnum |
|
77 bhits_tot += bhits |
|
78 |
|
79 xcoverage.setAttribute("line-rate", str(rate(lhits_tot, lnum_tot))) |
|
80 xcoverage.setAttribute("branch-rate", str(rate(bhits_tot, bnum_tot))) |
|
81 |
|
82 # Use the DOM to write the output file. |
|
83 outfile.write(self.xml_out.toprettyxml()) |
|
84 |
|
85 def xml_file(self, cu, analysis): |
|
86 """Add to the XML report for a single file.""" |
|
87 |
|
88 # Create the 'lines' and 'package' XML elements, which |
|
89 # are populated later. Note that a package == a directory. |
|
90 dirname, fname = os.path.split(cu.name) |
|
91 dirname = dirname or '.' |
|
92 package = self.packages.setdefault(dirname, [ {}, 0, 0, 0, 0 ]) |
|
93 |
|
94 xclass = self.xml_out.createElement("class") |
|
95 |
|
96 xclass.appendChild(self.xml_out.createElement("methods")) |
|
97 |
|
98 xlines = self.xml_out.createElement("lines") |
|
99 xclass.appendChild(xlines) |
|
100 className = fname.replace('.', '_') |
|
101 xclass.setAttribute("name", className) |
|
102 ext = os.path.splitext(cu.filename)[1] |
|
103 xclass.setAttribute("filename", cu.name + ext) |
|
104 xclass.setAttribute("complexity", "0.0") |
|
105 |
|
106 branch_lines = analysis.branch_lines() |
|
107 |
|
108 # For each statement, create an XML 'line' element. |
|
109 for line in analysis.statements: |
|
110 xline = self.xml_out.createElement("line") |
|
111 xline.setAttribute("number", str(line)) |
|
112 |
|
113 # Q: can we get info about the number of times a statement is |
|
114 # executed? If so, that should be recorded here. |
|
115 xline.setAttribute("hits", str(int(not line in analysis.missing))) |
|
116 |
|
117 if self.arcs: |
|
118 if line in branch_lines: |
|
119 xline.setAttribute("branch", "true") |
|
120 xlines.appendChild(xline) |
|
121 |
|
122 class_lines = 1.0 * len(analysis.statements) |
|
123 class_hits = class_lines - len(analysis.missing) |
|
124 |
|
125 if self.arcs: |
|
126 # We assume here that every branch line has 2 exits, which is |
|
127 # usually true. In theory, though, we could have a branch line |
|
128 # with more exits.. |
|
129 class_branches = 2.0 * len(branch_lines) |
|
130 missed_branch_targets = analysis.missing_branch_arcs().values() |
|
131 missing_branches = sum([len(b) for b in missed_branch_targets]) |
|
132 class_branch_hits = class_branches - missing_branches |
|
133 else: |
|
134 class_branches = 0.0 |
|
135 class_branch_hits = 0.0 |
|
136 |
|
137 # Finalize the statistics that are collected in the XML DOM. |
|
138 line_rate = rate(class_hits, class_lines) |
|
139 branch_rate = rate(class_branch_hits, class_branches) |
|
140 xclass.setAttribute("line-rate", str(line_rate)) |
|
141 xclass.setAttribute("branch-rate", str(branch_rate)) |
|
142 package[0][className] = xclass |
|
143 package[1] += class_hits |
|
144 package[2] += class_lines |
|
145 package[3] += class_branch_hits |
|
146 package[4] += class_branches |