2 |
2 |
3 import os, sys, time |
3 import os, sys, time |
4 import xml.dom.minidom |
4 import xml.dom.minidom |
5 |
5 |
6 from . import __url__, __version__ |
6 from . import __url__, __version__ |
7 from .backward import sorted # pylint: disable-msg=W0622 |
7 from .backward import sorted, rpartition # pylint: disable=W0622 |
8 from .report import Reporter |
8 from .report import Reporter |
9 |
9 |
10 def rate(hit, num): |
10 def rate(hit, num): |
11 """Return the fraction of `hit`/`num`.""" |
11 """Return the fraction of `hit`/`num`, as a string.""" |
12 return hit / (num or 1.0) |
12 return "%.4g" % (float(hit) / (num or 1.0)) |
13 |
13 |
14 |
14 |
15 class XmlReporter(Reporter): |
15 class XmlReporter(Reporter): |
16 """A reporter for writing Cobertura-style XML coverage results.""" |
16 """A reporter for writing Cobertura-style XML coverage results.""" |
17 |
17 |
18 def __init__(self, coverage, ignore_errors=False): |
18 def __init__(self, coverage, config): |
19 super(XmlReporter, self).__init__(coverage, ignore_errors) |
19 super(XmlReporter, self).__init__(coverage, config) |
20 |
20 |
21 self.packages = None |
21 self.packages = None |
22 self.xml_out = None |
22 self.xml_out = None |
23 self.arcs = coverage.data.has_arcs() |
23 self.arcs = coverage.data.has_arcs() |
24 |
24 |
25 def report(self, morfs, omit_prefixes=None, outfile=None): |
25 def report(self, morfs, outfile=None): |
26 """Generate a Cobertura-compatible XML report for `morfs`. |
26 """Generate a Cobertura-compatible XML report for `morfs`. |
27 |
27 |
28 `morfs` is a list of modules or filenames. `omit_prefixes` is a list |
28 `morfs` is a list of modules or filenames. |
29 of strings, prefixes of modules to omit from the report. |
29 |
|
30 `outfile` is a file object to write the XML to. |
30 |
31 |
31 """ |
32 """ |
32 # Initial setup. |
33 # Initial setup. |
33 outfile = outfile or sys.stdout |
34 outfile = outfile or sys.stdout |
34 |
35 |
50 xpackages = self.xml_out.createElement("packages") |
51 xpackages = self.xml_out.createElement("packages") |
51 xcoverage.appendChild(xpackages) |
52 xcoverage.appendChild(xpackages) |
52 |
53 |
53 # Call xml_file for each file in the data. |
54 # Call xml_file for each file in the data. |
54 self.packages = {} |
55 self.packages = {} |
55 self.report_files(self.xml_file, morfs, omit_prefixes=omit_prefixes) |
56 self.report_files(self.xml_file, morfs) |
56 |
57 |
57 lnum_tot, lhits_tot = 0, 0 |
58 lnum_tot, lhits_tot = 0, 0 |
58 bnum_tot, bhits_tot = 0, 0 |
59 bnum_tot, bhits_tot = 0, 0 |
59 |
60 |
60 # Populate the XML DOM with the package info. |
61 # Populate the XML DOM with the package info. |
61 for pkg_name, pkg_data in self.packages.items(): |
62 for pkg_name in sorted(self.packages.keys()): |
|
63 pkg_data = self.packages[pkg_name] |
62 class_elts, lhits, lnum, bhits, bnum = pkg_data |
64 class_elts, lhits, lnum, bhits, bnum = pkg_data |
63 xpackage = self.xml_out.createElement("package") |
65 xpackage = self.xml_out.createElement("package") |
64 xpackages.appendChild(xpackage) |
66 xpackages.appendChild(xpackage) |
65 xclasses = self.xml_out.createElement("classes") |
67 xclasses = self.xml_out.createElement("classes") |
66 xpackage.appendChild(xclasses) |
68 xpackage.appendChild(xclasses) |
67 for className in sorted(class_elts.keys()): |
69 for class_name in sorted(class_elts.keys()): |
68 xclasses.appendChild(class_elts[className]) |
70 xclasses.appendChild(class_elts[class_name]) |
69 xpackage.setAttribute("name", pkg_name.replace(os.sep, '.')) |
71 xpackage.setAttribute("name", pkg_name.replace(os.sep, '.')) |
70 xpackage.setAttribute("line-rate", str(rate(lhits, lnum))) |
72 xpackage.setAttribute("line-rate", rate(lhits, lnum)) |
71 xpackage.setAttribute("branch-rate", str(rate(bhits, bnum))) |
73 xpackage.setAttribute("branch-rate", rate(bhits, bnum)) |
72 xpackage.setAttribute("complexity", "0.0") |
74 xpackage.setAttribute("complexity", "0") |
73 |
75 |
74 lnum_tot += lnum |
76 lnum_tot += lnum |
75 lhits_tot += lhits |
77 lhits_tot += lhits |
76 bnum_tot += bnum |
78 bnum_tot += bnum |
77 bhits_tot += bhits |
79 bhits_tot += bhits |
78 |
80 |
79 xcoverage.setAttribute("line-rate", str(rate(lhits_tot, lnum_tot))) |
81 xcoverage.setAttribute("line-rate", rate(lhits_tot, lnum_tot)) |
80 xcoverage.setAttribute("branch-rate", str(rate(bhits_tot, bnum_tot))) |
82 xcoverage.setAttribute("branch-rate", rate(bhits_tot, bnum_tot)) |
81 |
83 |
82 # Use the DOM to write the output file. |
84 # Use the DOM to write the output file. |
83 outfile.write(self.xml_out.toprettyxml()) |
85 outfile.write(self.xml_out.toprettyxml()) |
|
86 |
|
87 # Return the total percentage. |
|
88 denom = lnum_tot + bnum_tot |
|
89 if denom == 0: |
|
90 pct = 0.0 |
|
91 else: |
|
92 pct = 100.0 * (lhits_tot + bhits_tot) / denom |
|
93 return pct |
84 |
94 |
85 def xml_file(self, cu, analysis): |
95 def xml_file(self, cu, analysis): |
86 """Add to the XML report for a single file.""" |
96 """Add to the XML report for a single file.""" |
87 |
97 |
88 # Create the 'lines' and 'package' XML elements, which |
98 # Create the 'lines' and 'package' XML elements, which |
89 # are populated later. Note that a package == a directory. |
99 # are populated later. Note that a package == a directory. |
90 dirname, fname = os.path.split(cu.name) |
100 package_name = rpartition(cu.name, ".")[0] |
91 dirname = dirname or '.' |
101 className = cu.name |
92 package = self.packages.setdefault(dirname, [ {}, 0, 0, 0, 0 ]) |
102 |
|
103 package = self.packages.setdefault(package_name, [{}, 0, 0, 0, 0]) |
93 |
104 |
94 xclass = self.xml_out.createElement("class") |
105 xclass = self.xml_out.createElement("class") |
95 |
106 |
96 xclass.appendChild(self.xml_out.createElement("methods")) |
107 xclass.appendChild(self.xml_out.createElement("methods")) |
97 |
108 |
98 xlines = self.xml_out.createElement("lines") |
109 xlines = self.xml_out.createElement("lines") |
99 xclass.appendChild(xlines) |
110 xclass.appendChild(xlines) |
100 className = fname.replace('.', '_') |
111 |
101 xclass.setAttribute("name", className) |
112 xclass.setAttribute("name", className) |
102 ext = os.path.splitext(cu.filename)[1] |
113 filename = cu.file_locator.relative_filename(cu.filename) |
103 xclass.setAttribute("filename", cu.name + ext) |
114 xclass.setAttribute("filename", filename.replace("\\", "/")) |
104 xclass.setAttribute("complexity", "0.0") |
115 xclass.setAttribute("complexity", "0") |
105 |
116 |
106 branch_lines = analysis.branch_lines() |
117 branch_stats = analysis.branch_stats() |
107 |
118 |
108 # For each statement, create an XML 'line' element. |
119 # For each statement, create an XML 'line' element. |
109 for line in analysis.statements: |
120 for line in sorted(analysis.statements): |
110 xline = self.xml_out.createElement("line") |
121 xline = self.xml_out.createElement("line") |
111 xline.setAttribute("number", str(line)) |
122 xline.setAttribute("number", str(line)) |
112 |
123 |
113 # Q: can we get info about the number of times a statement is |
124 # Q: can we get info about the number of times a statement is |
114 # executed? If so, that should be recorded here. |
125 # executed? If so, that should be recorded here. |
115 xline.setAttribute("hits", str(int(not line in analysis.missing))) |
126 xline.setAttribute("hits", str(int(line not in analysis.missing))) |
116 |
127 |
117 if self.arcs: |
128 if self.arcs: |
118 if line in branch_lines: |
129 if line in branch_stats: |
|
130 total, taken = branch_stats[line] |
119 xline.setAttribute("branch", "true") |
131 xline.setAttribute("branch", "true") |
|
132 xline.setAttribute("condition-coverage", |
|
133 "%d%% (%d/%d)" % (100*taken/total, taken, total) |
|
134 ) |
120 xlines.appendChild(xline) |
135 xlines.appendChild(xline) |
121 |
136 |
122 class_lines = 1.0 * len(analysis.statements) |
137 class_lines = len(analysis.statements) |
123 class_hits = class_lines - len(analysis.missing) |
138 class_hits = class_lines - len(analysis.missing) |
124 |
139 |
125 if self.arcs: |
140 if self.arcs: |
126 # We assume here that every branch line has 2 exits, which is |
141 class_branches = sum([t for t,k in branch_stats.values()]) |
127 # usually true. In theory, though, we could have a branch line |
142 missing_branches = sum([t-k for t,k in branch_stats.values()]) |
128 # with more exits.. |
143 class_br_hits = class_branches - missing_branches |
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: |
144 else: |
134 class_branches = 0.0 |
145 class_branches = 0.0 |
135 class_branch_hits = 0.0 |
146 class_br_hits = 0.0 |
136 |
147 |
137 # Finalize the statistics that are collected in the XML DOM. |
148 # Finalize the statistics that are collected in the XML DOM. |
138 line_rate = rate(class_hits, class_lines) |
149 xclass.setAttribute("line-rate", rate(class_hits, class_lines)) |
139 branch_rate = rate(class_branch_hits, class_branches) |
150 xclass.setAttribute("branch-rate", rate(class_br_hits, class_branches)) |
140 xclass.setAttribute("line-rate", str(line_rate)) |
|
141 xclass.setAttribute("branch-rate", str(branch_rate)) |
|
142 package[0][className] = xclass |
151 package[0][className] = xclass |
143 package[1] += class_hits |
152 package[1] += class_hits |
144 package[2] += class_lines |
153 package[2] += class_lines |
145 package[3] += class_branch_hits |
154 package[3] += class_br_hits |
146 package[4] += class_branches |
155 package[4] += class_branches |