summaryrefslogtreecommitdiffstats
path: root/merge-coverage.py
blob: 9e2f804dae690525bdd40fa5f3fb7d5506567010 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
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