# HG changeset patch # User cmlenz # Date 1128593378 0 # Node ID 77cdef044d486128f420cf52fdb26df34c254235 # Parent 5acdaab271874d475cd3a8664017c0e46d1cbff4 * Improve build log formatter performance: now only matches strings using the `path:line` format, and checks the existance of files in the repository when they are encountered. Should fix (or at least improve) #54. * More code reuse in the recipe commands code. Some minor/cosmetic cleanup. diff --git a/bitten/build/ctools.py b/bitten/build/ctools.py --- a/bitten/build/ctools.py +++ b/bitten/build/ctools.py @@ -36,18 +36,9 @@ if cxxflags: args.append('CXXFLAGS=%s' % cxxflags) - log_elem = xmlio.Fragment() - cmdline = CommandLine(ctxt.resolve(file_), args, cwd=ctxt.basedir) - for out, err in cmdline.execute(): - if out is not None: - log.info(out) - log_elem.append(xmlio.Element('message', level='info')[out]) - if err is not None: - log.error(err) - log_elem.append(xmlio.Element('message', level='error')[err]) - ctxt.log(log_elem) - - if cmdline.returncode != 0: + from bitten.build import shtools + returncode = shtools.execute(ctxt, file_=file_, args=args) + if returncode != 0: ctxt.error('configure failed (%s)' % cmdline.returncode) def make(ctxt, target=None, file_=None, keep_going=False): @@ -60,16 +51,7 @@ if target: args.append(target) - log_elem = xmlio.Fragment() - cmdline = CommandLine('make', args) - for out, err in cmdline.execute(): - if out is not None: - log.info(out) - log_elem.append(xmlio.Element('message', level='info')[out]) - if err is not None: - log.error(err) - log_elem.append(xmlio.Element('message', level='error')[err]) - ctxt.log(log_elem) - - if cmdline.returncode != 0: + from bitten.build import shtools + returncode = shtools.execute(ctxt, executable='make', args=args) + if returncode != 0: ctxt.error('make failed (%s)' % cmdline.returncode) diff --git a/bitten/build/javatools.py b/bitten/build/javatools.py --- a/bitten/build/javatools.py +++ b/bitten/build/javatools.py @@ -77,31 +77,31 @@ if not error_logged and cmdline.returncode != 0: ctxt.error('Ant failed (%s)' % cmdline.returncode) -def java_src(src, cls): - return posixpath.join(src, *cls.split('.')) + '.java' - -def junit(ctxt, file_=None, src=None): +def junit(ctxt, file_=None, srcdir=None): + """Extract test results from a JUnit XML report.""" assert file_, 'Missing required attribute "file"' try: total, failed = 0, 0 results = xmlio.Fragment() - for f in glob(ctxt.resolve(file_)): - fd = open(f, 'r') + for path in glob(ctxt.resolve(file_)): + fileobj = file(path, 'r') try: - for testcase in xmlio.parse(fd).children('testcase'): + for testcase in xmlio.parse(fileobj).children('testcase'): test = xmlio.Element('test') test.attr['fixture'] = testcase.attr['classname'] - test.attr['duration'] = testcase.attr['time'] - if src: - test.attr['file'] = java_src(src, - testcase.attr['classname']) + if 'time' in testcase.attr: + test.attr['duration'] = testcase.attr['time'] + if srcdir: + cls = testcase.attr['classname'].split('.') + test.attr['file'] = posixpath.join(srcdir, *cls) + \ + '.java' result = list(testcase.children()) if result: test.attr['status'] = result[0].name test.append(xmlio.Element('traceback')[ - result[0].gettext() - ]) + result[0].gettext() + ]) failed += 1 else: test.attr['status'] = 'success' @@ -109,13 +109,12 @@ results.append(test) total += 1 finally: - fd.close() + fileobj.close() if failed: ctxt.error('%d of %d test%s failed' % (failed, total, total != 1 and 's' or '')) ctxt.report('test', results) except IOError, e: - log.warning('Error opening junit results file (%s)', e) + log.warning('Error opening JUnit results file (%s)', e) except xmlio.ParseError, e: - log.warning('Error parsing junit results file (%s)', e) - + log.warning('Error parsing JUnit results file (%s)', e) diff --git a/bitten/build/pythontools.py b/bitten/build/pythontools.py --- a/bitten/build/pythontools.py +++ b/bitten/build/pythontools.py @@ -85,8 +85,8 @@ return from bitten.build import shtools - shtools.exec_(ctxt, executable=_python_path(ctxt), file_=file_, - output=output, args=args) + shtools.execute(ctxt, executable=_python_path(ctxt), file_=file_, + output=output, args=args) def pylint(ctxt, file_=None): """Extract data from a `pylint` run written to a file.""" diff --git a/bitten/build/shtools.py b/bitten/build/shtools.py --- a/bitten/build/shtools.py +++ b/bitten/build/shtools.py @@ -21,57 +21,34 @@ assert executable or file_, \ 'Either "executable" or "file" attribute required' - if args: - args = shlex.split(args) - else: - args = [] - - if executable is None: - executable = file_ - elif file_: - args[:0] = [file_] - - output_file = None - if output: - output = ctxt.resolve(output) - output_file = file(output, 'w') - - try: - cmdline = CommandLine(executable, args, cwd=ctxt.basedir) - log_elem = xmlio.Fragment() - for out, err in cmdline.execute(): - if out is not None: - log.info(out) - log_elem.append(xmlio.Element('message', level='info')[out]) - if output: - output_file.write(out + os.linesep) - if err is not None: - log.error(err) - log_elem.append(xmlio.Element('message', level='error')[err]) - if output: - output_file.write(err + os.linesep) - ctxt.log(log_elem) - finally: - if output: - output_file.close() - - if cmdline.returncode != 0: - ctxt.error('Executing %s failed (%s)' % (executable, - cmdline.returncode)) + returncode = execute(ctxt, executable=executable, file_=file_, + output=output, args=args) + if returncode != 0: + ctxt.error('Executing %s failed (%s)' % (executable or file_, + returncode)) def pipe(ctxt, executable=None, file_=None, input_=None, output=None, args=None): """Pipe the contents of a file through a script.""" assert executable or file_, \ 'Either "executable" or "file" attribute required' - assert input_, 'Missing required attribute "file"' + assert input_, 'Missing required attribute "input"' + returncode = execute(ctxt, executable=executable, file_=file_, + input_=input_, output=output, args=args) + if returncode != 0: + ctxt.error('Piping through %s failed (%s)' % (executable or file_, + returncode)) + +def execute(ctxt, executable=None, file_=None, input_=None, output=None, + args=None): + """Generic external program execution.""" if args: args = shlex.split(args) else: args = [] - if os.path.isfile(ctxt.resolve(file_)): + if file_ and os.path.isfile(ctxt.resolve(file_)): file_ = ctxt.resolve(file_) if executable is None: @@ -79,7 +56,10 @@ elif file_: args[:0] = [file_] - input_file = file(ctxt.resolve(input_), 'r') + if input_: + input_file = file(ctxt.resolve(input_), 'r') + else: + input_file = None output_file = None if output: @@ -93,20 +73,25 @@ for out, err in cmdline.execute(): if out is not None: log.info(out) - log_elem.append(xmlio.Element('message', level='info')[out]) + log_elem.append(xmlio.Element('message', level='info')[ + out.replace(ctxt.basedir + os.sep, '') + .replace(ctxt.basedir, '') + ]) if output: output_file.write(out + os.linesep) if err is not None: log.error(err) - log_elem.append(xmlio.Element('message', level='error')[err]) + log_elem.append(xmlio.Element('message', level='error')[ + err.replace(ctxt.basedir + os.sep, '') + .replace(ctxt.basedir, '') + ]) if output: output_file.write(err + os.linesep) ctxt.log(log_elem) finally: - input_file.close() + if input_: + input_file.close() if output: output_file.close() - if cmdline.returncode != 0: - ctxt.error('Piping through %s failed (%s)' % (executable, - cmdline.returncode)) + return cmdline.returncode diff --git a/bitten/queue.py b/bitten/queue.py --- a/bitten/queue.py +++ b/bitten/queue.py @@ -18,7 +18,7 @@ log = logging.getLogger('bitten.queue') -def collect_changes(repos, config): +def collect_changes(repos, config, db=None): """Collect all changes for a build configuration that either have already been built, or still need to be built. @@ -27,6 +27,8 @@ the changeset, and `build` is a `Build` object or `None`. """ env = config.env + if not db: + db = env.get_db_cnx() node = repos.get_node(config.path) for path, rev, chg in node.get_history(): @@ -53,8 +55,9 @@ # For every target platform, check whether there's a build # of this revision - for platform in TargetPlatform.select(env, config.name): - builds = list(Build.select(env, config.name, rev, platform.id)) + for platform in TargetPlatform.select(env, config.name, db=db): + builds = list(Build.select(env, config.name, rev, platform.id, + db=db)) if builds: build = builds[0] else: @@ -150,8 +153,11 @@ try: repos.sync() - for config in BuildConfig.select(self.env): - for platform, rev, build in collect_changes(repos, config): + db = self.env.get_db_cnx() + build = None + insert_build = False + for config in BuildConfig.select(self.env, db=db): + for platform, rev, build in collect_changes(repos, config, db): if build is None: log.info('Enqueuing build of configuration "%s" at ' 'revision [%s] on %s', config.name, rev, @@ -161,8 +167,10 @@ build.rev = str(rev) build.rev_time = repos.get_changeset(rev).date build.platform = platform.id - build.insert() + insert_build = True break + if insert_build: + build.insert(db=db) finally: repos.close() diff --git a/bitten/trac_ext/api.py b/bitten/trac_ext/api.py --- a/bitten/trac_ext/api.py +++ b/bitten/trac_ext/api.py @@ -40,11 +40,11 @@ """Extension point interface for components that format build log messages.""" - def get_formatter(req, build, step, type): + def get_formatter(req, build): """Return a function that gets called for every log message. - The function must take two positional arguments, `level` and `message`, - and return the formatted message. + The function must take four positional arguments, `step`, `generator`, + `level` and `message`, and return the formatted message. """ diff --git a/bitten/trac_ext/htdocs/bitten.css b/bitten/trac_ext/htdocs/bitten.css --- a/bitten/trac_ext/htdocs/bitten.css +++ b/bitten/trac_ext/htdocs/bitten.css @@ -57,6 +57,8 @@ #content.build div.platforms { margin-top: 2em; } #content.build form.platforms ul { list-style-type: none; padding-left: 1em; } +#content.build p.path { color: #999; font-size: smaller; margin-top: 0; } + #content.build #charts { clear: right; float: right; width: 44%; } #content.build #builds { clear: none; margin-top: 2em; table-layout: fixed; diff --git a/bitten/trac_ext/web_ui.py b/bitten/trac_ext/web_ui.py --- a/bitten/trac_ext/web_ui.py +++ b/bitten/trac_ext/web_ui.py @@ -8,6 +8,7 @@ # are also available at http://bitten.cmlenz.net/wiki/License. from datetime import datetime, timedelta +import posixpath import re try: set @@ -560,6 +561,15 @@ 'href': self.env.href.build(config.name) } + formatters = [] + for formatter in self.log_formatters: + formatters.append(formatter.get_formatter(req, build)) + + summarizers = {} # keyed by report type + for summarizer in self.report_summarizers: + categories = summarizer.get_supported_categories() + summarizers.update(dict([(cat, summarizer) for cat in categories])) + req.hdf['build'] = _build_to_hdf(self.env, req, build) steps = [] for step in BuildStep.select(self.env, build=build.id, db=db): @@ -568,8 +578,9 @@ 'duration': pretty_timedelta(step.started, step.stopped), 'failed': step.status == BuildStep.FAILURE, 'errors': step.errors, - 'log': self._render_log(req, build, step), - 'reports': self._render_reports(req, config, build, step) + 'log': self._render_log(req, build, formatters, step), + 'reports': self._render_reports(req, config, build, summarizers, + step) }) req.hdf['build.steps'] = steps req.hdf['build.can_delete'] = req.perm.has_permission('BUILD_DELETE') @@ -668,25 +679,16 @@ req.redirect(self.env.href.build(build.config)) - def _render_log(self, req, build, step): + def _render_log(self, req, build, formatters, step): items = [] for log in BuildLog.select(self.env, build=build.id, step=step.name): - formatters = [] - for formatter in self.log_formatters: - formatters.append(formatter.get_formatter(req, build, step, - log.generator)) for level, message in log.messages: for format in formatters: - message = format(level, message) + message = format(step, log.generator, level, message) items.append({'level': level, 'message': message}) return items - def _render_reports(self, req, config, build, step): - summarizers = {} # keyed by report type - for summarizer in self.report_summarizers: - categories = summarizer.get_supported_categories() - summarizers.update(dict([(cat, summarizer) for cat in categories])) - + def _render_reports(self, req, config, build, summarizers, step): reports = [] for report in Report.select(self.env, build=build.id, step=step.name): summarizer = summarizers.get(report.category) @@ -700,33 +702,39 @@ class SourceFileLinkFormatter(Component): - """Finds references to files and directories in the repository in the build - log and renders them as links to the repository browser.""" + """Detects references to files in the build log and renders them as links + to the repository browser.""" implements(ILogFormatter) - def get_formatter(self, req, build, step, type): - config = BuildConfig.fetch(self.env, build.config) + _fileref_re = re.compile('(?P[\w.-]+(?:/[\w.-]+)+)(?P(:\d+))') + + def get_formatter(self, req, build): + """Return the log message formatter function.""" + config = BuildConfig.fetch(self.env, name=build.config) repos = self.env.get_repository(req.authname) - nodes = [] - def _walk(node): - for child in node.get_entries(): - path = child.path[len(config.path) + 1:] - pattern = re.compile("([\s'\"])(%s|%s)([\s'\"])" - % (re.escape(path), - re.escape(path.replace('/', '\\')))) - nodes.append((child.path, pattern)) - if child.isdir: - _walk(child) - _walk(repos.get_node(config.path, build.rev)) - nodes.sort(lambda x, y: -cmp(len(x[0]), len(y[0]))) - - def _formatter(level, message): - for path, pattern in nodes: - def _replace(m): - return '%s%s%s' % (m.group(1), - self.env.href.browser(path, rev=build.rev), - m.group(2), m.group(3)) - message = pattern.sub(_replace, message) - return message + href = self.env.href.browser + cache = {} + def _replace(m): + filepath = posixpath.normpath(m.group('path').replace('\\', '/')) + if not cache.get(filepath) is True: + parts = filepath.split('/') + path = '' + for part in parts: + path = posixpath.join(path, part) + if not path in cache: + try: + self.log.debug('Cache miss for "%s" (%s)' % (path, m.group(0))) + repos.get_node(posixpath.join(config.path, path), + build.rev) + cache[path] = True + except TracError: + cache[path] = False + if cache[path] is False: + return m.group(0) + return '%s' % ( + href(m.group('path')) + '#L' + m.group('line')[1:], + m.group(0)) + def _formatter(step, type, level, message): + return self._fileref_re.sub(_replace, message) return _formatter