# This file is part of MAUS: http://micewww.pp.rl.ac.uk/projects/maus # # MAUS is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # MAUS is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with MAUS. If not, see . """ Handler for lcov output. Parses the lcov output to generate a list of coverage items; then we use a series of filters to select the appropriate files on which to report; and a function to sum over the list to return total reporting numbers for the list. """ import copy import os import json import HTMLParser CPP_COV = os.path.expandvars('$MAUS_ROOT_DIR/doc/cpp_coverage') class Coverage(): # pylint: disable=R0903 """ Data structure reflecting lcov coverage information """ def __init__(self): """Initialise data to 0.""" self.file = "" self.lines = {"percentage":0., "covered":0., "total":0.} self.functions = copy.deepcopy(self.lines) self.branches = copy.deepcopy(self.lines) def json_repr(self): """Return json representation of the data""" return { "file":self.file, "line":self.lines, "function":self.functions, "branch":self.branches } def __str__(self): """String representation of the data is a json.dumps of json_repr""" return json.dumps(self.json_repr()) class CoverageParser(HTMLParser.HTMLParser): # pylint: disable=R0904 """ Parse a lcov coverage html file to extract test coverage information To parse a file, use parse_file(file_name) which returns a list of Coverage objects. HTMLParser module uses a linear mode to parse html. Makes it quite horrible to figure out where in the html structure I am, especially with e.g. nested tables and such like. I use an obscure system of flags to do it - not nice. """ def __init__(self): """Initialise coverage and some flags""" HTMLParser.HTMLParser.__init__(self) self.coverage = [] self.td_index = -1 self.in_td = -1 self.in_a = 0 def handle_starttag(self, tag, attrs_list): """ Handler for opening e.g. tag Handles tr, td, a tag """ attrs_dict = {} for attr in attrs_list: attrs_dict[attr[0]] = attr[1] if tag == "tr": self.handle_tr_start(tag, attrs_dict) if tag == "td": self.handle_td_start(tag, attrs_dict) if tag == "a": self.in_a += 1 def handle_endtag(self, tag): """ Handler for closing e.g. tag Handles tr, td, a tag """ if tag == "tr": if self.td_index == -1: self.coverage.pop() if tag == "a": self.in_a -= 1 if tag == "td": self.in_td = -1 def handle_tr_start(self, tag, attrs_dict): # pylint: disable=W0613 """Handler for new table row""" if self.td_index > -1 and self.td_index < 3: return self.coverage.append(Coverage()) self.td_index = -1 def handle_td_start(self, tag, attrs_dict): # pylint: disable=W0613 """Handler for new table element""" if 'class' in attrs_dict: if attrs_dict['class'][0:5] == 'cover' and \ attrs_dict['class'][5:8] != 'Bar': self.td_index += 1 self.in_td = 0 def handle_data(self, data): """Handler for data""" if self.td_index == -1: return if self.in_td != 0: return if self.td_index == 0: if self.in_a > 0: self.coverage[-1].file = data if self.td_index == 1: self.coverage[-1].lines['percentage'] = data if self.td_index == 2: words = data.split('/') self.coverage[-1].lines['covered'] = int(words[0]) self.coverage[-1].lines['total'] = int(words[1]) if self.td_index == 3: self.coverage[-1].functions['percentage'] = data if self.td_index == 4: words = data.split('/') self.coverage[-1].functions['covered'] = int(words[0]) self.coverage[-1].functions['total'] = int(words[1]) if self.td_index == 5: self.coverage[-1].branches['percentage'] = data if self.td_index == 6: words = data.split('/') self.coverage[-1].branches['covered'] = int(words[0]) self.coverage[-1].branches['total'] = int(words[1]) if self.in_td > -1: self.in_td += 1 def parse_file(self, file_name): """Parse a file with the specified file name""" html_file = open(file_name) for line in html_file.readlines(): self.feed(line) return self.coverage def total_coverage(coverage_items): """ Sum the coverage of all items in the coverage_items list and return a Coverage() item with appropriate elements """ cover_sum = Coverage() cover_sum.file = 'total' for item in 'lines', 'functions', 'branches': covered = 0 total = 0 for coverage in coverage_items: covered += coverage.__dict__[item]['covered'] total += coverage.__dict__[item]['total'] cover_sum.__dict__[item]['covered'] = covered cover_sum.__dict__[item]['total'] = total cover_sum.__dict__[item]['percentage'] = float(covered)/float(total) return cover_sum def maus_filter(coverage_item): """ Return true for items that are in MAUS (i.e. not stl and third_party). Else return false """ if coverage_item.file[0:3] != 'src': return False if coverage_item.file[-5:] == 'build': return False if coverage_item.file.find('MausDataStructure.') > -1: return False return True def datastructure_filter(coverage_item): """ Return true for items that are in the DataStructure directory """ if coverage_item.file.find('src/common_cpp/DataStructure') == 0: return True return False def datastructure_getter(): """ Datastructure needs special handling to ignore ROOT generated MausDataStructure code. """ ds_list = CoverageParser().parse_file( CPP_COV+'/src/common_cpp/DataStructure/index.html') ds_list = [ds for ds in ds_list if ds.file.find('MausDataStructure.') == -1] return total_coverage(ds_list) def non_legacy_filter(coverage_item): """ Return true for items that are not legacy (i.e. not in src/legacy). Else return false """ if coverage_item.file[4:10] == 'legacy': return False return True def main(): """ Extract coverage information from the lcov output and make a few different summary reports """ coverage_list = CoverageParser().parse_file( os.path.expandvars('$MAUS_ROOT_DIR/doc/cpp_coverage/index.html') ) maus_coverage_list = [x for x in coverage_list if maus_filter(x)] maus_coverage_list = [x for x in maus_coverage_list \ if not datastructure_filter(x)] maus_coverage_list.append(datastructure_getter()) for item in maus_coverage_list: print item.file, item.json_repr()['line']['percentage'] print 'ALL MAUS\n', total_coverage(maus_coverage_list) non_legacy_list = [x for x in maus_coverage_list if non_legacy_filter(x)] print 'NON-LEGACY MAUS\n', total_coverage(non_legacy_list) legacy_list = [x for x in maus_coverage_list if not non_legacy_filter(x)] print 'LEGACY MAUS\n', total_coverage(legacy_list) if __name__ == "__main__": main()