Skip to content

Commit a475270

Browse files
author
Diego Saraiva
committed
addi support to free functions
1 parent 37285cb commit a475270

File tree

1 file changed

+224
-80
lines changed

1 file changed

+224
-80
lines changed

Diff for: coverage/xmlreport.py

+224-80
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
import sys
1010
import time
1111
import xml.dom.minidom
12-
12+
import numpy as np
1313
from coverage import env
1414
from coverage import __url__, __version__, files
1515
from coverage.backward import iitems
1616
from coverage.misc import isolate_module
1717
from coverage.report import get_analysis_to_report
18+
from coverage.backward import SimpleNamespace
1819

1920
os = isolate_module(os)
2021

@@ -30,13 +31,23 @@ def rate(hit, num):
3031
return "%.4g" % (float(hit) / num)
3132

3233

33-
class XmlReporter(object):
34-
"""A reporter for writing Cobertura-style XML coverage results."""
34+
def convert_to_dict(tup):
35+
di = {}
36+
for a, b in tup:
37+
di.setdefault(a, []).append(b)
38+
return di
3539

36-
def __init__(self, coverage):
40+
class XmlReporter(object):
41+
"""
42+
A reporter for writing Cobertura-style XML coverage results.
43+
"""
44+
EMPTY = "(empty)"
45+
def __init__(self, coverage, report_name=None):
3746
self.coverage = coverage
3847
self.config = self.coverage.config
39-
48+
#
49+
self.report_name = report_name
50+
#
4051
self.source_paths = set()
4152
if self.config.source:
4253
for src in self.config.source:
@@ -46,13 +57,15 @@ def __init__(self, coverage):
4657
self.source_paths.add(src)
4758
self.packages = {}
4859
self.xml_out = None
60+
self.is_class_level = False
4961

