Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 #!/usr/bin/env python | |
| 2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 """Contains generating and parsing systems of the Chromium Buildbot Annotator. | |
| 7 | |
| 8 When executed as a script, this reads step name / command pairs from a file and | |
| 9 executes those lines while annotating the output. The input is either | |
| 10 | |
| 11 step_name command arg1 arg2... | |
| 12 step_name2 command2 arg1 arg2... | |
| 13 | |
| 14 or if --json is specified: | |
| 15 {"name": "step_name", "cmd": ["command", "arg1", "arg2"]} | |
| 16 {"name": "step_name2", "cmd": ["command2", "arg1"]} | |
|
iannucci
2013/02/28 09:56:03
Why not just do json? It's easier to parse and the
| |
| 17 | |
| 18 """ | |
| 19 | |
| 20 import json | |
| 21 import optparse | |
| 22 import re | |
| 23 import shlex | |
| 24 import sys | |
| 25 import traceback | |
| 26 | |
| 27 from common import chromium_utils | |
| 28 | |
| 29 | |
| 30 class AdvancedAnnotationStream(object): | |
| 31 """Holds individual annotation generating functions. | |
| 32 | |
| 33 Most callers should use StructuredAnnotationStream to simplify coding and | |
| 34 avoid errors. For the rare cases where StructuredAnnotationStream is | |
| 35 insufficient (parallel step execution), the indidividual functions are exposed | |
| 36 here. | |
| 37 | |
| 38 """ | |
| 39 | |
| 40 def __init__(self, stream=sys.stdout): | |
| 41 self.stream = stream | |
| 42 | |
| 43 def emit(self, line): | |
| 44 print >> self.stream, line | |
| 45 | |
| 46 def seed_step(self, step): | |
| 47 self.emit('@@@SEED_STEP %s@@@' % step) | |
|
iannucci
2013/02/28 09:56:03
These methods can all be generated from the same l
| |
| 48 | |
| 49 def step_cursor(self, step): | |
| 50 self.emit('@@@STEP_CURSOR %s@@@' % step) | |
| 51 | |
| 52 def build_step(self, line, step): | |
| 53 self.emit('@@@BUILD_STEP %s@@@' % step) # deprecated, use SEED_STEP | |
| 54 | |
| 55 def step_started(self): | |
| 56 self.emit('@@@STEP_STARTED@@@') | |
| 57 | |
| 58 def step_closed(self): | |
| 59 self.emit('@@@STEP_CLOSED@@@') | |
| 60 | |
| 61 def step_warnings(self): | |
| 62 self.emit('@@@STEP_WARNINGS@@@') | |
| 63 | |
| 64 def step_failure(self): | |
| 65 self.emit('@@@STEP_FAILURE@@@') | |
| 66 | |
| 67 def step_exception(self): | |
| 68 self.emit('@@@STEP_EXCEPTION@@@') | |
| 69 | |
| 70 def halt_on_failure(self): | |
| 71 self.emit('@@@HALT_ON_FAILURE@@@') | |
| 72 | |
| 73 def honor_zero_return_code(self): | |
| 74 self.emit('@@@HONOR_ZERO_RETURN_CODE@@@') | |
| 75 | |
| 76 def step_clear(self): | |
| 77 self.emit('@@@STEP_CLEAR@@@') | |
| 78 | |
| 79 def step_summary_clear(self): | |
| 80 self.emit('@@@STEP_SUMMARY_CLEAR@@@') | |
| 81 | |
| 82 def step_text(self, text): | |
| 83 self.emit('@@@STEP_TEXT@%s@@@' % text) | |
| 84 | |
| 85 def step_summary_text(self, text): | |
| 86 self.emit('@@@STEP_SUMMARY_TEXT@%s@@@' % text) | |
| 87 | |
| 88 def write_log_line(self, logname, line): | |
| 89 self.emit('@@@STEP_LOG_LINE@%s@%s@@@' % (logname, line.rstrip('\n'))) | |
| 90 | |
| 91 def log_end(self, logname): | |
| 92 self.emit('@@@STEP_LOG_END@%s@@@' % logname) | |
| 93 | |
| 94 def log_end_perf(self, logname, perf): | |
| 95 self.emit('@@@STEP_LOG_END_PERF@%s@%s@@@' % (logname, perf)) | |
| 96 | |
| 97 def write_log_lines(self, logname, lines, perf=None): | |
| 98 for line in lines: | |
| 99 self.write_log_line(logname, line) | |
| 100 if perf: | |
| 101 self.log_end_perf(logname, perf) | |
| 102 else: | |
| 103 self.log_end(logname) | |
| 104 | |
| 105 | |
| 106 class StructuredAnnotationStream(AdvancedAnnotationStream): | |
| 107 """Provides an interface to handle an annotated build. | |
| 108 | |
| 109 StructuredAnnotationStream handles most of the step setup and closure calls | |
| 110 for you. All you have to do is execute your code within the steps and set any | |
| 111 failures or warnings that come up. You may optionally provide a list of steps | |
| 112 to seed before execution. | |
| 113 | |
| 114 Usage: | |
| 115 | |
| 116 stream = StructuredAnnotationStream() | |
| 117 with stream.step('compile') as s: | |
| 118 # do something | |
| 119 if error: | |
| 120 s.step_failure() | |
| 121 with stream.step('test') as s: | |
| 122 # do something | |
| 123 if warnings: | |
| 124 s.step_warnings() | |
| 125 """ | |
| 126 | |
| 127 def __init__(self, seed_steps=None, stream=sys.stdout): | |
| 128 super(StructuredAnnotationStream, self).__init__(stream=stream) | |
| 129 seed_steps = seed_steps or [] | |
| 130 self.seed_steps = seed_steps | |
| 131 | |
| 132 for step in seed_steps: | |
| 133 self.seed_step(step) | |
| 134 | |
| 135 self.current_step = '' | |
| 136 | |
| 137 def __enter__(self): | |
| 138 return self | |
| 139 | |
| 140 def __exit__(self, exc_type, exc_value, tb): | |
| 141 if exc_type: | |
| 142 trace = traceback.format_exception(exc_type, exc_value, tb) | |
| 143 unflattened = [l.split('\n') for l in trace] | |
| 144 flattened = [item for sublist in unflattened for item in sublist] | |
| 145 self.write_log_lines('exception', filter(None, flattened)) | |
| 146 self.step_exception() | |
| 147 self.step_closed() | |
| 148 self.current_step = '' | |
| 149 return not exc_type | |
| 150 | |
| 151 def step(self, name): | |
|
iannucci
2013/02/28 09:56:03
I'm not sure I like using self as both the contain
Mike Stip (use stip instead)
2013/02/28 20:35:22
Done.
| |
| 152 """Provide a context with which to execute a step.""" | |
| 153 if self.current_step: | |
| 154 raise Exception('Can\'t start step %s while in step %s.' % ( | |
| 155 name, self.current_step)) | |
| 156 if name in self.seed_steps: | |
| 157 # Seek ahead linearly, skipping steps that weren't emitted in order. | |
| 158 # chromium_step.AnnotatedCommands uses the last in case of duplicated | |
| 159 # step names, so we do the same here. | |
| 160 idx = len(self.seed_steps) - self.seed_steps[::-1].index(name) | |
| 161 self.seed_steps = self.seed_steps[idx:] | |
| 162 else: | |
| 163 self.seed_step(name) | |
| 164 | |
| 165 self.step_cursor(name) | |
| 166 self.step_started() | |
| 167 self.current_step = name | |
| 168 return self | |
| 169 | |
| 170 | |
| 171 class Match: | |
| 172 """Holds annotator line parsing functions.""" | |
| 173 | |
| 174 def __init__(self): | |
| 175 raise Exception('Don\'t instantiate the Match class!') | |
| 176 | |
| 177 @staticmethod | |
| 178 def _parse_line(regex, line): | |
| 179 m = re.match(regex, line) | |
| 180 if m: | |
| 181 return list(m.groups()) | |
| 182 else: | |
| 183 return [] | |
| 184 | |
| 185 @staticmethod | |
| 186 def log_line(line): | |
|
iannucci
2013/02/28 09:56:03
It seems to me like all of these methods should be
Mike Stip (use stip instead)
2013/02/28 20:35:22
I like this idea, but let's save it for a future C
agable
2013/02/28 21:32:08
While a fun trick, that doesn't gain you any perfo
| |
| 187 return Match._parse_line('^@@@STEP_LOG_LINE@(.*)@(.*)@@@', line) | |
| 188 | |
| 189 @staticmethod | |
| 190 def log_end(line): | |
| 191 return Match._parse_line('^@@@STEP_LOG_END@(.*)@@@', line) | |
| 192 | |
| 193 @staticmethod | |
| 194 def log_end_perf(line): | |
| 195 return Match._parse_line('^@@@STEP_LOG_END_PERF@(.*)@(.*)@@@', line) | |
| 196 | |
| 197 @staticmethod | |
| 198 def step_link(line): | |
| 199 m = Match._parse_line('^@@@STEP_LINK@(.*)@(.*)@@@', line) | |
| 200 if not m: | |
| 201 return Match._parse_line('^@@@link@(.*)@(.*)@@@', line) # deprecated | |
| 202 else: | |
| 203 return m | |
| 204 | |
| 205 @staticmethod | |
| 206 def step_started(line): | |
| 207 return line.startswith('@@@STEP_STARTED@@@') | |
| 208 | |
| 209 @staticmethod | |
| 210 def step_closed(line): | |
| 211 return line.startswith('@@@STEP_CLOSED@@@') | |
| 212 | |
| 213 @staticmethod | |
| 214 def step_warnings(line): | |
| 215 return (line.startswith('@@@STEP_WARNINGS@@@') or | |
| 216 line.startswith('@@@BUILD_WARNINGS@@@')) # deprecated | |
| 217 | |
| 218 @staticmethod | |
| 219 def step_failure(line): | |
| 220 return (line.startswith('@@@STEP_FAILURE@@@') or | |
| 221 line.startswith('@@@BUILD_FAILED@@@')) # deprecated | |
| 222 | |
| 223 @staticmethod | |
| 224 def step_exception(line): | |
| 225 return (line.startswith('@@@STEP_EXCEPTION@@@') or | |
| 226 line.startswith('@@@BUILD_EXCEPTION@@@')) # deprecated | |
| 227 | |
| 228 @staticmethod | |
| 229 def halt_on_failure(line): | |
| 230 return line.startswith('@@@HALT_ON_FAILURE@@@') | |
| 231 | |
| 232 @staticmethod | |
| 233 def honor_zero_return_code(line): | |
| 234 return line.startswith('@@@HONOR_ZERO_RETURN_CODE@@@') | |
| 235 | |
| 236 @staticmethod | |
| 237 def step_clear(line): | |
| 238 return line.startswith('@@@STEP_CLEAR@@@') | |
| 239 | |
| 240 @staticmethod | |
| 241 def step_summary_clear(line): | |
| 242 return line.startswith('@@@STEP_SUMMARY_CLEAR@@@') | |
| 243 | |
| 244 @staticmethod | |
| 245 def step_text(line): | |
| 246 return Match._parse_line('^@@@STEP_TEXT@(.*)@@@', line) | |
| 247 | |
| 248 @staticmethod | |
| 249 def step_summary_text(line): | |
| 250 return Match._parse_line('^@@@STEP_SUMMARY_TEXT@(.*)@@@', line) | |
| 251 | |
| 252 @staticmethod | |
| 253 def seed_step(line): | |
| 254 return Match._parse_line('^@@@SEED_STEP (.*)@@@', line) | |
| 255 | |
| 256 @staticmethod | |
| 257 def step_cursor(line): | |
| 258 return Match._parse_line('^@@@STEP_CURSOR (.*)@@@', line) | |
| 259 | |
| 260 @staticmethod | |
| 261 def build_step(line): | |
| 262 return Match._parse_line('^@@@BUILD_STEP (.*)@@@', line) | |
| 263 | |
| 264 | |
| 265 def main(): | |
| 266 usage = '%s <command list file>' % sys.argv[0] | |
| 267 parser = optparse.OptionParser(usage=usage) | |
| 268 parser.add_option('-j', '--json', action='store_true', | |
| 269 help='Specify that each line is a json blob encoding ' | |
| 270 'the step.') | |
| 271 options, args = parser.parse_args() | |
| 272 if not args: | |
| 273 parser.error('Must specify an input filename!') | |
| 274 | |
| 275 steps = [] | |
| 276 with open(args[0], 'rb') as f: | |
| 277 for line in f: | |
| 278 if options.json: | |
| 279 steps.append(json.loads(line)) | |
| 280 if ('cmd' not in steps[-1] or | |
| 281 'name' not in steps[-1]): | |
| 282 print 'line \'%s\' is invalid' % line.rstrip() | |
| 283 return 1 | |
| 284 else: | |
| 285 step = {} | |
| 286 chunks = line.split(' ') | |
| 287 step['name'] = chunks[0] | |
| 288 if not step['name']: | |
| 289 print 'line \'%s\' is invalid' % line.rstrip() | |
| 290 return 1 | |
| 291 command = line[len(step['name'])+1:] | |
| 292 if not command: | |
| 293 print 'line \'%s\' is invalid' % line.rstrip() | |
| 294 return 1 | |
| 295 step['cmd'] = shlex.split(command) | |
| 296 steps.append(step) | |
| 297 | |
| 298 stepnames = [s['name'] for s in steps] | |
| 299 stream = StructuredAnnotationStream(seed_steps = stepnames) | |
| 300 for step in steps: | |
| 301 with stream.step(step['name']) as s: | |
| 302 ret = chromium_utils.RunCommand(step['cmd']) | |
| 303 if ret != 0: | |
| 304 s.step_failure() | |
|
iannucci
2013/02/28 09:56:03
Should ret be interpreted to mean 88 == warning (i
| |
| 305 return 1 | |
|
iannucci
2013/02/28 09:56:03
Do we need a way to express commands which should
Mike Stip (use stip instead)
2013/02/28 20:35:22
Done.
| |
| 306 | |
| 307 return 0 | |
| 308 | |
| 309 | |
| 310 if __name__ == '__main__': | |
| 311 sys.exit(main()) | |
| OLD | NEW |