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