| 1 | # -*- coding: utf-8 -*- |
---|
| 2 | # |
---|
| 3 | # Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> |
---|
| 4 | # Copyright (C) 2008 Matt Good <matt@matt-good.net> |
---|
| 5 | # Copyright (C) 2008-2010 Edgewall Software |
---|
| 6 | # All rights reserved. |
---|
| 7 | # |
---|
| 8 | # This software is licensed as described in the file COPYING, which |
---|
| 9 | # you should have received as part of this distribution. The terms |
---|
| 10 | # are also available at http://bitten.edgewall.org/wiki/License. |
---|
| 11 | |
---|
1 | 12 | """Recipe commands for tools commonly used by Python projects.""" |
---|
| 13 | |
---|
1 | 14 | from __future__ import division |
---|
| 15 | |
---|
1 | 16 | import logging |
---|
1 | 17 | import os |
---|
1 | 18 | import cPickle as pickle |
---|
1 | 19 | import re |
---|
1 | 20 | try: |
---|
1 | 21 | set |
---|
0 | 22 | except NameError: |
---|
0 | 23 | from sets import Set as set |
---|
1 | 24 | import shlex |
---|
1 | 25 | import sys |
---|
| 26 | |
---|
1 | 27 | from bitten.build import CommandLine, FileSet |
---|
1 | 28 | from bitten.util import loc, xmlio |
---|
| 29 | |
---|
1 | 30 | log = logging.getLogger('bitten.build.pythontools') |
---|
| 31 | |
---|
1 | 32 | __docformat__ = 'restructuredtext en' |
---|
| 33 | |
---|
1 | 34 | def _python_path(ctxt): |
---|
| 35 | """Return the path to the Python interpreter. |
---|
| 36 | |
---|
| 37 | If the configuration has a ``python.path`` property, the value of that |
---|
| 38 | option is returned; otherwise the path to the current Python interpreter is |
---|
| 39 | returned. |
---|
| 40 | """ |
---|
0 | 41 | python_path = ctxt.config.get_filepath('python.path') |
---|
0 | 42 | if python_path: |
---|
0 | 43 | return python_path |
---|
0 | 44 | return sys.executable |
---|
| 45 | |
---|
1 | 46 | def distutils(ctxt, file_='setup.py', command='build', |
---|
1 | 47 | options=None, timeout=None): |
---|
| 48 | """Execute a ``distutils`` command. |
---|
| 49 | |
---|
| 50 | :param ctxt: the build context |
---|
| 51 | :type ctxt: `Context` |
---|
| 52 | :param file\_: name of the file defining the distutils setup |
---|
| 53 | :param command: the setup command to execute |
---|
| 54 | :param options: additional options to pass to the command |
---|
| 55 | :param timeout: the number of seconds before the external process should |
---|
| 56 | be aborted (has same constraints as CommandLine) |
---|
| 57 | """ |
---|
0 | 58 | if options: |
---|
0 | 59 | if isinstance(options, basestring): |
---|
0 | 60 | options = shlex.split(options) |
---|
0 | 61 | else: |
---|
0 | 62 | options = [] |
---|
| 63 | |
---|
0 | 64 | if timeout: |
---|
0 | 65 | timeout = int(timeout) |
---|
| 66 | |
---|
0 | 67 | cmdline = CommandLine(_python_path(ctxt), |
---|
0 | 68 | [ctxt.resolve(file_), command] + options, |
---|
0 | 69 | cwd=ctxt.basedir) |
---|
0 | 70 | log_elem = xmlio.Fragment() |
---|
0 | 71 | error_logged = False |
---|
0 | 72 | for out, err in cmdline.execute(timeout): |
---|
0 | 73 | if out is not None: |
---|
0 | 74 | log.info(out) |
---|
0 | 75 | log_elem.append(xmlio.Element('message', level='info')[out]) |
---|
0 | 76 | if err is not None: |
---|
0 | 77 | level = 'error' |
---|
0 | 78 | if err.startswith('warning: '): |
---|
0 | 79 | err = err[9:] |
---|
0 | 80 | level = 'warning' |
---|
0 | 81 | log.warning(err) |
---|
0 | 82 | elif err.startswith('error: '): |
---|
0 | 83 | ctxt.error(err[7:]) |
---|
0 | 84 | error_logged = True |
---|
0 | 85 | else: |
---|
0 | 86 | log.error(err) |
---|
0 | 87 | log_elem.append(xmlio.Element('message', level=level)[err]) |
---|
0 | 88 | ctxt.log(log_elem) |
---|
| 89 | |
---|
0 | 90 | if not error_logged and cmdline.returncode != 0: |
---|
0 | 91 | ctxt.error('distutils failed (%s)' % cmdline.returncode) |
---|
| 92 | |
---|
1 | 93 | def exec_(ctxt, file_=None, module=None, function=None, |
---|
1 | 94 | output=None, args=None, timeout=None): |
---|
| 95 | """Execute a Python script. |
---|
| 96 | |
---|
| 97 | Either the `file_` or the `module` parameter must be provided. If |
---|
| 98 | specified using the `file_` parameter, the file must be inside the project |
---|
| 99 | directory. If specified as a module, the module must either be resolvable |
---|
| 100 | to a file, or the `function` parameter must be provided |
---|
| 101 | |
---|
| 102 | :param ctxt: the build context |
---|
| 103 | :type ctxt: `Context` |
---|
| 104 | :param file\_: name of the script file to execute |
---|
| 105 | :param module: name of the Python module to execute |
---|
| 106 | :param function: name of the Python function to run |
---|
| 107 | :param output: name of the file to which output should be written |
---|
| 108 | :param args: extra arguments to pass to the script |
---|
| 109 | :param timeout: the number of seconds before the external process should |
---|
| 110 | be aborted (has same constraints as CommandLine) |
---|
| 111 | """ |
---|
0 | 112 | assert file_ or module, 'Either "file" or "module" attribute required' |
---|
0 | 113 | if function: |
---|
0 | 114 | assert module and not file_, '"module" attribute required for use of ' \ |
---|
0 | 115 | '"function" attribute' |
---|
| 116 | |
---|
0 | 117 | if module: |
---|
| 118 | # Script specified as module name, need to resolve that to a file, |
---|
| 119 | # or use the function name if provided |
---|
0 | 120 | if function: |
---|
0 | 121 | args = '-c "import sys; from %s import %s; %s(sys.argv)" %s' % ( |
---|
0 | 122 | module, function, function, args) |
---|
0 | 123 | else: |
---|
0 | 124 | try: |
---|
0 | 125 | mod = __import__(module, globals(), locals(), []) |
---|
0 | 126 | components = module.split('.') |
---|
0 | 127 | for comp in components[1:]: |
---|
0 | 128 | mod = getattr(mod, comp) |
---|
0 | 129 | file_ = mod.__file__.replace('\\', '/') |
---|
0 | 130 | except ImportError, e: |
---|
0 | 131 | ctxt.error('Cannot execute Python module %s: %s' % (module, e)) |
---|
0 | 132 | return |
---|
| 133 | |
---|
0 | 134 | from bitten.build import shtools |
---|
0 | 135 | returncode = shtools.execute(ctxt, executable=_python_path(ctxt), |
---|
0 | 136 | file_=file_, output=output, args=args, |
---|
0 | 137 | timeout=timeout) |
---|
0 | 138 | if returncode != 0: |
---|
0 | 139 | ctxt.error('Executing %s failed (error code %s)' % \ |
---|
0 | 140 | (file_ or function or module, returncode)) |
---|
| 141 | |
---|
1 | 142 | def pylint(ctxt, file_=None): |
---|
| 143 | """Extract data from a ``pylint`` run written to a file. |
---|
| 144 | |
---|
| 145 | :param ctxt: the build context |
---|
| 146 | :type ctxt: `Context` |
---|
| 147 | :param file\_: name of the file containing the Pylint output |
---|
| 148 | """ |
---|
3 | 149 | assert file_, 'Missing required attribute "file"' |
---|
3 | 150 | msg_re = re.compile(r'^(?P<file>.+):(?P<line>\d+): ' |
---|
3 | 151 | r'\[(?P<type>[A-Z]\d*)(?:, (?P<tag>[\w\.]+))?\] ' |
---|
3 | 152 | r'(?P<msg>.*)$') |
---|
3 | 153 | msg_categories = dict(W='warning', E='error', C='convention', R='refactor') |
---|
| 154 | |
---|
3 | 155 | problems = xmlio.Fragment() |
---|
3 | 156 | try: |
---|
3 | 157 | fd = open(ctxt.resolve(file_), 'r') |
---|
3 | 158 | try: |
---|
11 | 159 | for line in fd: |
---|
8 | 160 | match = msg_re.search(line) |
---|
8 | 161 | if match: |
---|
5 | 162 | msg_type = match.group('type') |
---|
5 | 163 | category = msg_categories.get(msg_type[0]) |
---|
5 | 164 | if len(msg_type) == 1: |
---|
5 | 165 | msg_type = None |
---|
5 | 166 | filename = match.group('file') |
---|
5 | 167 | if os.path.isabs(filename) \ |
---|
3 | 168 | and filename.startswith(ctxt.basedir): |
---|
3 | 169 | filename = filename[len(ctxt.basedir) + 1:] |
---|
5 | 170 | filename = filename.replace('\\', '/') |
---|
5 | 171 | lineno = int(match.group('line')) |
---|
5 | 172 | tag = match.group('tag') |
---|
5 | 173 | problems.append(xmlio.Element('problem', category=category, |
---|
5 | 174 | type=msg_type, tag=tag, |
---|
5 | 175 | line=lineno, file=filename)[ |
---|
5 | 176 | xmlio.Element('msg')[match.group('msg') or ''] |
---|
5 | 177 | ]) |
---|
3 | 178 | ctxt.report('lint', problems) |
---|
3 | 179 | finally: |
---|
3 | 180 | fd.close() |
---|
0 | 181 | except IOError, e: |
---|
0 | 182 | log.warning('Error opening pylint results file (%s)', e) |
---|
| 183 | |
---|
1 | 184 | def coverage(ctxt, summary=None, coverdir=None, include=None, exclude=None): |
---|
| 185 | """Extract data from a ``coverage.py`` run. |
---|
| 186 | |
---|
| 187 | :param ctxt: the build context |
---|
| 188 | :type ctxt: `Context` |
---|
| 189 | :param summary: path to the file containing the coverage summary |
---|
| 190 | :param coverdir: name of the directory containing the per-module coverage |
---|
| 191 | details |
---|
| 192 | :param include: patterns of files or directories to include in the report |
---|
| 193 | :param exclude: patterns of files or directories to exclude from the report |
---|
| 194 | """ |
---|
6 | 195 | assert summary, 'Missing required attribute "summary"' |
---|
| 196 | |
---|
4 | 197 | summary_line_re = re.compile(r'^(?P<module>.*?)\s+(?P<stmts>\d+)\s+' |
---|
4 | 198 | r'(?P<exec>\d+)\s+(?P<cov>\d+)%\s+' |
---|
4 | 199 | r'(?:(?P<missing>(?:\d+(?:-\d+)?(?:, )?)*)\s+)?' |
---|
4 | 200 | r'(?P<file>.+)$') |
---|
| 201 | |
---|
4 | 202 | fileset = FileSet(ctxt.basedir, include, exclude) |
---|
4 | 203 | missing_files = [] |
---|
7 | 204 | for filename in fileset: |
---|
3 | 205 | if os.path.splitext(filename)[1] != '.py': |
---|
0 | 206 | continue |
---|
3 | 207 | missing_files.append(filename) |
---|
4 | 208 | covered_modules = set() |
---|
| 209 | |
---|
4 | 210 | try: |
---|
4 | 211 | summary_file = open(ctxt.resolve(summary), 'r') |
---|
4 | 212 | try: |
---|
4 | 213 | coverage = xmlio.Fragment() |
---|
19 | 214 | for summary_line in summary_file: |
---|
15 | 215 | match = summary_line_re.search(summary_line) |
---|
15 | 216 | if match: |
---|
3 | 217 | modname = match.group(1) |
---|
3 | 218 | filename = match.group(6) |
---|
3 | 219 | if not os.path.isabs(filename): |
---|
2 | 220 | filename = os.path.normpath(os.path.join(ctxt.basedir, |
---|
2 | 221 | filename)) |
---|
2 | 222 | else: |
---|
1 | 223 | filename = os.path.realpath(filename) |
---|
3 | 224 | if not filename.startswith(ctxt.basedir): |
---|
0 | 225 | continue |
---|
3 | 226 | filename = filename[len(ctxt.basedir) + 1:] |
---|
3 | 227 | if not filename in fileset: |
---|
0 | 228 | continue |
---|
| 229 | |
---|
3 | 230 | percentage = int(match.group(4).rstrip('%')) |
---|
3 | 231 | num_lines = int(match.group(2)) |
---|
| 232 | |
---|
3 | 233 | missing_files.remove(filename) |
---|
3 | 234 | covered_modules.add(modname) |
---|
3 | 235 | module = xmlio.Element('coverage', name=modname, |
---|
3 | 236 | file=filename.replace(os.sep, '/'), |
---|
3 | 237 | percentage=percentage, |
---|
3 | 238 | lines=num_lines) |
---|
3 | 239 | coverage.append(module) |
---|
| 240 | |
---|
4 | 241 | for filename in missing_files: |
---|
0 | 242 | modname = os.path.splitext(filename.replace(os.sep, '.'))[0] |
---|
0 | 243 | if modname in covered_modules: |
---|
0 | 244 | continue |
---|
0 | 245 | covered_modules.add(modname) |
---|
0 | 246 | module = xmlio.Element('coverage', name=modname, |
---|
0 | 247 | file=filename.replace(os.sep, '/'), |
---|
0 | 248 | percentage=0) |
---|
0 | 249 | coverage.append(module) |
---|
| 250 | |
---|
4 | 251 | ctxt.report('coverage', coverage) |
---|
4 | 252 | finally: |
---|
4 | 253 | summary_file.close() |
---|
0 | 254 | except IOError, e: |
---|
0 | 255 | log.warning('Error opening coverage summary file (%s)', e) |
---|
| 256 | |
---|
1 | 257 | def trace(ctxt, summary=None, coverdir=None, include=None, exclude=None): |
---|
| 258 | """Extract data from a ``trace.py`` run. |
---|
| 259 | |
---|
| 260 | :param ctxt: the build context |
---|
| 261 | :type ctxt: `Context` |
---|
| 262 | :param summary: path to the file containing the coverage summary |
---|
| 263 | :param coverdir: name of the directory containing the per-module coverage |
---|
| 264 | details |
---|
| 265 | :param include: patterns of files or directories to include in the report |
---|
| 266 | :param exclude: patterns of files or directories to exclude from the report |
---|
| 267 | """ |
---|
5 | 268 | assert summary, 'Missing required attribute "summary"' |
---|
4 | 269 | assert coverdir, 'Missing required attribute "coverdir"' |
---|
| 270 | |
---|
3 | 271 | summary_line_re = re.compile(r'^\s*(?P<lines>\d+)\s+(?P<cov>\d+)%\s+' |
---|
3 | 272 | r'(?P<module>.*?)\s+\((?P<filename>.*?)\)') |
---|
3 | 273 | coverage_line_re = re.compile(r'\s*(?:(?P<hits>\d+): )?(?P<line>.*)') |
---|
| 274 | |
---|
3 | 275 | fileset = FileSet(ctxt.basedir, include, exclude) |
---|
3 | 276 | missing_files = [] |
---|
5 | 277 | for filename in fileset: |
---|
2 | 278 | if os.path.splitext(filename)[1] != '.py': |
---|
0 | 279 | continue |
---|
2 | 280 | missing_files.append(filename) |
---|
3 | 281 | covered_modules = set() |
---|
| 282 | |
---|
3 | 283 | def handle_file(elem, sourcefile, coverfile=None): |
---|
2 | 284 | code_lines = set() |
---|
2 | 285 | for lineno, linetype, line in loc.count(sourcefile): |
---|
0 | 286 | if linetype == loc.CODE: |
---|
0 | 287 | code_lines.add(lineno) |
---|
2 | 288 | num_covered = 0 |
---|
2 | 289 | lines = [] |
---|
| 290 | |
---|
2 | 291 | if coverfile: |
---|
0 | 292 | prev_hits = '0' |
---|
0 | 293 | for idx, coverline in enumerate(coverfile): |
---|
0 | 294 | match = coverage_line_re.search(coverline) |
---|
0 | 295 | if match: |
---|
0 | 296 | hits = match.group(1) |
---|
0 | 297 | if hits: # Line covered |
---|
0 | 298 | if hits != '0': |
---|
0 | 299 | num_covered += 1 |
---|
0 | 300 | lines.append(hits) |
---|
0 | 301 | prev_hits = hits |
---|
0 | 302 | elif coverline.startswith('>'): # Line not covered |
---|
0 | 303 | lines.append('0') |
---|
0 | 304 | prev_hits = '0' |
---|
0 | 305 | elif idx not in code_lines: # Not a code line |
---|
0 | 306 | lines.append('-') |
---|
0 | 307 | prev_hits = '0' |
---|
0 | 308 | else: # A code line not flagged by trace.py |
---|
0 | 309 | if prev_hits != '0': |
---|
0 | 310 | num_covered += 1 |
---|
0 | 311 | lines.append(prev_hits) |
---|
| 312 | |
---|
0 | 313 | elem.append(xmlio.Element('line_hits')[' '.join(lines)]) |
---|
| 314 | |
---|
2 | 315 | num_lines = not lines and len(code_lines) or \ |
---|
2 | 316 | len([l for l in lines if l != '-']) |
---|
2 | 317 | if num_lines: |
---|
0 | 318 | percentage = int(round(num_covered * 100 / num_lines)) |
---|
0 | 319 | else: |
---|
2 | 320 | percentage = 0 |
---|
2 | 321 | elem.attr['percentage'] = percentage |
---|
2 | 322 | elem.attr['lines'] = num_lines |
---|
| 323 | |
---|
3 | 324 | try: |
---|
3 | 325 | summary_file = open(ctxt.resolve(summary), 'r') |
---|
3 | 326 | try: |
---|
3 | 327 | coverage = xmlio.Fragment() |
---|
10 | 328 | for summary_line in summary_file: |
---|
7 | 329 | match = summary_line_re.search(summary_line) |
---|
7 | 330 | if match: |
---|
2 | 331 | modname = match.group(3) |
---|
2 | 332 | filename = match.group(4) |
---|
2 | 333 | if not os.path.isabs(filename): |
---|
1 | 334 | filename = os.path.normpath(os.path.join(ctxt.basedir, |
---|
1 | 335 | filename)) |
---|
1 | 336 | else: |
---|
1 | 337 | filename = os.path.realpath(filename) |
---|
2 | 338 | if not filename.startswith(ctxt.basedir): |
---|
0 | 339 | continue |
---|
2 | 340 | filename = filename[len(ctxt.basedir) + 1:] |
---|
2 | 341 | if not filename in fileset: |
---|
0 | 342 | continue |
---|
| 343 | |
---|
2 | 344 | missing_files.remove(filename) |
---|
2 | 345 | covered_modules.add(modname) |
---|
2 | 346 | module = xmlio.Element('coverage', name=modname, |
---|
2 | 347 | file=filename.replace(os.sep, '/')) |
---|
2 | 348 | sourcefile = file(ctxt.resolve(filename)) |
---|
2 | 349 | try: |
---|
2 | 350 | coverpath = ctxt.resolve(coverdir, modname + '.cover') |
---|
2 | 351 | if os.path.isfile(coverpath): |
---|
0 | 352 | coverfile = file(coverpath, 'r') |
---|
0 | 353 | else: |
---|
2 | 354 | log.warning('No coverage file for module %s at %s', |
---|
2 | 355 | modname, coverpath) |
---|
2 | 356 | coverfile = None |
---|
2 | 357 | try: |
---|
2 | 358 | handle_file(module, sourcefile, coverfile) |
---|
2 | 359 | finally: |
---|
2 | 360 | if coverfile: |
---|
0 | 361 | coverfile.close() |
---|
0 | 362 | finally: |
---|
2 | 363 | sourcefile.close() |
---|
2 | 364 | coverage.append(module) |
---|
| 365 | |
---|
3 | 366 | for filename in missing_files: |
---|
0 | 367 | modname = os.path.splitext(filename.replace(os.sep, '.'))[0] |
---|
0 | 368 | if modname in covered_modules: |
---|
0 | 369 | continue |
---|
0 | 370 | covered_modules.add(modname) |
---|
0 | 371 | module = xmlio.Element('coverage', name=modname, |
---|
0 | 372 | file=filename.replace(os.sep, '/'), |
---|
0 | 373 | percentage=0) |
---|
0 | 374 | filepath = ctxt.resolve(filename) |
---|
0 | 375 | fileobj = file(filepath, 'r') |
---|
0 | 376 | try: |
---|
0 | 377 | handle_file(module, fileobj) |
---|
0 | 378 | finally: |
---|
0 | 379 | fileobj.close() |
---|
0 | 380 | coverage.append(module) |
---|
| 381 | |
---|
3 | 382 | ctxt.report('coverage', coverage) |
---|
3 | 383 | finally: |
---|
3 | 384 | summary_file.close() |
---|
0 | 385 | except IOError, e: |
---|
0 | 386 | log.warning('Error opening coverage summary file (%s)', e) |
---|
| 387 | |
---|
1 | 388 | def figleaf(ctxt, summary=None, include=None, exclude=None): |
---|
| 389 | """Extract data from a ``Figleaf`` run. |
---|
| 390 | |
---|
| 391 | :param ctxt: the build context |
---|
| 392 | :type ctxt: `Context` |
---|
| 393 | :param summary: path to the file containing the coverage summary |
---|
| 394 | :param include: patterns of files or directories to include in the report |
---|
| 395 | :param exclude: patterns of files or directories to exclude from the report |
---|
| 396 | """ |
---|
5 | 397 | from figleaf import get_lines |
---|
5 | 398 | coverage = xmlio.Fragment() |
---|
5 | 399 | try: |
---|
5 | 400 | fileobj = open(ctxt.resolve(summary)) |
---|
1 | 401 | except IOError, e: |
---|
1 | 402 | log.warning('Error opening coverage summary file (%s)', e) |
---|
1 | 403 | return |
---|
4 | 404 | coverage_data = pickle.load(fileobj) |
---|
4 | 405 | fileset = FileSet(ctxt.basedir, include, exclude) |
---|
7 | 406 | for filename in fileset: |
---|
3 | 407 | base, ext = os.path.splitext(filename) |
---|
3 | 408 | if ext != '.py': |
---|
1 | 409 | continue |
---|
2 | 410 | modname = base.replace(os.path.sep, '.') |
---|
2 | 411 | realfilename = ctxt.resolve(filename) |
---|
2 | 412 | interesting_lines = get_lines(open(realfilename)) |
---|
2 | 413 | if not interesting_lines: |
---|
0 | 414 | continue |
---|
2 | 415 | covered_lines = coverage_data.get(realfilename, set()) |
---|
2 | 416 | percentage = int(round(len(covered_lines) * 100 / len(interesting_lines))) |
---|
2 | 417 | line_hits = [] |
---|
12 | 418 | for lineno in xrange(1, max(interesting_lines)+1): |
---|
10 | 419 | if lineno not in interesting_lines: |
---|
1 | 420 | line_hits.append('-') |
---|
9 | 421 | elif lineno in covered_lines: |
---|
3 | 422 | line_hits.append('1') |
---|
3 | 423 | else: |
---|
6 | 424 | line_hits.append('0') |
---|
2 | 425 | module = xmlio.Element('coverage', name=modname, |
---|
2 | 426 | file=filename.replace(os.sep, '/'), |
---|
2 | 427 | percentage=percentage, |
---|
2 | 428 | lines=len(interesting_lines), |
---|
2 | 429 | line_hits=' '.join(line_hits)) |
---|
2 | 430 | coverage.append(module) |
---|
4 | 431 | ctxt.report('coverage', coverage) |
---|
| 432 | |
---|
1 | 433 | def _normalize_filenames(ctxt, filenames, fileset): |
---|
2 | 434 | for filename in filenames: |
---|
1 | 435 | if not os.path.isabs(filename): |
---|
0 | 436 | filename = os.path.normpath(os.path.join(ctxt.basedir, |
---|
0 | 437 | filename)) |
---|
0 | 438 | else: |
---|
1 | 439 | filename = os.path.realpath(filename) |
---|
1 | 440 | if not filename.startswith(ctxt.basedir): |
---|
0 | 441 | continue |
---|
1 | 442 | filename = filename[len(ctxt.basedir) + 1:] |
---|
1 | 443 | if filename not in fileset: |
---|
0 | 444 | continue |
---|
1 | 445 | yield filename.replace(os.sep, '/') |
---|
| 446 | |
---|
1 | 447 | def unittest(ctxt, file_=None): |
---|
| 448 | """Extract data from a unittest results file in XML format. |
---|
| 449 | |
---|
| 450 | :param ctxt: the build context |
---|
| 451 | :type ctxt: `Context` |
---|
| 452 | :param file\_: name of the file containing the test results |
---|
| 453 | """ |
---|
5 | 454 | assert file_, 'Missing required attribute "file"' |
---|
| 455 | |
---|
4 | 456 | try: |
---|
4 | 457 | fileobj = file(ctxt.resolve(file_), 'r') |
---|
4 | 458 | try: |
---|
4 | 459 | total, failed = 0, 0 |
---|
4 | 460 | results = xmlio.Fragment() |
---|
7 | 461 | for child in xmlio.parse(fileobj).children(): |
---|
3 | 462 | test = xmlio.Element('test') |
---|
14 | 463 | for name, value in child.attr.items(): |
---|
11 | 464 | if name == 'file': |
---|
2 | 465 | value = os.path.realpath(value) |
---|
2 | 466 | if value.startswith(ctxt.basedir): |
---|
2 | 467 | value = value[len(ctxt.basedir) + 1:] |
---|
2 | 468 | value = value.replace(os.sep, '/') |
---|
2 | 469 | else: |
---|
0 | 470 | continue |
---|
11 | 471 | test.attr[name] = value |
---|
11 | 472 | if name == 'status' and value in ('error', 'failure'): |
---|
0 | 473 | failed += 1 |
---|
3 | 474 | for grandchild in child.children(): |
---|
0 | 475 | test.append(xmlio.Element(grandchild.name)[ |
---|
0 | 476 | grandchild.gettext() |
---|
0 | 477 | ]) |
---|
3 | 478 | results.append(test) |
---|
3 | 479 | total += 1 |
---|
4 | 480 | if failed: |
---|
0 | 481 | ctxt.error('%d of %d test%s failed' % (failed, total, |
---|
0 | 482 | total != 1 and 's' or '')) |
---|
4 | 483 | ctxt.report('test', results) |
---|
4 | 484 | finally: |
---|
4 | 485 | fileobj.close() |
---|
0 | 486 | except IOError, e: |
---|
0 | 487 | log.warning('Error opening unittest results file (%s)', e) |
---|
0 | 488 | except xmlio.ParseError, e: |
---|
0 | 489 | log.warning('Error parsing unittest results file (%s)', e) |
---|