~larry-e-works/uci-engine/amqp-to-kombu

« back to all changes in this revision

Viewing changes to cupstream2distro/cu2d-autopilot-report

  • Committer: Francis Ginther
  • Date: 2014-06-10 20:42:46 UTC
  • mto: This revision was merged to the branch mainline in revision 571.
  • Revision ID: francis.ginther@canonical.com-20140610204246-b1bsrik7nlcolqy7
Import lp:cupstream2distro rev 605.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#! /usr/bin/python
 
2
""" Calculate autopilot pass rate
 
3
"""
 
4
# Copyright (C) 2012-2013, Canonical Ltd (http://www.canonical.com/)
 
5
#
 
6
# Author: Jean-Baptiste Lallement <jean-baptiste.lallement@canonical.com>
 
7
#
 
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.
 
12
#
 
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.
 
17
#
 
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/>.
 
20
 
 
21
import os
 
22
import logging
 
23
import sys
 
24
import argparse
 
25
import xml.etree.ElementTree as etree
 
26
import ConfigParser
 
27
import glob
 
28
import hashlib
 
29
from datetime import datetime
 
30
from shutil import copyfile
 
31
 
 
32
BINDIR = os.path.dirname(__file__)
 
33
DEFAULT_LOGNAME = 'autopilot_result'
 
34
DEFAULT_CFG = {
 
35
    'history': '/tmp/cu2d-logs',
 
36
    'failure': '5%',
 
37
    'regression': '0.1%',
 
38
    'skip': '3%',
 
39
    'removal': '0'
 
40
}
 
41
DEFAULT_SYSID = ''
 
42
 
 
43
EFAILURE = 1 << 1
 
44
EREGRESS = 1 << 2
 
45
ESKIP = 1 << 3
 
46
EREMOVAL = 1 << 4
 
47
 
 
48
 
 
49
def parselog(path):
 
50
    """ Parse a JUnit log file and return the number of tests, and a list of
 
51
    failed tests
 
52
 
 
53
    :param path: path to an XML log file
 
54
    :return: (test count, [failed tests])
 
55
    """
 
56
    logging.debug('Parsing file: %s', path)
 
57
 
 
58
    if not os.path.exists(path):
 
59
        logging.error('File doesn\'t exists: %s', path)
 
60
        sys.exit(1)
 
61
 
 
62
    tree = etree.parse(path)
 
63
    root = tree.getroot()
 
64
 
 
65
    cases = root.findall('suites/suite/cases/case')
 
66
 
 
67
    testcount = 0
 
68
    skipcount = 0
 
69
    failed = []
 
70
    testcount = len(cases)
 
71
 
 
72
    for case in cases:
 
73
        skipped = case.find('skipped')
 
74
        classname = case.find('className')
 
75
        testname = case.find('testName')
 
76
 
 
77
        if 'true' in skipped:
 
78
            skipcount += 1
 
79
 
 
80
        stacktrace = case.find('errorStackTrace')
 
81
        if stacktrace is not None:
 
82
            failed.append(classname.text + "::" + testname.text)
 
83
 
 
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)
 
87
 
 
88
 
 
89
def set_logging(debugmode=False):
 
90
    """Initialize logging"""
 
91
    logging.basicConfig(
 
92
        level=logging.DEBUG if debugmode else logging.INFO,
 
93
        format="%(asctime)s %(levelname)s %(message)s")
 
94
    logging.debug('Debug mode enabled')
 
95
 
 
96
 
 
97
def load_config(path):
 
98
    '''Load configuration file
 
99
 
 
100
    :param path: path to configuration file
 
101
    '''
 
102
    config = ConfigParser.SafeConfigParser()
 
103
    if path:
 
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)
 
108
            sys.exit(1)
 
109
        config.read(path)
 
110
 
 
111
    cfg = {}
 
112
    for k, v in DEFAULT_CFG.iteritems():
 
113
        try:
 
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')
 
118
            cfg[k] = v
 
119
 
 
120
    logging.debug('Configuration loaded:\n%s', cfg)
 
121
    return cfg
 
122
 
 
123
 
 
124
def md5sum(filename):
 
125
    ''' Calculate md5 checksum of filename '''
 
126
    md5 = hashlib.md5()
 
127
    with open(filename, 'rb') as f:
 
128
        for chunk in iter(lambda: f.read(128 * md5.block_size), b''):
 
129
            md5.update(chunk)
 
130
    return md5.hexdigest()
 
131
 
 
132
 
 
133
def archive_log(histdir, lognew):
 
134
    ''' Copy logfile to histdir if not already archived '''
 
135
 
 
136
    histdir = os.path.abspath(os.path.expanduser(histdir))
 
137
    if not os.path.exists(histdir):
 
138
        logging.debug('Creating archive directory %s', histdir)
 
139
        try:
 
140
            os.makedirs(histdir)
 
141
        except OSError as exc:
 
142
            logging.error('Failed to create directory: %s . %s. Exiting!',
 
143
                          histdir, exc)
 
144
            sys.exit(1)
 
145
    elif not os.path.isdir(histdir):
 
146
        logging.error('\'%s\' exists and is not a directory. Aborting!',
 
147
                      histdir)
 
