Index: third_party/recipe_engine/third_party/annotator.py |
diff --git a/third_party/recipe_engine/third_party/annotator.py b/third_party/recipe_engine/third_party/annotator.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..8fcb5ebc02ba7e2aa3554c2c54cafb7ae9155f85 |
--- /dev/null |
+++ b/third_party/recipe_engine/third_party/annotator.py |
@@ -0,0 +1,348 @@ |
+# Copyright (c) 2015 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 the parsing system of the Chromium Buildbot Annotator.""" |
+ |
+import os |
+import sys |
+import traceback |
+ |
+# These are maps of annotation key -> number of expected arguments. |
+STEP_ANNOTATIONS = { |
+ 'SET_BUILD_PROPERTY': 2, |
+ 'STEP_CLEAR': 0, |
+ 'STEP_EXCEPTION': 0, |
+ 'STEP_FAILURE': 0, |
+ 'STEP_LINK': 2, |
+ 'STEP_LOG_END': 1, |
+ 'STEP_LOG_END_PERF': 2, |
+ 'STEP_LOG_LINE': 2, |
+ 'STEP_SUMMARY_CLEAR': 0, |
+ 'STEP_SUMMARY_TEXT': 1, |
+ 'STEP_TEXT': 1, |
+ 'STEP_TRIGGER': 1, |
+ 'STEP_WARNINGS': 0, |
+ 'STEP_NEST_LEVEL': 1, |
+} |
+ |
+CONTROL_ANNOTATIONS = { |
+ 'STEP_CLOSED': 0, |
+ 'STEP_STARTED': 0, |
+} |
+ |
+STREAM_ANNOTATIONS = { |
+ 'HALT_ON_FAILURE': 0, |
+ 'HONOR_ZERO_RETURN_CODE': 0, |
+ 'SEED_STEP': 1, |
+ 'SEED_STEP_TEXT': 2, |
+ 'STEP_CURSOR': 1, |
+} |
+ |
+DEPRECATED_ANNOTATIONS = { |
+ 'BUILD_STEP': 1, |
+} |
+ |
+ALL_ANNOTATIONS = {} |
+ALL_ANNOTATIONS.update(STEP_ANNOTATIONS) |
+ALL_ANNOTATIONS.update(CONTROL_ANNOTATIONS) |
+ALL_ANNOTATIONS.update(STREAM_ANNOTATIONS) |
+ALL_ANNOTATIONS.update(DEPRECATED_ANNOTATIONS) |
+ |
+# This is a mapping of old_annotation_name -> new_annotation_name. |
+# Theoretically all annotator scripts should use the new names, but it's hard |
+# to tell due to the decentralized nature of the annotator. |
+DEPRECATED_ALIASES = { |
+ 'BUILD_FAILED': 'STEP_FAILURE', |
+ 'BUILD_WARNINGS': 'STEP_WARNINGS', |
+ 'BUILD_EXCEPTION': 'STEP_EXCEPTION', |
+ 'link': 'STEP_LINK', |
+} |
+ |
+# A couple of the annotations have the format: |
+# @@@THING arg@@@ |
+# for reasons no one knows. We only need this case until all masters have been |
+# restarted to pick up the new master-side parsing code. |
+OLD_STYLE_ANNOTATIONS = set(( |
+ 'SEED_STEP', |
+ 'STEP_CURSOR', |
+)) |
+ |
+ |
+def emit(line, stream, flush_before=None): |
+ if flush_before: |
+ flush_before.flush() |
+ print >> stream |
+ # WinDOS can only handle 64kb of output to the console at a time, per process. |
+ if sys.platform.startswith('win'): |
+ lim = 2**15 |
+ while line: |
+ to_print, line = line[:lim], line[lim:] |
+ stream.write(to_print) |
+ stream.write('\n') |
+ else: |
+ print >> stream, line |
+ stream.flush() |
+ |
+ |
+class MetaAnnotationPrinter(type): |
+ def __new__(mcs, name, bases, dct): |
+ annotation_map = dct.get('ANNOTATIONS') |
+ if annotation_map: |
+ for key, v in annotation_map.iteritems(): |
+ key = key.lower() |
+ dct[key] = mcs.make_printer_fn(key, v) |
+ return type.__new__(mcs, name, bases, dct) |
+ |
+ @staticmethod |
+ def make_printer_fn(name, n_args): |
+ """Generates a method which emits an annotation to the log stream.""" |
+ upname = name.upper() |
+ if upname in OLD_STYLE_ANNOTATIONS: |
+ assert n_args >= 1 |
+ fmt = '@@@%s %%s%s@@@' % (upname, '@%s' * (n_args - 1)) |
+ else: |
+ fmt = '@@@%s%s@@@' % (upname, '@%s' * n_args) |
+ |
+ inner_args = n_args + 1 # self counts |
+ infix = '1 argument' if inner_args == 1 else ('%d arguments' % inner_args) |
+ err = '%s() takes %s (%%d given)' % (name, infix) |
+ |
+ def printer(self, *args): |
+ if len(args) != n_args: |
+ raise TypeError(err % (len(args) + 1)) |
+ self.emit(fmt % args) |
+ printer.__name__ = name |
+ printer.__doc__ = """Emits an annotation for %s.""" % name.upper() |
+ |
+ return printer |
+ |
+ |
+class AnnotationPrinter(object): |
+ """A derivable class which will inject annotation-printing methods into the |
+ subclass. |
+ |
+ A subclass should define a class variable ANNOTATIONS equal to a |
+ dictionary of the form { '<ANNOTATION_NAME>': <# args> }. This class will |
+ then inject methods whose names are the undercased version of your |
+ annotation names, and which take the number of arguments specified in the |
+ dictionary. |
+ |
+ Example: |
+ >>> my_annotations = { 'STEP_LOG_LINE': 2 } |
+ >>> class MyObj(AnnotationPrinter): |
+ ... ANNOTATIONS = my_annotations |
+ ... |
+ >>> o = MyObj() |
+ >>> o.step_log_line('logname', 'here is a line to put in the log') |
+ @@@STEP_LOG_LINE@logname@here is a line to put in the log@@@ |
+ >>> o.step_log_line() |
+ Traceback (most recent call last): |
+ File "<stdin>", line 1, in <module> |
+ TypeError: step_log_line() takes exactly 3 arguments (1 given) |
+ >>> o.setp_log_line.__doc__ |
+ "Emits an annotation for STEP_LOG_LINE." |
+ >>> |
+ """ |
+ __metaclass__ = MetaAnnotationPrinter |
+ |
+ def __init__(self, stream, flush_before): |
+ self.stream = stream |
+ self.flush_before = flush_before |
+ |
+ def emit(self, line): |
+ emit(line, self.stream, self.flush_before) |
+ |
+ |
+class StepCommands(AnnotationPrinter): |
+ """Class holding step commands. Intended to be subclassed.""" |
+ ANNOTATIONS = STEP_ANNOTATIONS |
+ |
+ def __init__(self, stream, flush_before): |
+ super(StepCommands, self).__init__(stream, flush_before) |
+ self.emitted_logs = set() |
+ |
+ def write_log_lines(self, logname, lines, perf=None): |
+ if logname in self.emitted_logs: |
+ raise ValueError('Log %s has been emitted multiple times.' % logname) |
+ self.emitted_logs.add(logname) |
+ |
+ logname = logname.replace('/', '/') |
+ |
+ for line in lines: |
+ for actual_line in line.split('\n'): |
+ self.step_log_line(logname, actual_line) |
+ |
+ if perf: |
+ self.step_log_end_perf(logname, perf) |
+ else: |
+ self.step_log_end(logname) |
+ |
+ |
+class StepControlCommands(AnnotationPrinter): |
+ """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(). |
+ """ |
+ ANNOTATIONS = CONTROL_ANNOTATIONS |
+ |
+ |
+class StructuredAnnotationStep(StepCommands, StepControlCommands): |
+ """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, self.flush_before) |
+ self.emitted_logs = set() |
+ |
+ |
+ def __enter__(self): |
+ return self.step_started() |
+ |
+ def step_started(self): |
+ self.control.step_started() |
+ return self |
+ |
+ def __exit__(self, exc_type, exc_value, tb): |
+ self.annotation_stream.step_cursor(self.annotation_stream.current_step) |
+ #TODO(martinis) combine this and step_ended |
+ if exc_type: |
+ self.step_exception_occured(exc_type, exc_value, tb) |
+ |
+ self.control.step_closed() |
+ self.annotation_stream.current_step = '' |
+ return not exc_type |
+ |
+ def step_exception_occured(self, exc_type, exc_value, tb): |
+ trace = traceback.format_exception(exc_type, exc_value, tb) |
+ trace_lines = ''.join(trace).split('\n') |
+ self.write_log_lines('exception', filter(None, trace_lines)) |
+ self.step_exception() |
+ |
+ def step_ended(self): |
+ self.annotation_stream.step_cursor(self.annotation_stream.current_step) |
+ self.control.step_closed() |
+ self.annotation_stream.current_step = '' |
+ |
+ return True |
+ |
+ |
+class StructuredAnnotationStream(AnnotationPrinter): |
+ """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() |
+ """ |
+ ANNOTATIONS = STREAM_ANNOTATIONS |
+ |
+ def __init__(self, stream=sys.stdout, |
+ flush_before=sys.stderr, |
+ seed_steps=None): # pylint: disable=W0613 |
+ super(StructuredAnnotationStream, self).__init__(stream=stream, |
+ flush_before=flush_before) |
+ 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)) |
+ |
+ self.seed_step(name) |
+ self.step_cursor(name) |
+ self.current_step = name |
+ return StructuredAnnotationStep(self, stream=self.stream, |
+ flush_before=self.flush_before) |
+ |
+ |
+def MatchAnnotation(line, callback_implementor): |
+ """Call back into |callback_implementor| if line contains an annotation. |
+ |
+ Args: |
+ line (str) - The line to analyze |
+ callback_implementor (object) - An object which contains methods |
+ corresponding to all of the annotations in the |ALL_ANNOTATIONS| |
+ dictionary. For example, it should contain a method STEP_SUMMARY_TEXT |
+ taking a single argument. |
+ |
+ Parsing method: |
+ * if line doesn't match /^@@@.*@@@$/, return without calling back |
+ * Look for the first '@' or ' ' |
+ """ |
+ if not (line.startswith('@@@') and line.endswith('@@@') and len(line) > 6): |
+ return |
+ line = line[3:-3] |
+ |
+ # look until the first @ or ' ' |
+ idx = min((x for x in (line.find('@'), line.find(' '), len(line)) if x > 0)) |
+ cmd_text = line[:idx] |
+ cmd = DEPRECATED_ALIASES.get(cmd_text, cmd_text) |
+ |
+ field_count = ALL_ANNOTATIONS.get(cmd) |
+ if field_count is None: |
+ raise Exception('Unrecognized annotator command "%s"' % cmd_text) |
+ |
+ if field_count: |
+ if idx == len(line): |
+ raise Exception('Annotator command "%s" expects %d args, got 0.' |
+ % (cmd_text, field_count)) |
+ |
+ line = line[idx+1:] |
+ |
+ args = line.split('@', field_count-1) |
+ if len(args) != field_count: |
+ raise Exception('Annotator command "%s" expects %d args, got %d.' |
+ % (cmd_text, field_count, len(args))) |
+ else: |
+ line = line[len(cmd_text):] |
+ if line: |
+ raise Exception('Annotator command "%s" expects no args, got cruft "%s".' |
+ % (cmd_text, line)) |
+ args = [] |
+ |
+ fn = getattr(callback_implementor, cmd, None) |
+ if fn is None: |
+ raise Exception('"%s" does not implement "%s"' |
+ % (callback_implementor, cmd)) |
+ |
+ fn(*args) |
+ |
+ |
+def print_step(step, env, stream): |
+ """Prints the step command and relevant metadata. |
+ |
+ Intended to be similar to the information that Buildbot prints at the |
+ beginning of each non-annotator step. |
+ """ |
+ step_info_lines = [] |
+ step_info_lines.append(' '.join(step['cmd'])) |
+ step_info_lines.append('in dir %s:' % (step['cwd'] or os.getcwd())) |
+ for key, value in sorted(step.items()): |
+ if value is not None: |
+ if callable(value): |
+ # This prevents functions from showing up as: |
+ # '<function foo at 0x7f523ec7a410>' |
+ # which is tricky to test. |
+ value = value.__name__+'(...)' |
+ step_info_lines.append(' %s: %s' % (key, value)) |
+ step_info_lines.append('full environment:') |
+ for key, value in sorted(env.items()): |
+ step_info_lines.append(' %s: %s' % (key, value)) |
+ step_info_lines.append('') |
+ stream.emit('\n'.join(step_info_lines)) |