| OLD | NEW |
| (Empty) |
| 1 # Copyright (c) 2015 The Chromium Authors. All rights reserved. | |
| 2 # Use of this source code is governed by a BSD-style license that can be | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 """Contains the parsing system of the Chromium Buildbot Annotator.""" | |
| 6 | |
| 7 import os | |
| 8 import sys | |
| 9 import traceback | |
| 10 | |
| 11 # These are maps of annotation key -> number of expected arguments. | |
| 12 STEP_ANNOTATIONS = { | |
| 13 'SET_BUILD_PROPERTY': 2, | |
| 14 'STEP_CLEAR': 0, | |
| 15 'STEP_EXCEPTION': 0, | |
| 16 'STEP_FAILURE': 0, | |
| 17 'STEP_LINK': 2, | |
| 18 'STEP_LOG_END': 1, | |
| 19 'STEP_LOG_END_PERF': 2, | |
| 20 'STEP_LOG_LINE': 2, | |
| 21 'STEP_SUMMARY_CLEAR': 0, | |
| 22 'STEP_SUMMARY_TEXT': 1, | |
| 23 'STEP_TEXT': 1, | |
| 24 'STEP_TRIGGER': 1, | |
| 25 'STEP_WARNINGS': 0, | |
| 26 'STEP_NEST_LEVEL': 1, | |
| 27 } | |
| 28 | |
| 29 CONTROL_ANNOTATIONS = { | |
| 30 'STEP_CLOSED': 0, | |
| 31 'STEP_STARTED': 0, | |
| 32 } | |
| 33 | |
| 34 STREAM_ANNOTATIONS = { | |
| 35 'HALT_ON_FAILURE': 0, | |
| 36 'HONOR_ZERO_RETURN_CODE': 0, | |
| 37 'SEED_STEP': 1, | |
| 38 'SEED_STEP_TEXT': 2, | |
| 39 'STEP_CURSOR': 1, | |
| 40 } | |
| 41 | |
| 42 DEPRECATED_ANNOTATIONS = { | |
| 43 'BUILD_STEP': 1, | |
| 44 } | |
| 45 | |
| 46 ALL_ANNOTATIONS = {} | |
| 47 ALL_ANNOTATIONS.update(STEP_ANNOTATIONS) | |
| 48 ALL_ANNOTATIONS.update(CONTROL_ANNOTATIONS) | |
| 49 ALL_ANNOTATIONS.update(STREAM_ANNOTATIONS) | |
| 50 ALL_ANNOTATIONS.update(DEPRECATED_ANNOTATIONS) | |
| 51 | |
| 52 # This is a mapping of old_annotation_name -> new_annotation_name. | |
| 53 # Theoretically all annotator scripts should use the new names, but it's hard | |
| 54 # to tell due to the decentralized nature of the annotator. | |
| 55 DEPRECATED_ALIASES = { | |
| 56 'BUILD_FAILED': 'STEP_FAILURE', | |
| 57 'BUILD_WARNINGS': 'STEP_WARNINGS', | |
| 58 'BUILD_EXCEPTION': 'STEP_EXCEPTION', | |
| 59 'link': 'STEP_LINK', | |
| 60 } | |
| 61 | |
| 62 # A couple of the annotations have the format: | |
| 63 # @@@THING arg@@@ | |
| 64 # for reasons no one knows. We only need this case until all masters have been | |
| 65 # restarted to pick up the new master-side parsing code. | |
| 66 OLD_STYLE_ANNOTATIONS = set(( | |
| 67 'SEED_STEP', | |
| 68 'STEP_CURSOR', | |
| 69 )) | |
| 70 | |
| 71 | |
| 72 def emit(line, stream, flush_before=None): | |
| 73 if flush_before: | |
| 74 flush_before.flush() | |
| 75 print >> stream | |
| 76 # WinDOS can only handle 64kb of output to the console at a time, per process. | |
| 77 if sys.platform.startswith('win'): | |
| 78 lim = 2**15 | |
| 79 while line: | |
| 80 to_print, line = line[:lim], line[lim:] | |
| 81 stream.write(to_print) | |
| 82 stream.write('\n') | |
| 83 else: | |
| 84 print >> stream, line | |
| 85 stream.flush() | |
| 86 | |
| 87 | |
| 88 class MetaAnnotationPrinter(type): | |
| 89 def __new__(mcs, name, bases, dct): | |
| 90 annotation_map = dct.get('ANNOTATIONS') | |
| 91 if annotation_map: | |
| 92 for key, v in annotation_map.iteritems(): | |
| 93 key = key.lower() | |
| 94 dct[key] = mcs.make_printer_fn(key, v) | |
| 95 return type.__new__(mcs, name, bases, dct) | |
| 96 | |
| 97 @staticmethod | |
| 98 def make_printer_fn(name, n_args): | |
| 99 """Generates a method which emits an annotation to the log stream.""" | |
| 100 upname = name.upper() | |
| 101 if upname in OLD_STYLE_ANNOTATIONS: | |
| 102 assert n_args >= 1 | |
| 103 fmt = '@@@%s %%s%s@@@' % (upname, '@%s' * (n_args - 1)) | |
| 104 else: | |
| 105 fmt = '@@@%s%s@@@' % (upname, '@%s' * n_args) | |
| 106 | |
| 107 inner_args = n_args + 1 # self counts | |
| 108 infix = '1 argument' if inner_args == 1 else ('%d arguments' % inner_args) | |
| 109 err = '%s() takes %s (%%d given)' % (name, infix) | |
| 110 | |
| 111 def printer(self, *args): | |
| 112 if len(args) != n_args: | |
| 113 raise TypeError(err % (len(args) + 1)) | |
| 114 self.emit(fmt % args) | |
| 115 printer.__name__ = name | |
| 116 printer.__doc__ = """Emits an annotation for %s.""" % name.upper() | |
| 117 | |
| 118 return printer | |
| 119 | |
| 120 | |
| 121 class AnnotationPrinter(object): | |
| 122 """A derivable class which will inject annotation-printing methods into the | |
| 123 subclass. | |
| 124 | |
| 125 A subclass should define a class variable ANNOTATIONS equal to a | |
| 126 dictionary of the form { '<ANNOTATION_NAME>': <# args> }. This class will | |
| 127 then inject methods whose names are the undercased version of your | |
| 128 annotation names, and which take the number of arguments specified in the | |
| 129 dictionary. | |
| 130 | |
| 131 Example: | |
| 132 >>> my_annotations = { 'STEP_LOG_LINE': 2 } | |
| 133 >>> class MyObj(AnnotationPrinter): | |
| 134 ... ANNOTATIONS = my_annotations | |
| 135 ... | |
| 136 >>> o = MyObj() | |
| 137 >>> o.step_log_line('logname', 'here is a line to put in the log') | |
| 138 @@@STEP_LOG_LINE@logname@here is a line to put in the log@@@ | |
| 139 >>> o.step_log_line() | |
| 140 Traceback (most recent call last): | |
| 141 File "<stdin>", line 1, in <module> | |
| 142 TypeError: step_log_line() takes exactly 3 arguments (1 given) | |
| 143 >>> o.setp_log_line.__doc__ | |
| 144 "Emits an annotation for STEP_LOG_LINE." | |
| 145 >>> | |
| 146 """ | |
| 147 __metaclass__ = MetaAnnotationPrinter | |
| 148 | |
| 149 def __init__(self, stream, flush_before): | |
| 150 self.stream = stream | |
| 151 self.flush_before = flush_before | |
| 152 | |
| 153 def emit(self, line): | |
| 154 emit(line, self.stream, self.flush_before) | |
| 155 | |
| 156 | |
| 157 class StepCommands(AnnotationPrinter): | |
| 158 """Class holding step commands. Intended to be subclassed.""" | |
| 159 ANNOTATIONS = STEP_ANNOTATIONS | |
| 160 | |
| 161 def __init__(self, stream, flush_before): | |
| 162 super(StepCommands, self).__init__(stream, flush_before) | |
| 163 self.emitted_logs = set() | |
| 164 | |
| 165 def write_log_lines(self, logname, lines, perf=None): | |
| 166 if logname in self.emitted_logs: | |
| 167 raise ValueError('Log %s has been emitted multiple times.' % logname) | |
| 168 self.emitted_logs.add(logname) | |
| 169 | |
| 170 logname = logname.replace('/', '/') | |
| 171 | |
| 172 for line in lines: | |
| 173 for actual_line in line.split('\n'): | |
| 174 self.step_log_line(logname, actual_line) | |
| 175 | |
| 176 if perf: | |
| 177 self.step_log_end_perf(logname, perf) | |
| 178 else: | |
| 179 self.step_log_end(logname) | |
| 180 | |
| 181 | |
| 182 class StepControlCommands(AnnotationPrinter): | |
| 183 """Subclass holding step control commands. Intended to be subclassed. | |
| 184 | |
| 185 This is subclassed out so callers in StructuredAnnotationStep can't call | |
| 186 step_started() or step_closed(). | |
| 187 """ | |
| 188 ANNOTATIONS = CONTROL_ANNOTATIONS | |
| 189 | |
| 190 | |
| 191 class StructuredAnnotationStep(StepCommands, StepControlCommands): | |
| 192 """Helper class to provide context for a step.""" | |
| 193 | |
| 194 def __init__(self, annotation_stream, *args, **kwargs): | |
| 195 self.annotation_stream = annotation_stream | |
| 196 super(StructuredAnnotationStep, self).__init__(*args, **kwargs) | |
| 197 self.control = StepControlCommands(self.stream, self.flush_before) | |
| 198 self.emitted_logs = set() | |
| 199 | |
| 200 | |
| 201 def __enter__(self): | |
| 202 return self.step_started() | |
| 203 | |
| 204 def step_started(self): | |
| 205 self.control.step_started() | |
| 206 return self | |
| 207 | |
| 208 def __exit__(self, exc_type, exc_value, tb): | |
| 209 self.annotation_stream.step_cursor(self.annotation_stream.current_step) | |
| 210 #TODO(martinis) combine this and step_ended | |
| 211 if exc_type: | |
| 212 self.step_exception_occured(exc_type, exc_value, tb) | |
| 213 | |
| 214 self.control.step_closed() | |
| 215 self.annotation_stream.current_step = '' | |
| 216 return not exc_type | |
| 217 | |
| 218 def step_exception_occured(self, exc_type, exc_value, tb): | |
| 219 trace = traceback.format_exception(exc_type, exc_value, tb) | |
| 220 trace_lines = ''.join(trace).split('\n') | |
| 221 self.write_log_lines('exception', filter(None, trace_lines)) | |
| 222 self.step_exception() | |
| 223 | |
| 224 def step_ended(self): | |
| 225 self.annotation_stream.step_cursor(self.annotation_stream.current_step) | |
| 226 self.control.step_closed() | |
| 227 self.annotation_stream.current_step = '' | |
| 228 | |
| 229 return True | |
| 230 | |
| 231 | |
| 232 class StructuredAnnotationStream(AnnotationPrinter): | |
| 233 """Provides an interface to handle an annotated build. | |
| 234 | |
| 235 StructuredAnnotationStream handles most of the step setup and closure calls | |
| 236 for you. All you have to do is execute your code within the steps and set any | |
| 237 failures or warnings that come up. You may optionally provide a list of steps | |
| 238 to seed before execution. | |
| 239 | |
| 240 Usage: | |
| 241 | |
| 242 stream = StructuredAnnotationStream() | |
| 243 with stream.step('compile') as s: | |
| 244 # do something | |
| 245 if error: | |
| 246 s.step_failure() | |
| 247 with stream.step('test') as s: | |
| 248 # do something | |
| 249 if warnings: | |
| 250 s.step_warnings() | |
| 251 """ | |
| 252 ANNOTATIONS = STREAM_ANNOTATIONS | |
| 253 | |
| 254 def __init__(self, stream=sys.stdout, | |
| 255 flush_before=sys.stderr, | |
| 256 seed_steps=None): # pylint: disable=W0613 | |
| 257 super(StructuredAnnotationStream, self).__init__(stream=stream, | |
| 258 flush_before=flush_before) | |
| 259 self.current_step = '' | |
| 260 | |
| 261 def step(self, name): | |
| 262 """Provide a context with which to execute a step.""" | |
| 263 if self.current_step: | |
| 264 raise Exception('Can\'t start step %s while in step %s.' % ( | |
| 265 name, self.current_step)) | |
| 266 | |
| 267 self.seed_step(name) | |
| 268 self.step_cursor(name) | |
| 269 self.current_step = name | |
| 270 return StructuredAnnotationStep(self, stream=self.stream, | |
| 271 flush_before=self.flush_before) | |
| 272 | |
| 273 | |
| 274 def MatchAnnotation(line, callback_implementor): | |
| 275 """Call back into |callback_implementor| if line contains an annotation. | |
| 276 | |
| 277 Args: | |
| 278 line (str) - The line to analyze | |
| 279 callback_implementor (object) - An object which contains methods | |
| 280 corresponding to all of the annotations in the |ALL_ANNOTATIONS| | |
| 281 dictionary. For example, it should contain a method STEP_SUMMARY_TEXT | |
| 282 taking a single argument. | |
| 283 | |
| 284 Parsing method: | |
| 285 * if line doesn't match /^@@@.*@@@$/, return without calling back | |
| 286 * Look for the first '@' or ' ' | |
| 287 """ | |
| 288 if not (line.startswith('@@@') and line.endswith('@@@') and len(line) > 6): | |
| 289 return | |
| 290 line = line[3:-3] | |
| 291 | |
| 292 # look until the first @ or ' ' | |
| 293 idx = min((x for x in (line.find('@'), line.find(' '), len(line)) if x > 0)) | |
| 294 cmd_text = line[:idx] | |
| 295 cmd = DEPRECATED_ALIASES.get(cmd_text, cmd_text) | |
| 296 | |
| 297 field_count = ALL_ANNOTATIONS.get(cmd) | |
| 298 if field_count is None: | |
| 299 raise Exception('Unrecognized annotator command "%s"' % cmd_text) | |
| 300 | |
| 301 if field_count: | |
| 302 if idx == len(line): | |
| 303 raise Exception('Annotator command "%s" expects %d args, got 0.' | |
| 304 % (cmd_text, field_count)) | |
| 305 | |
| 306 line = line[idx+1:] | |
| 307 | |
| 308 args = line.split('@', field_count-1) | |
| 309 if len(args) != field_count: | |
| 310 raise Exception('Annotator command "%s" expects %d args, got %d.' | |
| 311 % (cmd_text, field_count, len(args))) | |
| 312 else: | |
| 313 line = line[len(cmd_text):] | |
| 314 if line: | |
| 315 raise Exception('Annotator command "%s" expects no args, got cruft "%s".' | |
| 316 % (cmd_text, line)) | |
| 317 args = [] | |
| 318 | |
| 319 fn = getattr(callback_implementor, cmd, None) | |
| 320 if fn is None: | |
| 321 raise Exception('"%s" does not implement "%s"' | |
| 322 % (callback_implementor, cmd)) | |
| 323 | |
| 324 fn(*args) | |
| 325 | |
| 326 | |
| 327 def print_step(step, env, stream): | |
| 328 """Prints the step command and relevant metadata. | |
| 329 | |
| 330 Intended to be similar to the information that Buildbot prints at the | |
| 331 beginning of each non-annotator step. | |
| 332 """ | |
| 333 step_info_lines = [] | |
| 334 step_info_lines.append(' '.join(step['cmd'])) | |
| 335 step_info_lines.append('in dir %s:' % (step['cwd'] or os.getcwd())) | |
| 336 for key, value in sorted(step.items()): | |
| 337 if value is not None: | |
| 338 if callable(value): | |
| 339 # This prevents functions from showing up as: | |
| 340 # '<function foo at 0x7f523ec7a410>' | |
| 341 # which is tricky to test. | |
| 342 value = value.__name__+'(...)' | |
| 343 step_info_lines.append(' %s: %s' % (key, value)) | |
| 344 step_info_lines.append('full environment:') | |
| 345 for key, value in sorted(env.items()): | |
| 346 step_info_lines.append(' %s: %s' % (key, value)) | |
| 347 step_info_lines.append('') | |
| 348 stream.emit('\n'.join(step_info_lines)) | |
| OLD | NEW |