2
""" Calculate autopilot pass rate
4
# Copyright (C) 2012-2013, Canonical Ltd (http://www.canonical.com/)
6
# Author: Jean-Baptiste Lallement <jean-baptiste.lallement@canonical.com>
8
# This software is free software: you can redistribute it
9
# and/or modify it under the terms of the GNU General Public License
10
# as published by the Free Software Foundation, either version 3 of
11
# the License, or (at your option) any later version.
13
# This software is distributed in the hope that it will
14
# be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
15
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
# GNU General Public License for more details.
18
# You should have received a copy of the GNU General Public License
19
# along with this software. If not, see <http://www.gnu.org/licenses/>.
25
import xml.etree.ElementTree as etree
29
from datetime import datetime
30
from shutil import copyfile
32
BINDIR = os.path.dirname(__file__)
33
DEFAULT_LOGNAME = 'autopilot_result'
35
'history': '/tmp/cu2d-logs',
50
""" Parse a JUnit log file and return the number of tests, and a list of
53
:param path: path to an XML log file
54
:return: (test count, [failed tests])
56
logging.debug('Parsing file: %s', path)
58
if not os.path.exists(path):
59
logging.error('File doesn\'t exists: %s', path)
62
tree = etree.parse(path)
65
cases = root.findall('suites/suite/cases/case')
70
testcount = len(cases)
73
skipped = case.find('skipped')
74
classname = case.find('className')
75
testname = case.find('testName')
80
stacktrace = case.find('errorStackTrace')
81
if stacktrace is not None:
82
failed.append(classname.text + "::" + testname.text)
84
logging.debug("Test failures:\n\t{}".format("\n\t".join(failed)))
85
logging.debug("%d tests failed", (len(failed)))
86
return (testcount, skipcount, failed)
89
def set_logging(debugmode=False):
90
"""Initialize logging"""
92
level=logging.DEBUG if debugmode else logging.INFO,
93
format="%(asctime)s %(levelname)s %(message)s")
94
logging.debug('Debug mode enabled')
97
def load_config(path):
98
'''Load configuration file
100
:param path: path to configuration file
102
config = ConfigParser.SafeConfigParser()
104
logging.debug('Loading configuration from %s', path)
105
path = os.path.expanduser(path)
106
if not os.path.exists(path):
107
logging.error('Configuration file doesn\'t exists: %s', path)
112
for k, v in DEFAULT_CFG.iteritems():
114
logging.debug('Loading option %s', k)
115
cfg[k] = config.get('DEFAULT', k, raw=True)
116
except ConfigParser.NoOptionError:
117
logging.debug('option not found')
120
logging.debug('Configuration loaded:\n%s', cfg)
124
def md5sum(filename):
125
''' Calculate md5 checksum of filename '''
127
with open(filename, 'rb') as f:
128
for chunk in iter(lambda: f.read(128 * md5.block_size), b''):
130
return md5.hexdigest()
133
def archive_log(histdir, lognew):
134
''' Copy logfile to histdir if not already archived '''
136
histdir = os.path.abspath(os.path.expanduser(histdir))
137
if not os.path.exists(histdir):
138
logging.debug('Creating archive directory %s', histdir)
141
except OSError as exc:
142
logging.error('Failed to create directory: %s . %s. Exiting!',
145
elif not os.path.isdir(histdir):
146
logging.error('\'%s\' exists and is not a directory. Aborting!',
149
filelist = glob.glob(os.path.join(histdir, '*'))
152
md5_new = md5sum(lognew)
156
logprev = filelist[-1]
157
md5_prev = md5sum(logprev)
158
if md5_prev == md5_new:
161
'File already exist in archive. Reusing existing file.')
162
if len(filelist) > 1:
163
# More than 1 file in history and file already there just
165
logging.debug('Files found: %s', filelist[-2:])
166
return(filelist[-2:])
168
return(None, filelist[-1])
173
destpath = os.path.join(histdir, DEFAULT_LOGNAME + '.' +
174
datetime.now().strftime('%Y%m%d-%H%M%S.%f') +
176
logging.debug('Copying %s -> %s', lognew, destpath)
177
copyfile(lognew, destpath)
178
return(logprev, lognew)
181
def goal_reached(target, total, count, category):
182
''' Check if value is below target.
184
:param target: Value must be below target to succeed. Can be a percentage
185
of total or an absolute value
186
:param total: Max value
187
:param count: Current value
189
if target[-1] == '%':
190
goal = float(target[:-1]) * total / 100
194
logging.debug('Checking for %s: Goal: %f, Count: %f', category, goal, count)
195
return(count <= goal)
198
def compute_stat(cfg, new, old):
199
''' Calculate stats for latest and previous results, compare to the goal
200
and exit with non-zero if goal is not reached
202
:param cfg: Configration
203
:param new: new results
204
:param old: Previous results
208
logging.debug('Calculating stats with:')
209
dtotal = new['total'] - old['total']
210
dskip = new['skip'] - old['skip']
211
dfail = len(new['fail']) - len(old['fail'])
213
trun = new['total'] - new['skip']
214
toldrun = old['total'] - old['skip']
215
pfail = float(len(new['fail'])) / trun
216
pregr = float(dfail) / trun
218
ptotal = float(dtotal) / toldrun
219
except ZeroDivisionError:
221
pskip = float(new['skip']) / trun
223
sys.stdout.write('count | total: {:4d} | skip:{:4d} | failures: {:4d}\n'.format(
224
new['total'], new['skip'], len(new['fail'])))
225
sys.stdout.write('delta | dtotal: {:+4d} | tskip: {:+4d} | dfailures: {:+4d}\n'.format(
226
dtotal, dskip, dfail))
227
sys.stdout.write('ratio | ptotal: {:+4.3f} | pskip: {:+4.3f} | pfailures: {:+4.3f} | regressions: {:+4.3f}\n'.format(
228
ptotal, pskip, pfail, pregr))
232
if not goal_reached(cfg['failure'], trun, len(new['fail']), "failures"):
234
if not goal_reached(cfg['regression'], trun, dfail, "regression"):
236
if not goal_reached(cfg['skip'], trun, new['skip'], "skip"):
238
if not goal_reached(cfg['removal'], toldrun, -dtotal, "removal"):
246
parser = argparse.ArgumentParser(
247
formatter_class=argparse.RawTextHelpFormatter,
249
Calculate autopilot pass rate and regression rate.
251
3 lines of results are displayed: absolute count, difference with
252
previous run and percentage. The output is:
254
Absolute values count [total] [skip] [fail]
255
Delta with prev. run delta [+/-total] [+/-skip] [+/-fail]
256
Percentage ratio [%total] [%skip] [%fail] [%reg]
258
parser.add_argument('-A', '--no-archive', action='store_true',
260
help='Do not process archived log file')
261
parser.add_argument('-C', '--config',
262
help='Path to configuration file')
263
parser.add_argument('-d', '--debug', action='store_true', default=False,
264
help='enable debug mode')
265
parser.add_argument('logfile', help='autopilot result file in Junit '
267
parser.add_argument('systemid', default=DEFAULT_SYSID,
268
help='Unique ID. This id is used to archive the '
269
'results in a unique location when the same testsuite '
270
'is run for several configurations (default: \'%s\')' %
273
args = parser.parse_args()
274
set_logging(args.debug)
275
config = load_config(args.config)
277
if not os.path.exists(args.logfile):
278
logging.error('File doesn\'t exists: %s', args.logfile)
281
prevlog = currlog = None
282
if not args.no_archive:
283
(prevlog, currlog) = archive_log(
284
os.path.join(config['history'], args.systemid), args.logfile)
286
currlog = args.logfile
294
logging.debug('Parsing latest log file')
295
(nresult['total'], nresult['skip'], nresult['fail']) = parselog(currlog)
296
presult = dict(nresult)
298
logging.debug('Parsing previous log file')
299
(presult['total'], presult['skip'], presult['fail']) = parselog(
302
ret = compute_stat(config, nresult, presult)
304
logging.error('Check failed with exit code %d' % ret)
308
if __name__ == "__main__":