Skip to content

Instantly share code, notes, and snippets.

@TeresaP
Forked from tgsoverly/merge-xml-coverage.py
Last active March 20, 2020 06:58
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TeresaP/a55d78b67636c5fd8464 to your computer and use it in GitHub Desktop.
Save TeresaP/a55d78b67636c5fd8464 to your computer and use it in GitHub Desktop.
Merge Cobertura XML's
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('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
:param obj:
:param attrs:
"""
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.
:param root:
:param list1:
:param list2:
:param attr:
:param merge_function: """
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.
:param package1:
:param package2:
"""
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.
:param class1:
:param class2:
"""
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.
:param method1:
:param method2:
"""
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.
:param line1:
:param line2:
""" # 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment