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