148
 
 
149
    filelist = glob.glob(os.path.join(histdir, '*'))
 
150
    filelist.sort()
 
151
 
 
152
    md5_new = md5sum(lognew)
 
153
    logprev = None
 
154
 
 
155
    try:
 
156
        logprev = filelist[-1]
 
157
        md5_prev = md5sum(logprev)
 
158
        if md5_prev == md5_new:
 
159
            # Log already there
 
160
            logging.debug(
 
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
 
164
                # return
 
165
                logging.debug('Files found: %s', filelist[-2:])
 
166
                return(filelist[-2:])
 
167
            else:
 
168
                return(None, filelist[-1])
 
169
    except IndexError:
 
170
        # Nothing in history
 
171
        pass
 
172
 
 
173
    destpath = os.path.join(histdir, DEFAULT_LOGNAME + '.' +
 
174
                            datetime.now().strftime('%Y%m%d-%H%M%S.%f') +
 
175
                            '.xml')
 
176
    logging.debug('Copying %s -> %s', lognew, destpath)
 
177
    copyfile(lognew, destpath)
 
178
    return(logprev, lognew)
 
179
 
 
180
 
 
181
def goal_reached(target, total, count, category):
 
182
    ''' Check if value is below target.
 
183
 
 
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
 
188
    '''
 
189
    if target[-1] == '%':
 
190
        goal = float(target[:-1]) * total / 100
 
191
    else:
 
192
        goal = float(target)
 
193
 
 
194
    logging.debug('Checking for %s: Goal: %f, Count: %f', category, goal, count)
 
195
    return(count <= goal)
 
196
 
 
197
 
 
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
 
201
 
 
202
    :param cfg: Configration
 
203
    :param new: new results
 
204
    :param old: Previous results
 
205
    '''
 
206
    ret = 0
 
207
 
 
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'])
 
212
 
 
213
    trun = new['total'] - new['skip']
 
214
    toldrun = old['total'] - old['skip']
 
215
    pfail = float(len(new['fail'])) / trun
 
216
    pregr = float(dfail) / trun
 
217
    try:
 
218
        ptotal = float(dtotal) / toldrun
 
219
    except ZeroDivisionError:
 
220
        ptotal = 1
 
221
    pskip = float(new['skip']) / trun
 
222
 
 
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))
 
229
    sys.stdout.flush()
 
230
 
 
231
    # Result analysis
 
232
    if not goal_reached(cfg['failure'], trun, len(new['fail']), "failures"):
 
233
        ret += EFAILURE
 
234
    if not goal_reached(cfg['regression'], trun, dfail, "regression"):
 
235
        ret += EREGRESS
 
236
    if not goal_reached(cfg['skip'], trun, new['skip'], "skip"):
 
237
        ret += ESKIP
 
238
    if not goal_reached(cfg['removal'], toldrun, -dtotal, "removal"):
 
239
        ret += EREMOVAL
 
240
 
 
241
    return ret
 
242
 
 
243
 
 
244
def main():
 
245
    ''' Main routine '''
 
246
    parser = argparse.ArgumentParser(
 
247
        formatter_class=argparse.RawTextHelpFormatter,
 
248
        description='''
 
249
        Calculate autopilot pass rate and regression rate.
 
250
 
 
251
        3 lines of results are displayed: absolute count, difference with
 
252
        previous run and percentage. The output is:
 
253
 
 
254
        Absolute values        count   [total]    [skip]    [fail]
 
255
        Delta with prev. run   delta   [+/-total] [+/-skip] [+/-fail]
 
256
        Percentage             ratio   [%total]   [%skip]   [%fail]   [%reg]
 
257
        ''')
 
258
    parser.add_argument('-A', '--no-archive', action='store_true',
 
259
                        default=False,
 
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 '
 
266
                        'format')
 
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\')' %
 
271
                        DEFAULT_SYSID)
 
272
 
 
273
    args = parser.parse_args()
 
274
    set_logging(args.debug)
 
275
    config = load_config(args.config)
 
276
 
 
277
    if not os.path.exists(args.logfile):
 
278
        logging.error('File doesn\'t exists: %s', args.logfile)
 
279
        sys.exit(1)
 
280
 
 
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)
 
285
    else:
 
286
        currlog = args.logfile
 
287
 
 
288
    nresult = {
 
289
        'total': 0,
 
290
        'skip': 0,
 
291
        'fail': []
 
292
    }
 
293
 
 
294
    logging.debug('Parsing latest log file')
 
295
    (nresult['total'], nresult['skip'], nresult['fail']) = parselog(currlog)
 
296
    presult = dict(nresult)
 
297
    if prevlog:
 
298
        logging.debug('Parsing previous log file')
 
299
        (presult['total'], presult['skip'], presult['fail']) = parselog(
 
300
            prevlog)
 
301
 
 
302
    ret = compute_stat(config, nresult, presult)
 
303
    if (ret > 0):
 
304
        logging.error('Check failed with exit code %d' % ret)
 
305
    sys.exit(ret)
 
306
 
 
307
 
 
308
if __name__ == "__main__":
 
309
    main()