50-
def report(self, morfs, outfile=None):
51-
"""Generate a Cobertura-compatible XML report for `morfs`.
62+
def report(self, morfs=None, outfile=None):
63+
"""
64+
Generate a Cobertura-compatible XML report for `morfs`.
5265
53-
`morfs` is a list of modules or file names.
66+
`morfs` is a list of modules or file names.
5467
55-
`outfile` is a file object to write the XML to.
68+
`outfile` is a file object to write the XML to.
5669
5770
"""
5871
# Initial setup.
@@ -67,9 +80,7 @@ def report(self, morfs, outfile=None):
6780
xcoverage = self.xml_out.documentElement
6881
xcoverage.setAttribute("version", __version__)
6982
xcoverage.setAttribute("timestamp", str(int(time.time()*1000)))
70-
xcoverage.appendChild(self.xml_out.createComment(
71-
" Generated by coverage.py: %s " % __url__
72-
))
83+
xcoverage.appendChild(self.xml_out.createComment(" Generated by coverage.py: %s " % __url__))
7384
xcoverage.appendChild(self.xml_out.createComment(" Based on %s " % DTD_URL))
7485

7586
# Call xml_file for each file in the data.
@@ -94,19 +105,22 @@ def report(self, morfs, outfile=None):
94105

95106
# Populate the XML DOM with the package info.
96107
for pkg_name, pkg_data in sorted(iitems(self.packages)):
97-
class_elts, lhits, lnum, bhits, bnum = pkg_data
108+
modules_elts, lhits, lnum, bhits, bnum = pkg_data
98109
xpackage = self.xml_out.createElement("package")
99110
xpackages.appendChild(xpackage)
100111
xclasses = self.xml_out.createElement("classes")
101112
xpackage.appendChild(xclasses)
102-
for _, class_elt in sorted(iitems(class_elts)):
103-
xclasses.appendChild(class_elt)
113+
#
114+
for _, (class_elts, fn_elts) in sorted(iitems(modules_elts)):
115+
for class_elt in class_elts:
116+
xclasses.appendChild(class_elt)
117+
#
118+
for fn in fn_elts:
119+
xpackage.appendChild(fn)
120+
104121
xpackage.setAttribute("name", pkg_name.replace(os.sep, '.'))
105122
xpackage.setAttribute("line-rate", rate(lhits, lnum))
106-
if has_arcs:
107-
branch_rate = rate(bhits, bnum)
108-
else:
109-
branch_rate = "0"
123+
branch_rate = rate(bhits, bnum) if has_arcs else "0"
110124
xpackage.setAttribute("branch-rate", branch_rate)
111125
xpackage.setAttribute("complexity", "0")
112126

@@ -128,26 +142,48 @@ def report(self, morfs, outfile=None):
128142
xcoverage.setAttribute("branch-rate", "0")
129143
xcoverage.setAttribute("complexity", "0")
130144

145+
#
146+
if self.report_name:
147+
xcoverage.setAttribute("name", self.report_name)
131148
# Write the output file.
132149
outfile.write(serialize_xml(self.xml_out))
133150

134151
# Return the total percentage.
135152
denom = lnum_tot + bnum_tot
136-
if denom == 0:
137-
pct = 0.0
138-
else:
139-
pct = 100.0 * (lhits_tot + bhits_tot) / denom
153+
pct = 0.0 if denom == 0 else 100.0 * (lhits_tot + bhits_tot) / denom
140154
return pct
141155

142-
def xml_file(self, fr, analysis, has_arcs):
143-
"""Add to the XML report for a single file."""
144156

145-
if self.config.skip_empty:
146-
if analysis.numbers.n_statements == 0:
147-
return
157+
def is_property_tag(self, tokens_list):
158+
tokens = convert_to_dict(tokens_list)
159+
key = tokens.get('op', [''])[0]
160+
nam = tokens.get('nam', [''])[0]
161+
#
162+
is_tag = key == '@'
163+
if is_tag:
164+
if nam in ['staticmethod', 'classmethod']:
165+
self.is_class_level = True
166+
#
167+
return is_tag
148168

149-
# Create the 'lines' and 'package' XML elements, which
150-
# are populated later. Note that a package == a directory.
169+
170+
def is_member_fn(self, tokens):
171+
for token, value in tokens:
172+
if token == 'nam' and (value in ['self', 'cls']):
173+
return True
174+
return False
175+
176+
177+
def process_tokens(self, tokens_list, tag):
178+
tokens = convert_to_dict(tokens_list)
179+
key = tokens.get('key', [''])[0]
180+
name = tokens.get('nam', [''])[0]
181+
if key == tag:
182+
return True, name
183+
return False, None
184+
185+
186+
def extract_names(self, fr):
151187
filename = fr.filename.replace("\\", "/")
152188
for source_path in self.source_paths:
153189
source_path = files.canonical_filename(source_path)
@@ -160,70 +196,178 @@ def xml_file(self, fr, analysis, has_arcs):
160196

161197
dirname = os.path.dirname(rel_name) or u"."
162198
dirname = "/".join(dirname.split("/")[:self.config.xml_package_depth])
163-
package_name = dirname.replace("/", ".")
199+
return dirname, rel_name
164200

165-
package = self.packages.setdefault(package_name, [{}, 0, 0, 0, 0])
166201

202+
def create_class(self, name, rel_name, lineno):
167203
xclass = self.xml_out.createElement("class")
204+
xclass.setAttribute("name", name)
205+
xclass.setAttribute("filename", rel_name.replace("\\", "/"))
206+
xclass.setAttribute("complexity", "0")
207+
xclass.first_line = lineno
208+
return xclass
209+
210+
def set_class_stats(self, xclass, end_line, analysis):
211+
first_line = xclass.first_line
212+
class_lines = end_line - first_line
213+
filtered = [smt for smt in analysis.statements if smt >=first_line and smt <=end_line]
214+
class_hits = len(filtered)
215+
xclass.setAttribute("class_lines", str(class_lines))
216+
xclass.setAttribute("class_hits", str(class_hits))
217+
218+
219+
# if has_arcs:
220+
# class_branches = sum(t for t, k in branch_stats.values())
221+
# missing_branches = sum(t - k for t, k in branch_stats.values())
222+
# class_br_hits = class_branches - missing_branches
223+
# else:
224+
# class_branches = 0.0
225+
# class_br_hits = 0.0
168226

169-
xclass.appendChild(self.xml_out.createElement("methods"))
227+
# Finalize the statistics that are collected in the XML DOM.
228+
xclass.setAttribute("line-rate", rate(class_hits, class_lines))
170229

171-
xlines = self.xml_out.createElement("lines")
172-
xclass.appendChild(xlines)
230+
def set_method_stats(self, xmethod):
231+
method_hits = 0
232+
method_misses = 0
233+
for child in xmethod.childNodes:
234+
if child.getAttribute('hits') == "1":
235+
method_hits += 1
236+
else:
237+
method_misses += 1
238+
#
239+
method_lines = len(xmethod.childNodes)
240+
xmethod.setAttribute("method_lines", str(method_lines))
241+
xmethod.setAttribute("method_hits", str(method_hits))
242+
xmethod.setAttribute("method_misses", str(method_misses))
243+
244+
def process_class(self, rel_name, lineno, tokens, xclass, xmethod, analysis):
245+
found, name = self.process_tokens(tokens, "class")
246+
if found:
247+
#
248+
if xclass:
249+
if xmethod:
250+
xclass.appendChild(xmethod)
251+
#
252+
last_line = lineno
253+
for smt in analysis.statements:
254+
if smt < lineno:
255+
last_line = smt
256+
#
257+
self.set_class_stats(xclass, last_line, analysis)
258+
#
259+
xclass = self.create_class(name, rel_name, lineno)
260+
return True, xclass
261+
#
262+
return False, xclass
263+
264+
def process_method(self, xmethod, tokens, xclass, free_fn):
265+
found, method_name = self.process_tokens(tokens, "def")
266+
if found:
267+
#
268+
if xmethod:
269+
self.set_method_stats(xmethod)
270+
#
271+
xmethod = self.xml_out.createElement("method")
272+
xmethod.setAttribute("name", method_name)
273+
274+
if xclass and (self.is_member_fn(tokens) or self.is_class_level):
275+
xclass.appendChild(xmethod)
276+
self.is_class_level = False
277+
else:
278+
free_fn.append(xmethod)
279+
return True, xmethod
280+
return False, xmethod
173281

174-
xclass.setAttribute("name", os.path.relpath(rel_name, dirname))
175-
xclass.setAttribute("filename", rel_name.replace("\\", "/"))
176-
xclass.setAttribute("complexity", "0")
282+
def mount_package(self, dirname):
283+
package_name = dirname.replace("/", ".")
284+
package = self.packages.setdefault(package_name, [{}, 0, 0, 0, 0])
285+
return package
286+
287+
def process_line(self, line, has_arcs, branch_stats, missing_branch_arcs, analysis):
288+
# Processing Line
289+
xline = self.xml_out.createElement("line")
290+
xline.setAttribute("number", str(line))
291+
# Q: can we get info about the number of times a statement is
292+
# executed? If so, that should be recorded here.
293+
xline.setAttribute("hits", str(int(line not in analysis.missing)))
294+
if has_arcs:
295+
if line in branch_stats:
296+
total, taken = branch_stats[line]
297+
xline.setAttribute("branch", "true")
298+
xline.setAttribute("condition-coverage", "%d%% (%d/%d)" % (100*taken//total, taken, total))
299+
if line in missing_branch_arcs:
300+
annlines = ["exit" if b < 0 else str(b) for b in missing_branch_arcs[line]]
301+
xline.setAttribute("missing-branches", ",".join(annlines))
302+
return xline
177303

304+
305+
def xml_file(self, fr, analysis, has_arcs):
306+
"""Add to the XML report for a single file."""
307+
if self.config.skip_empty and analysis.numbers.n_statements == 0:
308+
return
309+
#
310+
dirname, rel_name = self.extract_names(fr)
311+
package = self.mount_package(dirname)
312+
# Free functions
313+
free_fn = []
314+
315+
#
316+
xclasses =[]
317+
xclass, xmethod = None, None
178318
branch_stats = analysis.branch_stats()
179319
missing_branch_arcs = analysis.missing_branch_arcs()
180320

181-
# For each statement, create an XML 'line' element.
182-
for line in sorted(analysis.statements):
183-
xline = self.xml_out.createElement("line")
184-
xline.setAttribute("number", str(line))
185-
186-
# Q: can we get info about the number of times a statement is
187-
# executed? If so, that should be recorded here.
188-
xline.setAttribute("hits", str(int(line not in analysis.missing)))
189-
190-
if has_arcs:
191-
if line in branch_stats:
192-
total, taken = branch_stats[line]
193-
xline.setAttribute("branch", "true")
194-
xline.setAttribute(
195-
"condition-coverage",
196-
"%d%% (%d/%d)" % (100*taken//total, taken, total)
197-
)
198-
if line in missing_branch_arcs:
199-
annlines = ["exit" if b < 0 else str(b) for b in missing_branch_arcs[line]]
200-
xline.setAttribute("missing-branches", ",".join(annlines))
201-
xlines.appendChild(xline)
202-
203-
class_lines = len(analysis.statements)
204-
class_hits = class_lines - len(analysis.missing)
205-
321+
line = 1
322+
self.is_class_level = False
323+
for line, tokens in enumerate(fr.source_token_lines(), start=1):
324+
if tokens:
325+
is_tag = self.is_property_tag(tokens)
326+
if is_tag:
327+
continue
328+
# We found a new class definition?
329+
created, xclass = self.process_class(rel_name, line, tokens, xclass, xmethod, analysis)
330+
if created:
331+
xclasses.append(xclass)
332+
continue
333+
#
334+
created, xmethod = self.process_method(xmethod, tokens, xclass, free_fn)
335+
if created:
336+
continue
337+
#
338+
# Processing a line
339+
xline = self.process_line(line, has_arcs, branch_stats, missing_branch_arcs, analysis)
340+
if xmethod:
341+
xmethod.appendChild(xline)
342+
elif xclass:
343+
xclass.appendChild(xline)
344+
#
345+
if xclass:
346+
self.set_class_stats(xclass, line, analysis)
347+
if xmethod:
348+
self.set_method_stats(xmethod)
349+
350+
#if xscope.hasChildNodes():
351+
# xclasses.append(xscope)
352+
# Rename
353+
package[0][rel_name] = (xclasses, free_fn)
206354
if has_arcs:
207-
class_branches = sum(t for t, k in branch_stats.values())
355+
classes_branches = sum(t for t, k in branch_stats.values())
208356
missing_branches = sum(t - k for t, k in branch_stats.values())
209-
class_br_hits = class_branches - missing_branches
357+
classes_br_hits = classes_branches - missing_branches
210358
else:
211-
class_branches = 0.0
212-
class_br_hits = 0.0
359+
classes_branches = 0.0
360+
classes_br_hits = 0.0
361+
#
362+
classes_lines = len(analysis.statements)
363+
classes_hits = classes_lines - len(analysis.missing)
364+
#
365+
package[1] += classes_hits
366+
package[2] += classes_lines
367+
package[3] += classes_br_hits
368+
package[4] += classes_branches
369+
213370

214-
# Finalize the statistics that are collected in the XML DOM.
215-
xclass.setAttribute("line-rate", rate(class_hits, class_lines))
216-
if has_arcs:
217-
branch_rate = rate(class_br_hits, class_branches)
218-
else:
219-
branch_rate = "0"
220-
xclass.setAttribute("branch-rate", branch_rate)
221-
222-
package[0][rel_name] = xclass
223-
package[1] += class_hits
224-
package[2] += class_lines
225-
package[3] += class_br_hits
226-
package[4] += class_branches
227371

228372

229373
def serialize_xml(dom):

0 commit comments

Comments
 (0)