9
9
import sys
10
10
import time
11
11
import xml .dom .minidom
12
-
12
+ import numpy as np
13
13
from coverage import env
14
14
from coverage import __url__ , __version__ , files
15
15
from coverage .backward import iitems
16
16
from coverage .misc import isolate_module
17
17
from coverage .report import get_analysis_to_report
18
+ from coverage .backward import SimpleNamespace
18
19
19
20
os = isolate_module (os )
20
21
@@ -30,13 +31,23 @@ def rate(hit, num):
30
31
return "%.4g" % (float (hit ) / num )
31
32
32
33
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
35
39
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 ):
37
46
self .coverage = coverage
38
47
self .config = self .coverage .config
39
-
48
+ #
49
+ self .report_name = report_name
50
+ #
40
51
self .source_paths = set ()
41
52
if self .config .source :
42
53
for src in self .config .source :
@@ -46,13 +57,15 @@ def __init__(self, coverage):
46
57
self .source_paths .add (src )
47
58
self .packages = {}
48
59
self .xml_out = None
60
+ self .is_class_level = False
49
61
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`.
52
65
53
- `morfs` is a list of modules or file names.
66
+ `morfs` is a list of modules or file names.
54
67
55
- `outfile` is a file object to write the XML to.
68
+ `outfile` is a file object to write the XML to.
56
69
57
70
"""
58
71
# Initial setup.
@@ -67,9 +80,7 @@ def report(self, morfs, outfile=None):
67
80
xcoverage = self .xml_out .documentElement
68
81
xcoverage .setAttribute ("version" , __version__ )
69
82
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__ ))
73
84
xcoverage .appendChild (self .xml_out .createComment (" Based on %s " % DTD_URL ))
74
85
75
86
# Call xml_file for each file in the data.
@@ -94,19 +105,22 @@ def report(self, morfs, outfile=None):
94
105
95
106
# Populate the XML DOM with the package info.
96
107
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
98
109
xpackage = self .xml_out .createElement ("package" )
99
110
xpackages .appendChild (xpackage )
100
111
xclasses = self .xml_out .createElement ("classes" )
101
112
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
+
104
121
xpackage .setAttribute ("name" , pkg_name .replace (os .sep , '.' ))
105
122
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"
110
124
xpackage .setAttribute ("branch-rate" , branch_rate )
111
125
xpackage .setAttribute ("complexity" , "0" )
112
126
@@ -128,26 +142,48 @@ def report(self, morfs, outfile=None):
128
142
xcoverage .setAttribute ("branch-rate" , "0" )
129
143
xcoverage .setAttribute ("complexity" , "0" )
130
144
145
+ #
146
+ if self .report_name :
147
+ xcoverage .setAttribute ("name" , self .report_name )
131
148
# Write the output file.
132
149
outfile .write (serialize_xml (self .xml_out ))
133
150
134
151
# Return the total percentage.
135
152
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
140
154
return pct
141
155
142
- def xml_file (self , fr , analysis , has_arcs ):
143
- """Add to the XML report for a single file."""
144
156
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
148
168
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 ):
151
187
filename = fr .filename .replace ("\\ " , "/" )
152
188
for source_path in self .source_paths :
153
189
source_path = files .canonical_filename (source_path )
@@ -160,70 +196,178 @@ def xml_file(self, fr, analysis, has_arcs):
160
196
161
197
dirname = os .path .dirname (rel_name ) or u"."
162
198
dirname = "/" .join (dirname .split ("/" )[:self .config .xml_package_depth ])
163
- package_name = dirname . replace ( "/" , "." )
199
+ return dirname , rel_name
164
200
165
- package = self .packages .setdefault (package_name , [{}, 0 , 0 , 0 , 0 ])
166
201
202
+ def create_class (self , name , rel_name , lineno ):
167
203
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
168
226
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 ))
170
229
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
173
281
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
177
303
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
178
318
branch_stats = analysis .branch_stats ()
179
319
missing_branch_arcs = analysis .missing_branch_arcs ()
180
320
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 )
206
354
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 ())
208
356
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
210
358
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
+
213
370
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
227
371
228
372
229
373
def serialize_xml (dom ):
0 commit comments