Chromium Code Reviews| Index: scripts/common/annotator.py |
| diff --git a/scripts/common/annotator.py b/scripts/common/annotator.py |
| new file mode 100755 |
| index 0000000000000000000000000000000000000000..bc444c6c3524ff2e1216599e5172f404c2a46251 |
| --- /dev/null |
| +++ b/scripts/common/annotator.py |
| @@ -0,0 +1,358 @@ |
| +#!/usr/bin/env python |
| +# Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| +# Use of this source code is governed by a BSD-style license that can be |
| +# found in the LICENSE file. |
| + |
| +"""Contains generating and parsing systems of the Chromium Buildbot Annotator. |
| + |
| +When executed as a script, this reads step name / command pairs from a file and |
| +executes those lines while annotating the output. The input is json: |
| + |
| +[{"name": "step_name", "cmd": ["command", "arg1", "arg2"]}, |
| + {"name": "step_name2", "cmd": ["command2", "arg1"]}] |
| + |
| +""" |
| + |
| +import json |
| +import optparse |
| +import re |
| +import sys |
| +import traceback |
| + |
| +from common import chromium_utils |
| + |
| + |
| +class StepCommands(object): |
| + """Subclass holding step commands. Intended to be subclassed.""" |
|
agable
2013/03/02 00:49:56
"""Class holding..."""
|
| + def __init__(self, stream): |
| + self.stream = stream |
| + |
| + def emit(self, line): |
| + print >> self.stream, line |
| + |
| + def step_warnings(self): |
| + self.emit('@@@STEP_WARNINGS@@@') |
| + |
| + def step_failure(self): |
| + self.emit('@@@STEP_FAILURE@@@') |
| + |
| + def step_exception(self): |
| + self.emit('@@@STEP_EXCEPTION@@@') |
| + |
| + def step_clear(self): |
| + self.emit('@@@STEP_CLEAR@@@') |
| + |
| + def step_summary_clear(self): |
| + self.emit('@@@STEP_SUMMARY_CLEAR@@@') |
| + |
| + def step_text(self, text): |
| + self.emit('@@@STEP_TEXT@%s@@@' % text) |
| + |
| + def step_summary_text(self, text): |
| + self.emit('@@@STEP_SUMMARY_TEXT@%s@@@' % text) |
| + |
| + def step_log_line(self, logname, line): |
| + self.emit('@@@STEP_LOG_LINE@%s@%s@@@' % (logname, line.rstrip('\n'))) |
| + |
| + def step_log_end(self, logname): |
| + self.emit('@@@STEP_LOG_END@%s@@@' % logname) |
| + |
| + def step_log_end_perf(self, logname, perf): |
| + self.emit('@@@STEP_LOG_END_PERF@%s@%s@@@' % (logname, perf)) |
| + |
| + def write_log_lines(self, logname, lines, perf=None): |
| + for line in lines: |
| + self.step_log_line(logname, line) |
| + if perf: |
| + self.step_log_end_perf(logname, perf) |
| + else: |
| + self.step_log_end(logname) |
| + |
| + |
| +class StepControlCommands(object): |
| + """Subclass holding step control commands. Intended to be subclassed. |
| + |
| + This is subclassed out so callers in StructuredAnnotationStep can't call |
| + step_started() or step_closed(). |
| + |
| + """ |
| + def __init__(self, stream): |
| + self.stream = stream |
| + |
| + def emit(self, line): |
|
iannucci
2013/03/02 01:49:14
Seems like there should be a way to only have 'emi
|
| + print >> self.stream, line |
| + |
| + def step_started(self): |
| + self.emit('@@@STEP_STARTED@@@') |
| + |
| + def step_closed(self): |
| + self.emit('@@@STEP_CLOSED@@@') |
| + |
| + |
| +class StructuredAnnotationStep(StepCommands): |
| + """Helper class to provide context for a step.""" |
| + |
| + def __init__(self, annotation_stream, *args, **kwargs): |
| + self.annotation_stream = annotation_stream |
| + super(StructuredAnnotationStep, self).__init__(*args, **kwargs) |
| + self.control = StepControlCommands(self.stream) |
| + |
| + def emit(self, line): |
| + print >> self.stream, line |
| + |
| + def __enter__(self): |
| + self.control.step_started() |
| + return self |
| + |
| + def __exit__(self, exc_type, exc_value, tb): |
| + if exc_type: |
| + trace = traceback.format_exception(exc_type, exc_value, tb) |
| + unflattened = [l.split('\n') for l in trace] |
| + flattened = [item for sublist in unflattened for item in sublist] |
|
iannucci
2013/03/02 01:49:14
Maybe "".join(trace).split('\n')
|
| + self.write_log_lines('exception', filter(None, flattened)) |
| + self.step_exception() |
| + |
| + self.control.step_closed() |
| + self.annotation_stream.current_step = '' |
| + return not exc_type |
| + |
| +class AdvancedAnnotationStep(StepCommands, StepControlCommands): |
| + """Holds additional step functions for finer step control. |
| + |
| + Most users will want to use StructuredAnnotationSteps generated from a |
| + StructuredAnnotationStream as these handle state automatically. |
| + """ |
| + |
| + def __init__(self, *args, **kwargs): |
| + super(AdvancedAnnotationStep, self).__init__(*args, **kwargs) |
| + |
| + |
| +class AdvancedAnnotationStream(object): |
| + """Holds individual annotation generating functions for streams. |
| + |
| + Most callers should use StructuredAnnotationStream to simplify coding and |
| + avoid errors. For the rare cases where StructuredAnnotationStream is |
| + insufficient (parallel step execution), the indidividual functions are exposed |
| + here. |
| + |
|
agable
2013/03/02 00:49:56
No newline.
|
| + """ |
| + |
| + def __init__(self, stream=sys.stdout): |
| + self.stream = stream |
| + |
| + def emit(self, line): |
| + print >> self.stream, line |
| + |
| + def seed_step(self, step): |
| + self.emit('@@@SEED_STEP %s@@@' % step) |
| + |
| + def step_cursor(self, step): |
| + self.emit('@@@STEP_CURSOR %s@@@' % step) |
| + |
| + def halt_on_failure(self): |
| + self.emit('@@@HALT_ON_FAILURE@@@') |
| + |
| + def honor_zero_return_code(self): |
| + self.emit('@@@HONOR_ZERO_RETURN_CODE@@@') |
| + |
| + |
| +class StructuredAnnotationStream(AdvancedAnnotationStream): |
| + """Provides an interface to handle an annotated build. |
| + |
| + StructuredAnnotationStream handles most of the step setup and closure calls |
| + for you. All you have to do is execute your code within the steps and set any |
| + failures or warnings that come up. You may optionally provide a list of steps |
| + to seed before execution. |
| + |
| + Usage: |
| + |
| + stream = StructuredAnnotationStream() |
| + with stream.step('compile') as s: |
| + # do something |
| + if error: |
| + s.step_failure() |
| + with stream.step('test') as s: |
| + # do something |
| + if warnings: |
| + s.step_warnings() |
| + """ |
| + |
| + def __init__(self, seed_steps=None, stream=sys.stdout): |
| + super(StructuredAnnotationStream, self).__init__(stream=stream) |
| + seed_steps = seed_steps or [] |
| + self.seed_steps = seed_steps |
| + |
| + for step in seed_steps: |
| + self.seed_step(step) |
| + |
| + self.current_step = '' |
| + |
| + def step(self, name): |
| + """Provide a context with which to execute a step.""" |
| + if self.current_step: |
| + raise Exception('Can\'t start step %s while in step %s.' % ( |
| + name, self.current_step)) |
| + if name in self.seed_steps: |
| + # Seek ahead linearly, skipping steps that weren't emitted in order. |
| + # chromium_step.AnnotatedCommands uses the last in case of duplicated |
| + # step names, so we do the same here. |
| + idx = len(self.seed_steps) - self.seed_steps[::-1].index(name) |
| + self.seed_steps = self.seed_steps[idx:] |
| + else: |
| + self.seed_step(name) |
| + |
| + self.step_cursor(name) |
| + self.current_step = name |
| + return StructuredAnnotationStep(self, stream=self.stream) |
| + |
| + |
| +class Match: |
| + """Holds annotator line parsing functions.""" |
| + |
| + def __init__(self): |
| + raise Exception('Don\'t instantiate the Match class!') |
| + |
| + @staticmethod |
| + def _parse_line(regex, line): |
| + m = re.match(regex, line) |
| + if m: |
| + return list(m.groups()) |
| + else: |
| + return [] |
| + |
| + @staticmethod |
| + def log_line(line): |
| + return Match._parse_line('^@@@STEP_LOG_LINE@(.*)@(.*)@@@', line) |
| + |
| + @staticmethod |
| + def log_end(line): |
| + return Match._parse_line('^@@@STEP_LOG_END@(.*)@@@', line) |
| + |
| + @staticmethod |
| + def log_end_perf(line): |
| + return Match._parse_line('^@@@STEP_LOG_END_PERF@(.*)@(.*)@@@', line) |
| + |
| + @staticmethod |
| + def step_link(line): |
| + m = Match._parse_line('^@@@STEP_LINK@(.*)@(.*)@@@', line) |
| + if not m: |
| + return Match._parse_line('^@@@link@(.*)@(.*)@@@', line) # Deprecated. |
| + else: |
| + return m |
| + |
| + @staticmethod |
| + def step_started(line): |
| + return line.startswith('@@@STEP_STARTED@@@') |
| + |
| + @staticmethod |
| + def step_closed(line): |
| + return line.startswith('@@@STEP_CLOSED@@@') |
| + |
| + @staticmethod |
| + def step_warnings(line): |
| + return (line.startswith('@@@STEP_WARNINGS@@@') or |
| + line.startswith('@@@BUILD_WARNINGS@@@')) # Deprecated. |
| + |
| + @staticmethod |
| + def step_failure(line): |
| + return (line.startswith('@@@STEP_FAILURE@@@') or |
| + line.startswith('@@@BUILD_FAILED@@@')) # Deprecated. |
| + |
| + @staticmethod |
| + def step_exception(line): |
| + return (line.startswith('@@@STEP_EXCEPTION@@@') or |
| + line.startswith('@@@BUILD_EXCEPTION@@@')) # Deprecated. |
| + |
| + @staticmethod |
| + def halt_on_failure(line): |
| + return line.startswith('@@@HALT_ON_FAILURE@@@') |
| + |
| + @staticmethod |
| + def honor_zero_return_code(line): |
| + return line.startswith('@@@HONOR_ZERO_RETURN_CODE@@@') |
| + |
| + @staticmethod |
| + def step_clear(line): |
| + return line.startswith('@@@STEP_CLEAR@@@') |
| + |
| + @staticmethod |
| + def step_summary_clear(line): |
| + return line.startswith('@@@STEP_SUMMARY_CLEAR@@@') |
| + |
| + @staticmethod |
| + def step_text(line): |
| + return Match._parse_line('^@@@STEP_TEXT@(.*)@@@', line) |
| + |
| + @staticmethod |
| + def step_summary_text(line): |
| + return Match._parse_line('^@@@STEP_SUMMARY_TEXT@(.*)@@@', line) |
| + |
| + @staticmethod |
| + def seed_step(line): |
| + return Match._parse_line('^@@@SEED_STEP (.*)@@@', line) |
| + |
| + @staticmethod |
| + def step_cursor(line): |
| + return Match._parse_line('^@@@STEP_CURSOR (.*)@@@', line) |
| + |
| + @staticmethod |
| + def build_step(line): |
| + return Match._parse_line('^@@@BUILD_STEP (.*)@@@', line) |
| + |
| + |
| +def main(): |
| + usage = '%s <command list file>' % sys.argv[0] |
| + parser = optparse.OptionParser(usage=usage) |
| + _, args = parser.parse_args() |
| + if not args: |
| + parser.error('Must specify an input filename!') |
|
iannucci
2013/03/02 01:49:14
Check for len(args) > 1?
|
| + |
| + steps = [] |
| + with open(args[0], 'rb') as f: |
| + steps.extend(json.load(f)) |
| + |
| + for step in steps: |
| + if ('cmd' not in step or |
| + 'name' not in step): |
| + print 'step \'%s\' is invalid' % json.dumps(step) |
| + return 1 |
| + |
| + # Make sure these steps always run, even if there is a build failure. |
| + always_run = {} |
| + for step in steps: |
| + if step.get('always_run'): |
| + always_run[step['name']] = step |
| + |
| + stepnames = [s['name'] for s in steps] |
| + |
| + stream = StructuredAnnotationStream(seed_steps=stepnames) |
| + build_failure = False |
| + for step in steps: |
| + if step['name'] in always_run: |
| + del always_run[step['name']] |
| + try: |
| + with stream.step(step['name']) as s: |
| + ret = chromium_utils.RunCommand(step['cmd']) |
| + if ret != 0: |
| + s.step_failure() |
| + build_failure = True |
| + break |
| + except OSError: |
| + # File wasn't found, error has been already reported to stream. |
| + build_failure = True |
| + break |
| + |
| + for step_name in always_run: |
| + with stream.step(step_name) as s: |
| + ret = chromium_utils.RunCommand(always_run[step_name]['cmd']) |
| + if ret != 0: |
| + s.step_failure() |
| + build_failure = True |
| + |
| + if build_failure: |
| + return 1 |
| + return 0 |
| + |
| + |
| +if __name__ == '__main__': |
| + sys.exit(main()) |