Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1298)

Unified Diff: scripts/common/annotator.py

Issue 12386016: Initial implementation of annotator library. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Add unit tests, refactor step classes for protection. Created 7 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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())
« no previous file with comments | « no previous file | scripts/common/unittests/annotator_test.py » ('j') | scripts/common/unittests/annotator_test.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698