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 json: | |
| 10 | |
| 11 [{"name": "step_name", "cmd": ["command", "arg1", "arg2"]}, | |
| 12 {"name": "step_name2", "cmd": ["command2", "arg1"]}] | |
| 13 | |
| 14 """ | |
| 15 | |
| 16 import json | |
| 17 import optparse | |
| 18 import re | |
| 19 import sys | |
| 20 import traceback | |
| 21 | |
| 22 from common import chromium_utils | |
| 23 | |
| 24 | |
| 25 class StepCommands(object): | |
| 26 """Subclass holding step commands. Intended to be subclassed.""" | |
|
agable
2013/03/02 00:49:56
"""Class holding..."""
| |
| 27 def __init__(self, stream): | |
| 28 self.stream = stream | |
| 29 | |
| 30 def emit(self, line): | |
| 31 print >> self.stream, line | |
| 32 | |
| 33 def step_warnings(self): | |
| 34 self.emit('@@@STEP_WARNINGS@@@') | |
| 35 | |
| 36 def step_failure(self): | |
| 37 self.emit('@@@STEP_FAILURE@@@') | |
| 38 | |
| 39 def step_exception(self): | |
| 40 self.emit('@@@STEP_EXCEPTION@@@') | |
| 41 | |
| 42 def step_clear(self): | |
| 43 self.emit('@@@STEP_CLEAR@@@') | |
| 44 | |
| 45 def step_summary_clear(self): | |
| 46 self.emit('@@@STEP_SUMMARY_CLEAR@@@') | |
| 47 | |
| 48 def step_text(self, text): | |
| 49 self.emit('@@@STEP_TEXT@%s@@@' % text) | |
| 50 | |
| 51 def step_summary_text(self, text): | |
| 52 self.emit('@@@STEP_SUMMARY_TEXT@%s@@@' % text) | |
| 53 | |
| 54 def step_log_line(self, logname, line): | |
| 55 self.emit('@@@STEP_LOG_LINE@%s@%s@@@' % (logname, line.rstrip('\n'))) | |
| 56 | |
| 57 def step_log_end(self, logname): | |
| 58 self.emit('@@@STEP_LOG_END@%s@@@' % logname) | |
| 59 | |
| 60 def step_log_end_perf(self, logname, perf): | |
| 61 self.emit('@@@STEP_LOG_END_PERF@%s@%s@@@' % (logname, perf)) | |
| 62 | |
| 63 def write_log_lines(self, logname, lines, perf=None): | |
| 64 for line in lines: | |
| 65 self.step_log_line(logname, line) | |
| 66 if perf: | |
| 67 self.step_log_end_perf(logname, perf) | |
| 68 else: | |
| 69 self.step_log_end(logname) | |
| 70 | |
| 71 | |
| 72 class StepControlCommands(object): | |
| 73 """Subclass holding step control commands. Intended to be subclassed. | |
| 74 | |
| 75 This is subclassed out so callers in StructuredAnnotationStep can't call | |
| 76 step_started() or step_closed(). | |
| 77 | |
| 78 """ | |
| 79 def __init__(self, stream): | |
| 80 self.stream = stream | |
| 81 | |
| 82 def emit(self, line): | |
|
iannucci
2013/03/02 01:49:14
Seems like there should be a way to only have 'emi
| |
| 83 print >> self.stream, line | |
| 84 | |
| 85 def step_started(self): | |
| 86 self.emit('@@@STEP_STARTED@@@') | |
| 87 | |
| 88 def step_closed(self): | |
| 89 self.emit('@@@STEP_CLOSED@@@') | |
| 90 | |
| 91 | |
| 92 class StructuredAnnotationStep(StepCommands): | |
| 93 """Helper class to provide context for a step.""" | |
| 94 | |
| 95 def __init__(self, annotation_stream, *args, **kwargs): | |
| 96 self.annotation_stream = annotation_stream | |
| 97 super(StructuredAnnotationStep, self).__init__(*args, **kwargs) | |
| 98 self.control = StepControlCommands(self.stream) | |
| 99 | |
| 100 def emit(self, line): | |
| 101 print >> self.stream, line | |
| 102 | |
| 103 def __enter__(self): | |
| 104 self.control.step_started() | |
| 105 return self | |
| 106 | |
| 107 def __exit__(self, exc_type, exc_value, tb): | |
| 108 if exc_type: | |
| 109 trace = traceback.format_exception(exc_type, exc_value, tb) | |
| 110 unflattened = [l.split('\n') for l in trace] | |
| 111 flattened = [item for sublist in unflattened for item in sublist] | |
|
iannucci
2013/03/02 01:49:14
Maybe "".join(trace).split('\n')
| |
| 112 self.write_log_lines('exception', filter(None, flattened)) | |
| 113 self.step_exception() | |
| 114 | |
| 115 self.control.step_closed() | |
| 116 self.annotation_stream.current_step = '' | |
| 117 return not exc_type | |
| 118 | |
| 119 class AdvancedAnnotationStep(StepCommands, StepControlCommands): | |
| 120 """Holds additional step functions for finer step control. | |
| 121 | |
| 122 Most users will want to use StructuredAnnotationSteps generated from a | |
| 123 StructuredAnnotationStream as these handle state automatically. | |
| 124 """ | |
| 125 | |
| 126 def __init__(self, *args, **kwargs): | |
| 127 super(AdvancedAnnotationStep, self).__init__(*args, **kwargs) | |
| 128 | |
| 129 | |
| 130 class AdvancedAnnotationStream(object): | |
| 131 """Holds individual annotation generating functions for streams. | |
| 132 | |
| 133 Most callers should use StructuredAnnotationStream to simplify coding and | |
| 134 avoid errors. For the rare cases where StructuredAnnotationStream is | |
| 135 insufficient (parallel step execution), the indidividual functions are exposed | |
| 136 here. | |
| 137 | |
|
agable
2013/03/02 00:49:56
No newline.
| |
| 138 """ | |
| 139 | |
| 140 def __init__(self, stream=sys.stdout): | |
| 141 self.stream = stream | |
| 142 | |
| 143 def emit(self, line): | |
| 144 print >> self.stream, line | |
| 145 | |
| 146 def seed_step(self, step): | |
| 147 self.emit('@@@SEED_STEP %s@@@' % step) | |
| 148 | |
| 149 def step_cursor(self, step): | |
| 150 self.emit('@@@STEP_CURSOR %s@@@' % step) | |
| 151 | |
| 152 def halt_on_failure(self): | |
| 153 self.emit('@@@HALT_ON_FAILURE@@@') | |
| 154 | |
| 155 def honor_zero_return_code(self): | |
| 156 self.emit('@@@HONOR_ZERO_RETURN_CODE@@@') | |
| 157 | |
| 158 | |
| 159 class StructuredAnnotationStream(AdvancedAnnotationStream): | |
| 160 """Provides an interface to handle an annotated build. | |
| 161 | |
| 162 StructuredAnnotationStream handles most of the step setup and closure calls | |
| 163 for you. All you have to do is execute your code within the steps and set any | |
| 164 failures or warnings that come up. You may optionally provide a list of steps | |
| 165 to seed before execution. | |
| 166 | |
| 167 Usage: | |
| 168 | |
| 169 stream = StructuredAnnotationStream() | |
| 170 with stream.step('compile') as s: | |
| 171 # do something | |
| 172 if error: | |
| 173 s.step_failure() | |
| 174 with stream.step('test') as s: | |
| 175 # do something | |
| 176 if warnings: | |
| 177 s.step_warnings() | |
| 178 """ | |
| 179 | |
| 180 def __init__(self, seed_steps=None, stream=sys.stdout): | |
| 181 super(StructuredAnnotationStream, self).__init__(stream=stream) | |
| 182 seed_steps = seed_steps or [] | |
| 183 self.seed_steps = seed_steps | |
| 184 | |
| 185 for step in seed_steps: | |
| 186 self.seed_step(step) | |
| 187 | |
| 188 self.current_step = '' | |
| 189 | |
| 190 def step(self, name): | |
| 191 """Provide a context with which to execute a step.""" | |
| 192 if self.current_step: | |
| 193 raise Exception('Can\'t start step %s while in step %s.' % ( | |
| 194 name, self.current_step)) | |
| 195 if name in self.seed_steps: | |
| 196 # Seek ahead linearly, skipping steps that weren't emitted in order. | |
| 197 # chromium_step.AnnotatedCommands uses the last in case of duplicated | |
| 198 # step names, so we do the same here. | |
| 199 idx = len(self.seed_steps) - self.seed_steps[::-1].index(name) | |
| 200 self.seed_steps = self.seed_steps[idx:] | |
| 201 else: | |
| 202 self.seed_step(name) | |
| 203 | |
| 204 self.step_cursor(name) | |
| 205 self.current_step = name | |
| 206 return StructuredAnnotationStep(self, stream=self.stream) | |
| 207 | |
| 208 | |
| 209 class Match: | |
| 210 """Holds annotator line parsing functions.""" | |
| 211 | |
| 212 def __init__(self): | |
| 213 raise Exception('Don\'t instantiate the Match class!') | |
| 214 | |
| 215 @staticmethod | |
| 216 def _parse_line(regex, line): | |
| 217 m = re.match(regex, line) | |
| 218 if m: | |
| 219 return list(m.groups()) | |
| 220 else: | |
| 221 return [] | |
| 222 | |
| 223 @staticmethod | |
| 224 def log_line(line): | |
| 225 return Match._parse_line('^@@@STEP_LOG_LINE@(.*)@(.*)@@@', line) | |
| 226 | |
| 227 @staticmethod | |
| 228 def log_end(line): | |
| 229 return Match._parse_line('^@@@STEP_LOG_END@(.*)@@@', line) | |
| 230 | |
| 231 @staticmethod | |
| 232 def log_end_perf(line): | |
| 233 return Match._parse_line('^@@@STEP_LOG_END_PERF@(.*)@(.*)@@@', line) | |
| 234 | |
| 235 @staticmethod | |
| 236 def step_link(line): | |
| 237 m = Match._parse_line('^@@@STEP_LINK@(.*)@(.*)@@@', line) | |
| 238 if not m: | |
| 239 return Match._parse_line('^@@@link@(.*)@(.*)@@@', line) # Deprecated. | |
| 240 else: | |
| 241 return m | |
| 242 | |
| 243 @staticmethod | |
| 244 def step_started(line): | |
| 245 return line.startswith('@@@STEP_STARTED@@@') | |
| 246 | |
| 247 @staticmethod | |
| 248 def step_closed(line): | |
| 249 return line.startswith('@@@STEP_CLOSED@@@') | |
| 250 | |
| 251 @staticmethod | |
| 252 def step_warnings(line): | |
| 253 return (line.startswith('@@@STEP_WARNINGS@@@') or | |
| 254 line.startswith('@@@BUILD_WARNINGS@@@')) # Deprecated. | |
| 255 | |
| 256 @staticmethod | |
| 257 def step_failure(line): | |
| 258 return (line.startswith('@@@STEP_FAILURE@@@') or | |
| 259 line.startswith('@@@BUILD_FAILED@@@')) # Deprecated. | |
| 260 | |
| 261 @staticmethod | |
| 262 def step_exception(line): | |
| 263 return (line.startswith('@@@STEP_EXCEPTION@@@') or | |
| 264 line.startswith('@@@BUILD_EXCEPTION@@@')) # Deprecated. | |
| 265 | |
| 266 @staticmethod | |
| 267 def halt_on_failure(line): | |
| 268 return line.startswith('@@@HALT_ON_FAILURE@@@') | |
| 269 | |
| 270 @staticmethod | |
| 271 def honor_zero_return_code(line): | |
| 272 return line.startswith('@@@HONOR_ZERO_RETURN_CODE@@@') | |
| 273 | |
| 274 @staticmethod | |
| 275 def step_clear(line): | |
| 276 return line.startswith('@@@STEP_CLEAR@@@') | |
| 277 | |
| 278 @staticmethod | |
| 279 def step_summary_clear(line): | |
| 280 return line.startswith('@@@STEP_SUMMARY_CLEAR@@@') | |
| 281 | |
| 282 @staticmethod | |
| 283 def step_text(line): | |
| 284 return Match._parse_line('^@@@STEP_TEXT@(.*)@@@', line) | |
| 285 | |
| 286 @staticmethod | |
| 287 def step_summary_text(line): | |
| 288 return Match._parse_line('^@@@STEP_SUMMARY_TEXT@(.*)@@@', line) | |
| 289 | |
| 290 @staticmethod | |
| 291 def seed_step(line): | |
| 292 return Match._parse_line('^@@@SEED_STEP (.*)@@@', line) | |
| 293 | |
| 294 @staticmethod | |
| 295 def step_cursor(line): | |
| 296 return Match._parse_line('^@@@STEP_CURSOR (.*)@@@', line) | |
| 297 | |
| 298 @staticmethod | |
| 299 def build_step(line): | |
| 300 return Match._parse_line('^@@@BUILD_STEP (.*)@@@', line) | |
| 301 | |
| 302 | |
| 303 def main(): | |
| 304 usage = '%s <command list file>' % sys.argv[0] | |
| 305 parser = optparse.OptionParser(usage=usage) | |
| 306 _, args = parser.parse_args() | |
| 307 if not args: | |
| 308 parser.error('Must specify an input filename!') | |
|
iannucci
2013/03/02 01:49:14
Check for len(args) > 1?
| |
| 309 | |
| 310 steps = [] | |
| 311 with open(args[0], 'rb') as f: | |
| 312 steps.extend(json.load(f)) | |
| 313 | |
| 314 for step in steps: | |
| 315 if ('cmd' not in step or | |
| 316 'name' not in step): | |
| 317 print 'step \'%s\' is invalid' % json.dumps(step) | |
| 318 return 1 | |
| 319 | |
| 320 # Make sure these steps always run, even if there is a build failure. | |
| 321 always_run = {} | |
| 322 for step in steps: | |
| 323 if step.get('always_run'): | |
| 324 always_run[step['name']] = step | |
| 325 | |
| 326 stepnames = [s['name'] for s in steps] | |
| 327 | |
| 328 stream = StructuredAnnotationStream(seed_steps=stepnames) | |
| 329 build_failure = False | |
| 330 for step in steps: | |
| 331 if step['name'] in always_run: | |
| 332 del always_run[step['name']] | |
| 333 try: | |
| 334 with stream.step(step['name']) as s: | |
| 335 ret = chromium_utils.RunCommand(step['cmd']) | |
| 336 if ret != 0: | |
| 337 s.step_failure() | |
| 338 build_failure = True | |
| 339 break | |
| 340 except OSError: | |
| 341 # File wasn't found, error has been already reported to stream. | |
| 342 build_failure = True | |
| 343 break | |
| 344 | |
| 345 for step_name in always_run: | |
| 346 with stream.step(step_name) as s: | |
| 347 ret = chromium_utils.RunCommand(always_run[step_name]['cmd']) | |
| 348 if ret != 0: | |
| 349 s.step_failure() | |
| 350 build_failure = True | |
| 351 | |
| 352 if build_failure: | |
| 353 return 1 | |
| 354 return 0 | |
| 355 | |
| 356 | |
| 357 if __name__ == '__main__': | |
| 358 sys.exit(main()) | |
| OLD | NEW |