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