diff --git a/merge-coverage.py b/merge-coverage.py new file mode 100755 index 0000000..9e2f804 --- /dev/null +++ b/merge-coverage.py @@ -0,0 +1,260 @@ +#!/usr/bin/python +import sys +import os +import xml.etree.ElementTree as ET +import logging +import re +from shutil import copyfile +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 ('excluding package "{0}"'.format(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 ('.', '\.').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 ('{1}/{2} filtering: {0}'.format (xmlfile, currfile, totalfiles)) + 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 ('{2}/{3} merging: {0} & {1}'.format (xmlfiles[0], xmlfiles[1], currfile, totalfiles-1)) + merge_xml (xmlfiles[0], xmlfiles[1], finalxml) + + + currfile = 2 + for i in range (totalfiles-2): + xmlfile = xmlfiles[i+2] + logging.debug ('{2}/{3} merging: {0} & {1}'.format (finalxml, xmlfile, currfile, totalfiles-1)) + merge_xml (finalxml, xmlfile, finalxml) + currfile += 1