33# Copyright 2011-2012 Eric Wendelin
44#
55# 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.
187
198"""
209Converts lcov line coverage output to Cobertura-compatible XML for CI
2413import sys
2514import os
2615import time
16+ import subprocess
2717from xml .dom import minidom
2818from optparse import OptionParser
2919
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'
3129__all__ = ['LcovCobertura' ]
3230
3331
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+
3442class LcovCobertura (object ):
3543 """
3644 Converts code coverage report files in lcov format to Cobertura's XML
@@ -41,10 +49,10 @@ class LcovCobertura(object):
4149 >>> LCOV_INPUT = 'your lcov input'
4250 >>> converter = LcovCobertura(LCOV_INPUT)
4351 >>> cobertura_xml = converter.convert()
44- >>> print cobertura_xml
52+ >>> print( cobertura_xml)
4553 """
4654
47- def __init__ (self , lcov_data , base_dir = '.' , excludes = None ):
55+ def __init__ (self , lcov_data , base_dir = '.' , excludes = None , demangle = False ):
4856 """
4957 Create a new :class:`LcovCobertura` object using the given `lcov_data`
5058 and `options`.
@@ -55,13 +63,20 @@ def __init__(self, lcov_data, base_dir='.', excludes=None):
5563 :type base_dir: string
5664 :param excludes: list of regexes to packages as excluded
5765 :type excludes: [string]
66+ :param demangle: whether to demangle function names using c++filt
67+ :type demangle: bool
5868 """
5969
6070 if not excludes :
6171 excludes = []
6272 self .lcov_data = lcov_data
6373 self .base_dir = base_dir
6474 self .excludes = excludes
75+ if demangle :
76+ demangler = Demangler ()
77+ self .format = demangler .demangle
78+ else :
79+ self .format = lambda x : x
6580
6681 def convert (self ):
6782 """
@@ -119,17 +134,17 @@ def parse(self):
119134 file_name = line_parts [- 1 ].strip ()
120135 relative_file_name = os .path .relpath (file_name , self .base_dir )
121136 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 ))
123138 if package not in coverage_data ['packages' ]:
124139 coverage_data ['packages' ][package ] = {
125140 'classes' : {}, 'lines-total' : 0 , 'lines-covered' : 0 ,
126141 'branches-total' : 0 , 'branches-covered' : 0
127142 }
128143 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
133148 }
134149 package = package
135150 current_file = relative_file_name
@@ -177,12 +192,14 @@ def parse(self):
177192 file_branches_covered = int (line_parts [1 ])
178193 elif input_type == 'FN' :
179194 # 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' ]
182197 elif input_type == 'FNDA' :
183198 # FNDA:0,(anonymous_1)
184199 (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
186203
187204 # Exclude packages
188205 excluded = [x for x in coverage_data ['packages' ] for e in self .excludes
@@ -211,7 +228,7 @@ def generate_cobertura_xml(self, coverage_data):
211228
212229 dom_impl = minidom .getDOMImplementation ()
213230 doctype = dom_impl .createDocumentType ("coverage" , None ,
214- "http://cobertura.sourceforge.net/xml/coverage-03 .dtd" )
231+ "http://cobertura.sourceforge.net/xml/coverage-04 .dtd" )
215232 document = dom_impl .createDocument (None , "coverage" , doctype )
216233 root = document .documentElement
217234 summary = coverage_data ['summary' ]
@@ -223,9 +240,10 @@ def generate_cobertura_xml(self, coverage_data):
223240 'complexity' : '0' ,
224241 'line-rate' : self ._percent (summary ['lines-total' ],
225242 summary ['lines-covered' ]),
243+ 'lines-covered' : str (summary ['lines-covered' ]),
226244 'lines-valid' : str (summary ['lines-total' ]),
227245 'timestamp' : coverage_data ['timestamp' ],
228- 'version' : '1.9 '
246+ 'version' : '2.0.3 '
229247 })
230248
231249 sources = self ._el (document , 'sources' , {})
@@ -242,7 +260,8 @@ def generate_cobertura_xml(self, coverage_data):
242260 package_el = self ._el (document , 'package' , {
243261 'line-rate' : package_data ['line-rate' ],
244262 'branch-rate' : package_data ['branch-rate' ],
245- 'name' : package_name
263+ 'name' : package_name ,
264+ 'complexity' : '0' ,
246265 })
247266 classes_el = self ._el (document , 'classes' , {})
248267 for class_name , class_data in list (package_data ['classes' ].items ()):
@@ -258,12 +277,21 @@ def generate_cobertura_xml(self, coverage_data):
258277
259278 # Process methods
260279 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 ()):
262281 method_el = self ._el (document , 'method' , {
263- 'name' : method_name ,
282+ 'name' : self . format ( method_name ) ,
264283 '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' ,
266292 })
293+ method_lines_el .appendChild (method_line_el )
294+ method_el .appendChild (method_lines_el )
267295 methods_el .appendChild (method_el )
268296
269297 # Process lines
@@ -334,44 +362,53 @@ def _percent(self, lines_total, lines_covered):
334362 return '0.0'
335363 return str (float (float (lines_covered ) / float (lines_total )))
336364
337- if __name__ == '__main__' :
338- def main (argv ):
339- """
340- Converts LCOV coverage data to Cobertura-compatible XML for reporting.
341365
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.
345369
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 ])
348412
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