Edgewall Software

source: trunk/bitten/build/javatools.py @ 1001

Last change on this file since 1001 was 910, checked in by osimons, 13 years ago

Updated copyright to 2010.

  • Property svn:eol-style set to native
File size: 8.9 KB
CovLine 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
4# Copyright (C) 2006 Matthew Good <matt@matt-good.net>
5# Copyright (C) 2007-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
112"""Recipe commands for tools commonly used in Java projects."""
13
114from glob import glob
115import logging
116import os
117import posixpath
118import shlex
119import tempfile
20
121from bitten.build import CommandLine
122from bitten.util import xmlio
23
124log = logging.getLogger('bitten.build.javatools')
25
126__docformat__ = 'restructuredtext en'
27
128def ant(ctxt, file_=None, target=None, keep_going=False, args=None):
29    """Run an Ant build.
30   
31    :param ctxt: the build context
32    :type ctxt: `Context`
33    :param file\_: name of the Ant build file
34    :param target: name of the target that should be executed (optional)
35    :param keep_going: whether Ant should keep going when errors are encountered
36    :param args: additional arguments to pass to Ant
37    """
038    executable = 'ant'
039    ant_home = ctxt.config.get_dirpath('ant.home')
040    if ant_home:
041        executable = os.path.join(ant_home, 'bin', 'ant')
42
043    java_home = ctxt.config.get_dirpath('java.home')
044    if java_home:
045        os.environ['JAVA_HOME'] = java_home
46
047    logfile = tempfile.NamedTemporaryFile(prefix='ant_log', suffix='.xml')
048    logfile.close()
049    if args:
050        args = shlex.split(args)
051    else:
052        args = []
053    args += ['-noinput', '-listener', 'org.apache.tools.ant.XmlLogger',
054             '-Dant.XmlLogger.stylesheet.uri', '""',
055             '-DXmlLogger.file', logfile.name]
056    if file_:
057        args += ['-buildfile', ctxt.resolve(file_)]
058    if keep_going:
059        args.append('-keep-going')
060    if target:
061        args.append(target)
62
063    shell = False
064    if os.name == 'nt':
65        # Need to execute ant.bat through a shell on Windows
066        shell = True
67
068    cmdline = CommandLine(executable, args, cwd=ctxt.basedir, shell=shell)
069    for out, err in cmdline.execute():
070        if out is not None:
071            log.info(out)
072        if err is not None:
073            log.error(err)
74
075    error_logged = False
076    log_elem = xmlio.Fragment()
077    try:
078        xml_log = xmlio.parse(file(logfile.name, 'r'))
079        def collect_log_messages(node):
080            for child in node.children():
081                if child.name == 'message':
082                    if child.attr['priority'] == 'debug':
083                        continue
084                    log_elem.append(xmlio.Element('message',
085                                                  level=child.attr['priority'])[
086                        child.gettext().replace(ctxt.basedir + os.sep, '')
087                                       .replace(ctxt.basedir, '')
088                    ])
089                else:
090                    collect_log_messages(child)
091        collect_log_messages(xml_log)
92
093        if 'error' in xml_log.attr:
094            ctxt.error(xml_log.attr['error'])
095            error_logged = True
96
097    except xmlio.ParseError, e:
098        log.warning('Error parsing Ant XML log file (%s)', e)
099    ctxt.log(log_elem)
100
0101    if not error_logged and cmdline.returncode != 0:
0102        ctxt.error('Ant failed (%s)' % cmdline.returncode)
103
104
1105def _fix_traceback(result):
106    """
107    Sometimes the traceback isn't prefixed with the exception type and
108    message, so add it in if needed.
109    """
2110    attr = result[0].attr
2111    tracebackprefix = ""
112    # py.test does not include the type tag in some cases so don't do this
113    # fix when it is missing
2114    if "type" in attr:
0115        tracebackprefix = "%s: %s" % (attr['type'], attr.get('message', ''))
2116    if result[0].gettext().startswith(tracebackprefix):
2117        return result[0].gettext()
2118    else:
0119        return "\n".join((tracebackprefix, result[0].gettext()))
120
121
1122def junit(ctxt, file_=None, srcdir=None):
123    """Extract test results from a JUnit XML report.
124   
125    :param ctxt: the build context
126    :type ctxt: `Context`
127    :param file\_: path to the JUnit XML test results; may contain globbing
128                  wildcards for matching multiple results files
129    :param srcdir: name of the directory containing the test sources, used to
130                   link test results to the corresponding source files
131    """
3132    assert file_, 'Missing required attribute "file"'
3133    try:
3134        total, failed = 0, 0
3135        results = xmlio.Fragment()
6136        for path in glob(ctxt.resolve(file_)):
3137            fileobj = file(path, 'r')
3138            try:
6139                for testcase in xmlio.parse(fileobj).children('testcase'):
3140                    test = xmlio.Element('test')
3141                    test.attr['fixture'] = testcase.attr['classname']
3142                    test.attr['name'] = testcase.attr['name']
3143                    if 'time' in testcase.attr:
3144                        test.attr['duration'] = testcase.attr['time']
3145                    if srcdir is not None:
0146                        cls = testcase.attr['classname'].split('.')
0147                        test.attr['file'] = posixpath.join(srcdir, *cls) + \
0148                                            '.java'
149
3150                    result = list(testcase.children())
3151                    if result:
2152                        junit_status = result[0].name
2153                        test.append(xmlio.Element('traceback')[_fix_traceback(result)])
2154                        if junit_status == 'skipped':
1155                            test.attr['status'] = 'ignore'
1156                        elif junit_status == 'error':
1157                            test.attr['status'] = 'error'
1158                            failed += 1
1159                        else:
0160                            test.attr['status'] = 'failure'
0161                            failed += 1
0162                    else:
1163                        test.attr['status'] = 'success'
164
3165                    results.append(test)
3166                    total += 1
3167            finally:
3168                fileobj.close()
3169        if failed:
1170            ctxt.error('%d of %d test%s failed' % (failed, total,
1171                       total != 1 and 's' or ''))
3172        ctxt.report('test', results)
0173    except IOError, e:
0174        log.warning('Error opening JUnit results file (%s)', e)
0175    except xmlio.ParseError, e:
0176        log.warning('Error parsing JUnit results file (%s)', e)
177
178
2179class _LineCounter(object):
1180    def __init__(self):
3181        self.lines = []
3182        self.covered = 0
3183        self.num_lines = 0
184
1185    def __getitem__(self, idx):
0186        if idx >= len(self.lines):
0187            return 0
0188        return self.lines[idx]
189
1190    def __setitem__(self, idx, val):
6191        idx = int(idx) - 1 # 1-indexed to 0-indexed
6192        from itertools import repeat
6193        if idx >= len(self.lines):
6194            self.lines.extend(repeat('-', idx - len(self.lines) + 1))
6195        self.lines[idx] = val
6196        self.num_lines += 1
6197        if val != '0':
3198            self.covered += 1
199
1200    def line_hits(self):
3201        return ' '.join(self.lines)
1202    line_hits = property(line_hits)
203
1204    def percentage(self):
3205        if self.num_lines == 0:
1206            return 0
2207        return int(round(self.covered * 100. / self.num_lines))
1208    percentage = property(percentage)
209
210
1211def cobertura(ctxt, file_=None):
212    """Extract test coverage information from a Cobertura XML report.
213   
214    :param ctxt: the build context
215    :type ctxt: `Context`
216    :param file\_: path to the Cobertura XML output
217    """
3218    assert file_, 'Missing required attribute "file"'
219
3220    coverage = xmlio.Fragment()
3221    doc = xmlio.parse(open(ctxt.resolve(file_)))
3222    srcdir = [s.gettext().strip() for ss in doc.children('sources')
6223                                  for s in ss.children('source')][0]
224
3225    classes = [cls for pkgs in doc.children('packages')
3226                   for pkg in pkgs.children('package')
3227                   for clss in pkg.children('classes')
6228                   for cls in clss.children('class')]
229
3230    counters = {}
3231    class_names = {}
232
6233    for cls in classes:
3234        filename = cls.attr['filename'].replace(os.sep, '/')
3235        name = cls.attr['name']
3236        if not '$' in name: # ignore internal classes
3237            class_names[filename] = name
3238        counter = counters.get(filename)
3239        if counter is None:
3240            counter = counters[filename] = _LineCounter()
3241        lines = [l for ls in cls.children('lines')
9242                   for l in ls.children('line')]
9243        for line in lines:
6244            counter[line.attr['number']] = line.attr['hits']
245
6246    for filename, name in class_names.iteritems():
3247        counter = counters[filename]
3248        module = xmlio.Element('coverage', name=name,
3249                               file=posixpath.join(srcdir, filename),
3250                               lines=counter.num_lines,
3251                               percentage=counter.percentage)
3252        module.append(xmlio.Element('line_hits')[counter.line_hits])
3253        coverage.append(module)
3254    ctxt.report('coverage', coverage)
Note: See TracBrowser for help on using the repository browser.