Mercurial > bitten > bitten-test
view bitten/build/ctools.py @ 605:c94481bc4646
0.6dev: Reverting [677] as many of these paths are also used for URLs, and hadn't considered this change enough. No major point in changing code that already works well, so simply reverting seems the best idea...
author | osimons |
---|---|
date | Thu, 30 Jul 2009 19:51:11 +0000 |
parents | 7af6ebc30ff8 |
children | 639e5c466c96 |
line wrap: on
line source
# -*- coding: utf-8 -*- # # Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> # Copyright (C) 2007 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at http://bitten.edgewall.org/wiki/License. """Recipe commands for build tasks commonly used for C/C++ projects.""" import logging import re import os import posixpath import shlex from bitten.build import CommandLine, FileSet from bitten.util import xmlio log = logging.getLogger('bitten.build.ctools') __docformat__ = 'restructuredtext en' def configure(ctxt, file_='configure', enable=None, disable=None, with_=None, without=None, cflags=None, cxxflags=None, prefix=None, **kw): """Run a ``configure`` script. :param ctxt: the build context :type ctxt: `Context` :param file\_: name of the configure script :param enable: names of the features to enable, seperated by spaces :param disable: names of the features to disable, separated by spaces :param with_: names of external packages to include :param without: names of external packages to exclude :param cflags: ``CFLAGS`` to pass to the configure script :param cxxflags: ``CXXFLAGS`` to pass to the configure script :param prefix: install prefix to pass to the configure script, will be postfixed by the machine name from the build """ args = [] if enable: args += ['--enable-%s' % feature for feature in enable.split()] if disable: args += ['--disable-%s' % feature for feature in disable.split()] # since 'with' is a reserved word in python, we need to handle the argument carefully with_ = kw.pop('with', with_) for key in kw: raise TypeError("configure() got an unexpected keyword argument '%s'" % key) if with_: for pkg in with_.split(): pkg_path = pkg + '.path' if pkg_path in ctxt.config: args.append('--with-%s=%s' % (pkg, ctxt.config[pkg_path])) else: args.append('--with-%s' % pkg) if without: args += ['--without-%s' % pkg for pkg in without.split()] if cflags: args.append('CFLAGS=%s' % cflags) if cxxflags: args.append('CXXFLAGS=%s' % cxxflags) if prefix: args.append('--prefix=%ss' % prefix) from bitten.build import shtools returncode = shtools.execute(ctxt, file_=file_, args=args) if returncode != 0: ctxt.error('configure failed (%s)' % returncode) def autoreconf(ctxt, file_='configure', force=None, install=None, symlink=None, warnings=None, prepend_include=None, include =None): """Run the autotoll ``autoreconf``. :param ctxt: the build context :type ctxt: `Context` :param force: consider all files obsolete :param install: copy missing auxiliary files :param symlink: install symbolic links instead of copies :param warnings: report the warnings falling in CATEGORY :prepend_include: prepend directories to search path :include: append directories to search path """ args = [] if install: args.append('--install') if symlink: args.append('--symlink') if force: args.append('--force') if warnings: args.append('--warnings=%s' % warnings) if include: args += ['--include=%s' % inc for inc in include.split()] if prepend_include: args += ['--prepend-include=%s' % pinc for pinc in prepend_include.split()] from bitten.build import shtools returncode = shtools.execute(ctxt, 'autoreconf', args=args) if returncode != 0: ctxt.error('autoreconf failed (%s)' % returncode) def make(ctxt, target=None, file_=None, keep_going=False, directory=None, jobs=None, args=None): """Execute a Makefile target. :param ctxt: the build context :type ctxt: `Context` :param file\_: name of the Makefile :param keep_going: whether make should keep going when errors are encountered :param directory: directory in which to build; defaults to project source directory :param jobs: number of concurrent jobs to run :param args: command-line arguments to pass to the script """ executable = ctxt.config.get_filepath('make.path') or 'make' if directory is None: directory = ctxt.basedir margs = ['--directory', directory] if file_: margs += ['--file', ctxt.resolve(file_)] if keep_going: margs.append('--keep-going') if target: margs.append(target) if jobs: margs += ['--jobs', jobs] if args: if isinstance(args, basestring): margs += shlex.split(args) from bitten.build import shtools returncode = shtools.execute(ctxt, executable=executable, args=margs) if returncode != 0: ctxt.error('make failed (%s)' % returncode) def cppunit(ctxt, file_=None, srcdir=None): """Collect CppUnit XML data. :param ctxt: the build context :type ctxt: `Context` :param file\_: path of the file containing the CppUnit results; may contain globbing wildcards to match multiple files :param srcdir: name of the directory containing the source files, used to link the test results to the corresponding files """ assert file_, 'Missing required attribute "file"' try: fileobj = file(ctxt.resolve(file_), 'r') try: total, failed = 0, 0 results = xmlio.Fragment() for group in xmlio.parse(fileobj): if group.name not in ('FailedTests', 'SuccessfulTests'): continue for child in group.children(): test = xmlio.Element('test') name = child.children('Name').next().gettext() if '::' in name: parts = name.split('::') test.attr['fixture'] = '::'.join(parts[:-1]) name = parts[-1] test.attr['name'] = name for location in child.children('Location'): for file_elem in location.children('File'): filepath = file_elem.gettext() if srcdir is not None: filepath = posixpath.join(srcdir, filepath) test.attr['file'] = filepath break for line_elem in location.children('Line'): test.attr['line'] = line_elem.gettext() break break if child.name == 'FailedTest': for message in child.children('Message'): test.append(xmlio.Element('traceback')[ message.gettext() ]) test.attr['status'] = 'failure' failed += 1 else: test.attr['status'] = 'success' results.append(test) total += 1 if failed: ctxt.error('%d of %d test%s failed' % (failed, total, total != 1 and 's' or '')) ctxt.report('test', results) finally: fileobj.close() except IOError, e: log.warning('Error opening CppUnit results file (%s)', e) except xmlio.ParseError, e: print e log.warning('Error parsing CppUnit results file (%s)', e) def cunit (ctxt, file_=None, srcdir=None): """Collect CUnit XML data. :param ctxt: the build context :type ctxt: `Context` :param file\_: path of the file containing the CUnit results; may contain globbing wildcards to match multiple files :param srcdir: name of the directory containing the source files, used to link the test results to the corresponding files """ assert file_, 'Missing required attribute "file"' try: fileobj = file(ctxt.resolve(file_), 'r') try: total, failed = 0, 0 results = xmlio.Fragment() log_elem = xmlio.Fragment() def info (msg): log.info (msg) log_elem.append (xmlio.Element ('message', level='info')[msg]) def warning (msg): log.warning (msg) log_elem.append (xmlio.Element ('message', level='warning')[msg]) def error (msg): log.error (msg) log_elem.append (xmlio.Element ('message', level='error')[msg]) for node in xmlio.parse(fileobj): if node.name != 'CUNIT_RESULT_LISTING': continue for suiteRun in node.children ('CUNIT_RUN_SUITE'): for suite in suiteRun.children(): if suite.name not in ('CUNIT_RUN_SUITE_SUCCESS', 'CUNIT_RUN_SUITE_FAILURE'): warning ("Unknown node: %s" % suite.name) continue suiteName = suite.children ('SUITE_NAME').next().gettext() info ("%s [%s]" % ("*" * (57 - len (suiteName)), suiteName)) for record in suite.children ('CUNIT_RUN_TEST_RECORD'): for result in record.children(): if result.name not in ('CUNIT_RUN_TEST_SUCCESS', 'CUNIT_RUN_TEST_FAILURE'): continue testName = result.children ('TEST_NAME').next().gettext() info ("Running %s..." % testName); test = xmlio.Element('test') test.attr['fixture'] = suiteName test.attr['name'] = testName if result.name == 'CUNIT_RUN_TEST_FAILURE': error ("%s(%d): %s" % (result.children ('FILE_NAME').next().gettext(), int (result.children ('LINE_NUMBER').next().gettext()), result.children ('CONDITION').next().gettext())) test.attr['status'] = 'failure' failed += 1 else: test.attr['status'] = 'success' results.append(test) total += 1 if failed: ctxt.error('%d of %d test%s failed' % (failed, total, total != 1 and 's' or '')) ctxt.report('test', results) ctxt.log (log_elem) finally: fileobj.close() except IOError, e: log.warning('Error opening CUnit results file (%s)', e) except xmlio.ParseError, e: print e log.warning('Error parsing CUnit results file (%s)', e) def gcov(ctxt, include=None, exclude=None, prefix=None, root=""): """Run ``gcov`` to extract coverage data where available. :param ctxt: the build context :type ctxt: `Context` :param include: patterns of files and directories to include :param exclude: patterns of files and directories that should be excluded :param prefix: optional prefix name that is added to object files by the build system :param root: optional root path in which the build system puts the object files """ file_re = re.compile(r'^File (?:\'|\`)(?P<file>[^\']+)\'\s*$') lines_re = re.compile(r'^Lines executed:(?P<cov>\d+\.\d+)\% of (?P<num>\d+)\s*$') files = [] for filename in FileSet(ctxt.basedir, include, exclude): if os.path.splitext(filename)[1] in ('.c', '.cpp', '.cc', '.cxx'): files.append(filename) coverage = xmlio.Fragment() log_elem = xmlio.Fragment() def info (msg): log.info (msg) log_elem.append (xmlio.Element ('message', level='info')[msg]) def warning (msg): log.warning (msg) log_elem.append (xmlio.Element ('message', level='warning')[msg]) def error (msg): log.error (msg) log_elem.append (xmlio.Element ('message', level='error')[msg]) for srcfile in files: # Determine the coverage for each source file by looking for a .gcno # and .gcda pair info ("Getting coverage info for %s" % srcfile) filepath, filename = os.path.split(srcfile) stem = os.path.splitext(filename)[0] if prefix is not None: stem = prefix + '-' + stem objfile = os.path.join (root, filepath, stem + '.o') if not os.path.isfile(ctxt.resolve(objfile)): warning ('No object file found for %s at %s' % (srcfile, objfile)) continue if not os.path.isfile (ctxt.resolve (os.path.join (root, filepath, stem + '.gcno'))): warning ('No .gcno file found for %s at %s' % (srcfile, os.path.join (root, filepath, stem + '.gcno'))) continue if not os.path.isfile (ctxt.resolve (os.path.join (root, filepath, stem + '.gcda'))): warning ('No .gcda file found for %s at %s' % (srcfile, os.path.join (root, filepath, stem + '.gcda'))) continue num_lines, num_covered = 0, 0 skip_block = False cmd = CommandLine('gcov', ['-b', '-n', '-o', objfile, srcfile], cwd=ctxt.basedir) for out, err in cmd.execute(): if out == '': # catch blank lines, reset the block state... skip_block = False elif out and not skip_block: # Check for a file name match = file_re.match(out) if match: if os.path.isabs(match.group('file')): skip_block = True continue else: # check for a "Lines executed" message match = lines_re.match(out) if match: lines = float(match.group('num')) cov = float(match.group('cov')) num_covered += int(lines * cov / 100) num_lines += int(lines) if cmd.returncode != 0: continue module = xmlio.Element('coverage', name=os.path.basename(srcfile), file=srcfile.replace(os.sep, '/'), lines=num_lines, percentage=0) if num_lines: percent = int(round(num_covered * 100 / num_lines)) module.attr['percentage'] = percent coverage.append(module) ctxt.report('coverage', coverage) ctxt.log (log_elem)