3
3
# Copyright 2011-2012 Eric Wendelin
4
4
#
5
5
# This is free software, licensed under the Apache License, Version 2.0,
6
- #
7
- # Licensed under the Apache License, Version 2.0 (the "License");
8
- # you may not use this file except in compliance with the License.
9
- # You may obtain a copy of the License at
10
- #
11
- # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
- # Unless required by applicable law or agreed to in writing, software
14
- # distributed under the License is distributed on an "AS IS" BASIS,
15
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
6
+ # available in the accompanying LICENSE.txt file.
18
7
19
8
"""
20
9
Converts lcov line coverage output to Cobertura-compatible XML for CI
24
13
import sys
25
14
import os
26
15
import time
16
+ import subprocess
27
17
from xml .dom import minidom
28
18
from optparse import OptionParser
29
19
30
- VERSION = '1.5'
20
+ from distutils .spawn import find_executable
21
+
22
+ CPPFILT = "c++filt"
23
+ HAVE_CPPFILT = False
24
+
25
+ if find_executable (CPPFILT ) is not None :
26
+ HAVE_CPPFILT = True
27
+
28
+ VERSION = '1.6'
31
29
__all__ = ['LcovCobertura' ]
32
30
33
31
32
+ class Demangler (object ):
33
+ def __init__ (self ):
34
+ self .pipe = subprocess .Popen (
35
+ CPPFILT , stdin = subprocess .PIPE , stdout = subprocess .PIPE )
36
+
37
+ def demangle (self , name ):
38
+ self .pipe .stdin .write (name + "\n " )
39
+ return self .pipe .stdout .readline ().rstrip ()
40
+
41
+
34
42
class LcovCobertura (object ):
35
43
"""
36
44
Converts code coverage report files in lcov format to Cobertura's XML
@@ -41,10 +49,10 @@ class LcovCobertura(object):
41
49
>>> LCOV_INPUT = 'your lcov input'
42
50
>>> converter = LcovCobertura(LCOV_INPUT)
43
51
>>> cobertura_xml = converter.convert()
44
- >>> print cobertura_xml
52
+ >>> print( cobertura_xml)
45
53
"""
46
54
47
- def __init__ (self , lcov_data , base_dir = '.' , excludes = None ):
55
+ def __init__ (self , lcov_data , base_dir = '.' , excludes = None , demangle = False ):
48
56
"""
49
57
Create a new :class:`LcovCobertura` object using the given `lcov_data`
50
58
and `options`.
@@ -55,13 +63,20 @@ def __init__(self, lcov_data, base_dir='.', excludes=None):
55
63
:type base_dir: string
56
64
:param excludes: list of regexes to packages as excluded
57
65
:type excludes: [string]
66
+ :param demangle: whether to demangle function names using c++filt
67
+ :type demangle: bool
58
68
"""
59
69
60
70
if not excludes :
61
71
excludes = []
62
72
self .lcov_data = lcov_data
63
73
self .base_dir = base_dir
64
74
self .excludes = excludes
75
+ if demangle :
76
+ demangler = Demangler ()
77
+ self .format = demangler .demangle
78
+ else :
79
+ self .format = lambda x : x
65
80
66
81
def convert (self ):
67
82
"""
@@ -119,17 +134,17 @@ def parse(self):
119
134
file_name = line_parts [- 1 ].strip ()
120
135
relative_file_name = os .path .relpath (file_name , self .base_dir )
121
136
package = '.' .join (relative_file_name .split (os .path .sep )[0 :- 1 ])
122
- class_name = file_name . split (os .path .sep )[ - 1 ]
137
+ class_name = '.' . join ( relative_file_name . split (os .path .sep ))
123
138
if package not in coverage_data ['packages' ]:
124
139
coverage_data ['packages' ][package ] = {
125
140
'classes' : {}, 'lines-total' : 0 , 'lines-covered' : 0 ,
126
141
'branches-total' : 0 , 'branches-covered' : 0
127
142
}
128
143
coverage_data ['packages' ][package ]['classes' ][
129
- relative_file_name ] = {
130
- 'name' : class_name , 'lines' : {}, 'lines-total' : 0 ,
131
- 'lines-covered' : 0 , 'branches-total' : 0 ,
132
- 'branches-covered' : 0
144
+ relative_file_name ] = {
145
+ 'name' : class_name , 'lines' : {}, 'lines-total' : 0 ,
146
+ 'lines-covered' : 0 , 'branches-total' : 0 ,
147
+ 'branches-covered' : 0
133
148
}
134
149
package = package
135
150
current_file = relative_file_name
@@ -177,12 +192,14 @@ def parse(self):
177
192
file_branches_covered = int (line_parts [1 ])
178
193
elif input_type == 'FN' :
179
194
# FN:5,(anonymous_1)
180
- function_name = line_parts [- 1 ].strip ().split (',' )[ 1 ]
181
- file_methods [function_name ] = '0'
195
+ function_line , function_name = line_parts [- 1 ].strip ().split (',' )
196
+ file_methods [function_name ] = [ function_line , '0' ]
182
197
elif input_type == 'FNDA' :
183
198
# FNDA:0,(anonymous_1)
184
199
(function_hits , function_name ) = line_parts [- 1 ].strip ().split (',' )
185
- file_methods [function_name ] = function_hits
200
+ if function_name not in file_methods :
201
+ file_methods [function_name ] = ['0' , '0' ]
202
+ file_methods [function_name ][- 1 ] = function_hits
186
203
187
204
# Exclude packages
188
205
excluded = [x for x in coverage_data ['packages' ] for e in self .excludes
@@ -211,7 +228,7 @@ def generate_cobertura_xml(self, coverage_data):
211
228
212
229
dom_impl = minidom .getDOMImplementation ()
213
230
doctype = dom_impl .createDocumentType ("coverage" , None ,
214
- "http://cobertura.sourceforge.net/xml/coverage-03 .dtd" )
231
+ "http://cobertura.sourceforge.net/xml/coverage-04 .dtd" )
215
232
document = dom_impl .createDocument (None , "coverage" , doctype )
216
233
root = document .documentElement
217
234
summary = coverage_data ['summary' ]
@@ -223,9 +240,10 @@ def generate_cobertura_xml(self, coverage_data):
223
240
'complexity' : '0' ,
224
241
'line-rate' : self ._percent (summary ['lines-total' ],
225
242
summary ['lines-covered' ]),
243
+ 'lines-covered' : str (summary ['lines-covered' ]),
226
244
'lines-valid' : str (summary ['lines-total' ]),
227
245
'timestamp' : coverage_data ['timestamp' ],
228
- 'version' : '1.9 '
246
+ 'version' : '2.0.3 '
229
247
})
230
248
231
249
sources = self ._el (document , 'sources' , {})
@@ -242,7 +260,8 @@ def generate_cobertura_xml(self, coverage_data):
242
260
package_el = self ._el (document , 'package' , {
243
261
'line-rate' : package_data ['line-rate' ],
244
262
'branch-rate' : package_data ['branch-rate' ],
245
- 'name' : package_name
263
+ 'name' : package_name ,
264
+ 'complexity' : '0' ,
246
265
})
247
266
classes_el = self ._el (document , 'classes' , {})
248
267
for class_name , class_data in list (package_data ['classes' ].items ()):
@@ -258,12 +277,21 @@ def generate_cobertura_xml(self, coverage_data):
258
277
259
278
# Process methods
260
279
methods_el = self ._el (document , 'methods' , {})
261
- for method_name , hits in list (class_data ['methods' ].items ()):
280
+ for method_name , ( line , hits ) in list (class_data ['methods' ].items ()):
262
281
method_el = self ._el (document , 'method' , {
263
- 'name' : method_name ,
282
+ 'name' : self . format ( method_name ) ,
264
283
'signature' : '' ,
265
- 'hits' : hits
284
+ 'line-rate' : '1.0' if int (hits ) > 0 else '0.0' ,
285
+ 'branch-rate' : '1.0' if int (hits ) > 0 else '0.0' ,
286
+ })
287
+ method_lines_el = self ._el (document , 'lines' , {})
288
+ method_line_el = self ._el (document , 'line' , {
289
+ 'hits' : hits ,
290
+ 'number' : line ,
291
+ 'branch' : 'false' ,
266
292
})
293
+ method_lines_el .appendChild (method_line_el )
294
+ method_el .appendChild (method_lines_el )
267
295
methods_el .appendChild (method_el )
268
296
269
297
# Process lines
@@ -334,44 +362,53 @@ def _percent(self, lines_total, lines_covered):
334
362
return '0.0'
335
363
return str (float (float (lines_covered ) / float (lines_total )))
336
364
337
- if __name__ == '__main__' :
338
- def main (argv ):
339
- """
340
- Converts LCOV coverage data to Cobertura-compatible XML for reporting.
341
365
342
- Usage :
343
- lcov_cobertura.py lcov-file.dat
344
- lcov_cobertura.py lcov-file.dat -b src/dir -e test.lib -o path/out.xml
366
+ def main ( argv = None ) :
367
+ """
368
+ Converts LCOV coverage data to Cobertura-compatible XML for reporting.
345
369
346
- By default, XML output will be written to ./coverage.xml
347
- """
370
+ Usage:
371
+ lcov_cobertura.py lcov-file.dat
372
+ lcov_cobertura.py lcov-file.dat -b src/dir -e test.lib -o path/out.xml
373
+
374
+ By default, XML output will be written to ./coverage.xml
375
+ """
376
+ if argv is None :
377
+ argv = sys .argv
378
+ parser = OptionParser ()
379
+ parser .usage = ('lcov_cobertura.py lcov-file.dat [-b source/dir] '
380
+ '[-e <exclude packages regex>] [-o output.xml] [-d]' )
381
+ parser .description = 'Converts lcov output to cobertura-compatible XML'
382
+ parser .add_option ('-b' , '--base-dir' , action = 'store' ,
383
+ help = 'Directory where source files are located' ,
384
+ dest = 'base_dir' , default = '.' )
385
+ parser .add_option ('-e' , '--excludes' ,
386
+ help = 'Comma-separated list of regexes of packages to exclude' ,
387
+ action = 'append' , dest = 'excludes' , default = [])
388
+ parser .add_option ('-o' , '--output' ,
389
+ help = 'Path to store cobertura xml file' ,
390
+ action = 'store' , dest = 'output' , default = 'coverage.xml' )
391
+ parser .add_option ('-d' , '--demangle' ,
392
+ help = 'Demangle C++ function names using %s' % CPPFILT ,
393
+ action = 'store_true' , dest = 'demangle' , default = False )
394
+ (options , args ) = parser .parse_args (args = argv )
395
+
396
+ if options .demangle and not HAVE_CPPFILT :
397
+ raise RuntimeError ("C++ filter executable (%s) not found!" % CPPFILT )
398
+
399
+ if len (args ) != 2 :
400
+ print (main .__doc__ )
401
+ sys .exit (1 )
402
+
403
+ try :
404
+ with open (args [1 ], 'r' ) as lcov_file :
405
+ lcov_data = lcov_file .read ()
406
+ lcov_cobertura = LcovCobertura (lcov_data , options .base_dir , options .excludes , options .demangle )
407
+ cobertura_xml = lcov_cobertura .convert ()
408
+ with open (options .output , mode = 'wt' ) as output_file :
409
+ output_file .write (cobertura_xml )
410
+ except IOError :
411
+ sys .stderr .write ("Unable to convert %s to Cobertura XML" % args [1 ])
348
412
349
- parser = OptionParser ()
350
- parser .usage = 'lcov_cobertura.py lcov-file.dat [-b source/dir] [-e <exclude packages regex>] [-o output.xml]'
351
- parser .description = 'Converts lcov output to cobertura-compatible XML'
352
- parser .add_option ('-b' , '--base-dir' , action = 'store' ,
353
- help = 'Directory where source files are located' ,
354
- dest = 'base_dir' , default = '.' )
355
- parser .add_option ('-e' , '--excludes' ,
356
- help = 'Comma-separated list of regexes of packages to exclude' ,
357
- action = 'append' , dest = 'excludes' , default = [])
358
- parser .add_option ('-o' , '--output' ,
359
- help = 'Path to store cobertura xml file' ,
360
- action = 'store' , dest = 'output' , default = 'coverage.xml' )
361
- (options , args ) = parser .parse_args (args = argv )
362
-
363
- if len (args ) != 2 :
364
- print ((main .__doc__ ))
365
- sys .exit (1 )
366
-
367
- try :
368
- with open (args [1 ], 'r' ) as lcov_file :
369
- lcov_data = lcov_file .read ()
370
- lcov_cobertura = LcovCobertura (lcov_data , options .base_dir , options .excludes )
371
- cobertura_xml = lcov_cobertura .convert ()
372
- with open (options .output , mode = 'wt' ) as output_file :
373
- output_file .write (cobertura_xml )
374
- except IOError :
375
- sys .stderr .write ("Unable to convert %s to Cobertura XML" % args [1 ])
376
-
377
- main (sys .argv )
413
+ if __name__ == '__main__' :
414
+ main ()
0 commit comments