#!/usr/bin/python import logging import os import re import sys import xml.etree.ElementTree as ET from optparse import OptionParser ### This file came from the https://github.com/flow123d/flow123d repo they were nice enough to spend time to write this. ### It is copied here for other people to use on its own. # parse arguments newline = 10 * '\t' parser = OptionParser( usage='%prog [options] [file1 file2 ... filen]', version='%prog 1.0', epilog='If no files are specified all xml files in current directory will be selected. \n' + 'Useful when there is not known precise file name only location', ) parser.add_option( '-o', '--output', dest='filename', default='coverage-merged.xml', help='output file xml name', metavar='FILE', ) parser.add_option( '-p', '--path', dest='path', default='./', help='xml location, default current directory', metavar='FILE' ) parser.add_option( '-l', '--log', dest='loglevel', default='DEBUG', help='Log level DEBUG, INFO, WARNING, ERROR, CRITICAL' ) parser.add_option( '-f', '--filteronly', dest='filteronly', default=False, action='store_true', help='If set all files will be filtered by keep rules otherwise ' + 'all given files will be merged and filtered.', ) parser.add_option( '-s', '--suffix', dest='suffix', default='', help='Additional suffix which will be added to filtered files so they original files can be preserved', ) parser.add_option( '-k', '--keep', dest='packagefilters', default=None, metavar='NAME', action='append', help='preserves only specific packages. e.g.: ' + newline + "'python merge.py -k src.la.*'" + newline + 'will keep all packgages in folder ' + 'src/la/ and all subfolders of this folders. ' + newline + 'There can be mutiple rules e.g.:' + newline + "'python merge.py -k src.la.* -k unit_tests.la.'" + newline + 'Format of the rule is simple dot (.) separated names with wildcard (*) allowed, e.g: ' + newline + 'package.subpackage.*', ) (options, args) = parser.parse_args() # get arguments path = options.path xmlfiles = args loglevel = getattr(logging, options.loglevel.upper()) finalxml = os.path.join(path, options.filename) filteronly = options.filteronly filtersuffix = options.suffix packagefilters = options.packagefilters logging.basicConfig(level=loglevel, format='%(levelname)s %(asctime)s: %(message)s', datefmt='%x %X') if not xmlfiles: for filename in os.listdir(path): if not filename.endswith('.xml'): continue fullname = os.path.join(path, filename) if fullname == finalxml: continue xmlfiles.append(fullname) if not xmlfiles: print('No xml files found!') sys.exit(1) else: xmlfiles = [path + filename for filename in xmlfiles] # constants PACKAGES_LIST = 'packages/package' PACKAGES_ROOT = 'packages' CLASSES_LIST = 'classes/class' CLASSES_ROOT = 'classes' METHODS_LIST = 'methods/method' METHODS_ROOT = 'methods' LINES_LIST = 'lines/line' LINES_ROOT = 'lines' def merge_xml(xmlfile1, xmlfile2, outputfile): # parse xml1 = ET.parse(xmlfile1) xml2 = ET.parse(xmlfile2) # get packages packages1 = filter_xml(xml1) packages2 = filter_xml(xml2) # find root packages1root = xml1.find(PACKAGES_ROOT) # merge packages merge(packages1root, packages1, packages2, 'name', merge_packages) # write result to output file xml1.write(outputfile, encoding='UTF-8', xml_declaration=True) def filter_xml(xmlfile): xmlroot = xmlfile.getroot() packageroot = xmlfile.find(PACKAGES_ROOT) packages = xmlroot.findall(PACKAGES_LIST) # delete nodes from tree AND from list included = [] if packagefilters: logging.debug('excluding packages:') for pckg in packages: name = pckg.get('name') if not include_package(name): logging.debug(f'excluding package "{name}"') packageroot.remove(pckg) else: included.append(pckg) return included def prepare_packagefilters(): if not packagefilters: return None # create simple regexp from given filter for i in range(len(packagefilters)): packagefilters[i] = '^' + packagefilters[i].replace('.', r'\.').replace('*', '.*') + '$' def include_package(name): if not packagefilters: return True for packagefilter in packagefilters: if re.search(packagefilter, name): return True return False def get_attributes_chain(obj, attrs): """Return a joined arguments of object based on given arguments""" if type(attrs) is list: result = '' for attr in attrs: result += obj.attrib[attr] return result else: return obj.attrib[attrs] def merge(root, list1, list2, attr, merge_function): """Groups given lists based on group attributes. Process of merging items with same key is handled by passed merge_function. Returns list1.""" for item2 in list2: found = False for item1 in list1: if get_attributes_chain(item1, attr) == get_attributes_chain(item2, attr): item1 = merge_function(item1, item2) found = True break if found: continue else: root.append(item2) def merge_packages(package1, package2): """Merges two packages. Returns package1.""" classes1 = package1.findall(CLASSES_LIST) classes2 = package2.findall(CLASSES_LIST) if classes1 or classes2: merge(package1.find(CLASSES_ROOT), classes1, classes2, ['filename', 'name'], merge_classes) return package1 def merge_classes(class1, class2): """Merges two classes. Returns class1.""" lines1 = class1.findall(LINES_LIST) lines2 = class2.findall(LINES_LIST) if lines1 or lines2: merge(class1.find(LINES_ROOT), lines1, lines2, 'number', merge_lines) methods1 = class1.findall(METHODS_LIST) methods2 = class2.findall(METHODS_LIST) if methods1 or methods2: merge(class1.find(METHODS_ROOT), methods1, methods2, 'name', merge_methods) return class1 def merge_methods(method1, method2): """Merges two methods. Returns method1.""" lines1 = method1.findall(LINES_LIST) lines2 = method2.findall(LINES_LIST) merge(method1.find(LINES_ROOT), lines1, lines2, 'number', merge_lines) def merge_lines(line1, line2): """Merges two lines by summing their hits. Returns line1.""" # merge hits value = int(line1.get('hits')) + int(line2.get('hits')) line1.set('hits', str(value)) # merge conditionals con1 = line1.get('condition-coverage') con2 = line2.get('condition-coverage') if con1 is not None and con2 is not None: con1value = int(con1.split('%')[0]) con2value = int(con2.split('%')[0]) # bigger coverage on second line, swap their conditionals if con2value > con1value: line1.set('condition-coverage', str(con2)) line1.__setitem__(0, line2.__getitem__(0)) return line1 # prepare filters prepare_packagefilters() if filteronly: # filter all given files currfile = 1 totalfiles = len(xmlfiles) for xmlfile in xmlfiles: xml = ET.parse(xmlfile) filter_xml(xml) logging.debug(f'{currfile}/{totalfiles} filtering: {xmlfile}') xml.write(xmlfile + filtersuffix, encoding='UTF-8', xml_declaration=True) currfile += 1 else: # merge all given files totalfiles = len(xmlfiles) # special case if only one file was given # filter given file and save it if totalfiles == 1: logging.warning('Only one file given!') xmlfile = xmlfiles.pop(0) xml = ET.parse(xmlfile) filter_xml(xml) xml.write(finalxml, encoding='UTF-8', xml_declaration=True) sys.exit(0) currfile = 1 logging.debug(f'{currfile}/{totalfiles - 1} merging: {xmlfiles[0]} & {xmlfiles[1]}') merge_xml(xmlfiles[0], xmlfiles[1], finalxml) currfile = 2 for i in range(totalfiles - 2): xmlfile = xmlfiles[i + 2] logging.debug(f'{currfile}/{totalfiles - 1} merging: {finalxml} & {xmlfile}') merge_xml(finalxml, xmlfile, finalxml) currfile += 1