Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(796)

Side by Side Diff: scripts/common/annotator.py

Issue 1151423002: Move recipe engine to third_party/recipe_engine. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Moved field_composer_test with its buddies Created 5 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | scripts/common/unittests/annotator_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 # Copyright (c) 2015 The Chromium Authors. All rights reserved.
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 2 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 3 # found in the LICENSE file.
5 4
6 """Contains generating and parsing systems of the Chromium Buildbot Annotator. 5 """Contains the parsing system of the Chromium Buildbot Annotator."""
7 6
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 calendar
17 import contextlib
18 import datetime
19 import json
20 import optparse
21 import os 7 import os
22 import subprocess
23 import sys 8 import sys
24 import threading
25 import traceback 9 import traceback
26 10
27
28 # These are maps of annotation key -> number of expected arguments. 11 # These are maps of annotation key -> number of expected arguments.
29 STEP_ANNOTATIONS = { 12 STEP_ANNOTATIONS = {
30 'SET_BUILD_PROPERTY': 2, 13 'SET_BUILD_PROPERTY': 2,
31 'STEP_CLEAR': 0, 14 'STEP_CLEAR': 0,
32 'STEP_EXCEPTION': 0, 15 'STEP_EXCEPTION': 0,
33 'STEP_FAILURE': 0, 16 'STEP_FAILURE': 0,
34 'STEP_LINK': 2, 17 'STEP_LINK': 2,
35 'STEP_LOG_END': 1, 18 'STEP_LOG_END': 1,
36 'STEP_LOG_END_PERF': 2, 19 'STEP_LOG_END_PERF': 2,
37 'STEP_LOG_LINE': 2, 20 'STEP_LOG_LINE': 2,
(...skipping 147 matching lines...) Expand 10 before | Expand all | Expand 10 after
185 168
186 logname = logname.replace('/', '/') 169 logname = logname.replace('/', '/')
187 170
188 for line in lines: 171 for line in lines:
189 self.step_log_line(logname, line) 172 self.step_log_line(logname, line)
190 if perf: 173 if perf:
191 self.step_log_end_perf(logname, perf) 174 self.step_log_end_perf(logname, perf)
192 else: 175 else:
193 self.step_log_end(logname) 176 self.step_log_end(logname)
194 177
178
195 class StepControlCommands(AnnotationPrinter): 179 class StepControlCommands(AnnotationPrinter):
196 """Subclass holding step control commands. Intended to be subclassed. 180 """Subclass holding step control commands. Intended to be subclassed.
197 181
198 This is subclassed out so callers in StructuredAnnotationStep can't call 182 This is subclassed out so callers in StructuredAnnotationStep can't call
199 step_started() or step_closed(). 183 step_started() or step_closed().
200 """ 184 """
201 ANNOTATIONS = CONTROL_ANNOTATIONS 185 ANNOTATIONS = CONTROL_ANNOTATIONS
202 186
203 187
204 class StructuredAnnotationStep(StepCommands): 188 class StructuredAnnotationStep(StepCommands, StepControlCommands):
205 """Helper class to provide context for a step.""" 189 """Helper class to provide context for a step."""
206 190
207 def __init__(self, annotation_stream, *args, **kwargs): 191 def __init__(self, annotation_stream, *args, **kwargs):
208 self.annotation_stream = annotation_stream 192 self.annotation_stream = annotation_stream
209 super(StructuredAnnotationStep, self).__init__(*args, **kwargs) 193 super(StructuredAnnotationStep, self).__init__(*args, **kwargs)
210 self.control = StepControlCommands(self.stream, self.flush_before) 194 self.control = StepControlCommands(self.stream, self.flush_before)
211 self.emitted_logs = set() 195 self.emitted_logs = set()
212 196
213 197
214 def __enter__(self): 198 def __enter__(self):
(...skipping 19 matching lines...) Expand all
234 self.write_log_lines('exception', filter(None, trace_lines)) 218 self.write_log_lines('exception', filter(None, trace_lines))
235 self.step_exception() 219 self.step_exception()
236 220
237 def step_ended(self): 221 def step_ended(self):
238 self.annotation_stream.step_cursor(self.annotation_stream.current_step) 222 self.annotation_stream.step_cursor(self.annotation_stream.current_step)
239 self.control.step_closed() 223 self.control.step_closed()
240 self.annotation_stream.current_step = '' 224 self.annotation_stream.current_step = ''
241 225
242 return True 226 return True
243 227
244 class AdvancedAnnotationStep(StepCommands, StepControlCommands):
245 """Holds additional step functions for finer step control.
246 228
247 Most users will want to use StructuredAnnotationSteps generated from a 229 class StructuredAnnotationStream(AnnotationPrinter):
248 StructuredAnnotationStream as these handle state automatically.
249 """
250
251 def __init__(self, *args, **kwargs):
252 super(AdvancedAnnotationStep, self).__init__(*args, **kwargs)
253
254
255 class AdvancedAnnotationStream(AnnotationPrinter):
256 """Holds individual annotation generating functions for streams.
257
258 Most callers should use StructuredAnnotationStream to simplify coding and
259 avoid errors. For the rare cases where StructuredAnnotationStream is
260 insufficient (parallel step execution), the individual functions are exposed
261 here.
262 """
263 ANNOTATIONS = STREAM_ANNOTATIONS
264
265
266 class StructuredAnnotationStream(AdvancedAnnotationStream):
267 """Provides an interface to handle an annotated build. 230 """Provides an interface to handle an annotated build.
268 231
269 StructuredAnnotationStream handles most of the step setup and closure calls 232 StructuredAnnotationStream handles most of the step setup and closure calls
270 for you. All you have to do is execute your code within the steps and set any 233 for you. All you have to do is execute your code within the steps and set any
271 failures or warnings that come up. You may optionally provide a list of steps 234 failures or warnings that come up. You may optionally provide a list of steps
272 to seed before execution. 235 to seed before execution.
273 236
274 Usage: 237 Usage:
275 238
276 stream = StructuredAnnotationStream() 239 stream = StructuredAnnotationStream()
277 with stream.step('compile') as s: 240 with stream.step('compile') as s:
278 # do something 241 # do something
279 if error: 242 if error:
280 s.step_failure() 243 s.step_failure()
281 with stream.step('test') as s: 244 with stream.step('test') as s:
282 # do something 245 # do something
283 if warnings: 246 if warnings:
284 s.step_warnings() 247 s.step_warnings()
285 """ 248 """
249 ANNOTATIONS = STREAM_ANNOTATIONS
286 250
287 def __init__(self, stream=sys.stdout, 251 def __init__(self, stream=sys.stdout,
288 flush_before=sys.stderr, 252 flush_before=sys.stderr,
289 seed_steps=None): # pylint: disable=W0613 253 seed_steps=None): # pylint: disable=W0613
290 super(StructuredAnnotationStream, self).__init__(stream=stream, 254 super(StructuredAnnotationStream, self).__init__(stream=stream,
291 flush_before=flush_before) 255 flush_before=flush_before)
292 self.current_step = '' 256 self.current_step = ''
293 257
294 def step(self, name): 258 def step(self, name):
295 """Provide a context with which to execute a step.""" 259 """Provide a context with which to execute a step."""
(...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after
350 args = [] 314 args = []
351 315
352 fn = getattr(callback_implementor, cmd, None) 316 fn = getattr(callback_implementor, cmd, None)
353 if fn is None: 317 if fn is None:
354 raise Exception('"%s" does not implement "%s"' 318 raise Exception('"%s" does not implement "%s"'
355 % (callback_implementor, cmd)) 319 % (callback_implementor, cmd))
356 320
357 fn(*args) 321 fn(*args)
358 322
359 323
360 def _merge_envs(original, override):
361 """Merges two environments.
362
363 Returns a new environment dict with entries from |override| overwriting
364 corresponding entries in |original|. Keys whose value is None will completely
365 remove the environment variable. Values can contain %(KEY)s strings, which
366 will be substituted with the values from the original (useful for amending, as
367 opposed to overwriting, variables like PATH).
368 """
369 result = original.copy()
370 if not override:
371 return result
372 for k, v in override.items():
373 if v is None:
374 if k in result:
375 del result[k]
376 else:
377 result[str(k)] = str(v) % original
378 return result
379
380
381 def _validate_step(step):
382 """Validates parameters of the step.
383 Returns None if it's OK, error message if not.
384 """
385 for req in ['cmd', 'name']:
386 if req not in step:
387 return 'missing \'%s\' parameter' % (req,)
388 if 'cwd' in step and not os.path.isabs(step['cwd']):
389 return '\'cwd\' should be an absolute path'
390 return None
391
392
393 def print_step(step, env, stream): 324 def print_step(step, env, stream):
394 """Prints the step command and relevant metadata. 325 """Prints the step command and relevant metadata.
395 326
396 Intended to be similar to the information that Buildbot prints at the 327 Intended to be similar to the information that Buildbot prints at the
397 beginning of each non-annotator step. 328 beginning of each non-annotator step.
398 """ 329 """
399 step_info_lines = [] 330 step_info_lines = []
400 step_info_lines.append(' '.join(step['cmd'])) 331 step_info_lines.append(' '.join(step['cmd']))
401 step_info_lines.append('in dir %s:' % (step['cwd'] or os.getcwd())) 332 step_info_lines.append('in dir %s:' % (step['cwd'] or os.getcwd()))
402 for key, value in sorted(step.items()): 333 for key, value in sorted(step.items()):
403 if value is not None: 334 if value is not None:
404 if callable(value): 335 if callable(value):
405 # This prevents functions from showing up as: 336 # This prevents functions from showing up as:
406 # '<function foo at 0x7f523ec7a410>' 337 # '<function foo at 0x7f523ec7a410>'
407 # which is tricky to test. 338 # which is tricky to test.
408 value = value.__name__+'(...)' 339 value = value.__name__+'(...)'
409 step_info_lines.append(' %s: %s' % (key, value)) 340 step_info_lines.append(' %s: %s' % (key, value))
410 step_info_lines.append('full environment:') 341 step_info_lines.append('full environment:')
411 for key, value in sorted(env.items()): 342 for key, value in sorted(env.items()):
412 step_info_lines.append(' %s: %s' % (key, value)) 343 step_info_lines.append(' %s: %s' % (key, value))
413 step_info_lines.append('') 344 step_info_lines.append('')
414 stream.emit('\n'.join(step_info_lines)) 345 stream.emit('\n'.join(step_info_lines))
415
416
417 @contextlib.contextmanager
418 def modify_lookup_path(path):
419 """Places the specified path into os.environ.
420
421 Necessary because subprocess.Popen uses os.environ to perform lookup on the
422 supplied command, and only uses the |env| kwarg for modifying the environment
423 of the child process.
424 """
425 saved_path = os.environ['PATH']
426 try:
427 if path is not None:
428 os.environ['PATH'] = path
429 yield
430 finally:
431 os.environ['PATH'] = saved_path
432
433
434 def normalizeChange(change):
435 assert isinstance(change, dict), 'Change is not a dict'
436 change = change.copy()
437
438 # Convert when_timestamp to UNIX timestamp.
439 when = change.get('when_timestamp')
440 if isinstance(when, datetime.datetime):
441 when = calendar.timegm(when.utctimetuple())
442 change['when_timestamp'] = when
443
444 return change
445
446
447 def triggerBuilds(step, trigger_specs):
448 assert trigger_specs is not None
449 for trig in trigger_specs:
450 builder_name = trig.get('builder_name')
451 if not builder_name:
452 raise ValueError('Trigger spec: builder_name is not set')
453
454 changes = trig.get('buildbot_changes', [])
455 assert isinstance(changes, list), 'buildbot_changes must be a list'
456 changes = map(normalizeChange, changes)
457
458 step.step_trigger(json.dumps({
459 'builderNames': [builder_name],
460 'bucket': trig.get('bucket'),
461 'changes': changes,
462 'properties': trig.get('properties'),
463 }, sort_keys=True))
464
465
466 def run_step(stream, name, cmd,
467 cwd=None, env=None,
468 allow_subannotations=False,
469 trigger_specs=None,
470 **kwargs):
471 """Runs a single step.
472
473 Context:
474 stream: StructuredAnnotationStream to use to emit step
475
476 Step parameters:
477 name: name of the step, will appear in buildbots waterfall
478 cmd: command to run, list of one or more strings
479 cwd: absolute path to working directory for the command
480 env: dict with overrides for environment variables
481 allow_subannotations: if True, lets the step emit its own annotations
482 trigger_specs: a list of trigger specifications, which are dict with keys:
483 properties: a dict of properties.
484 Buildbot requires buildername property.
485
486 Known kwargs:
487 stdout: Path to a file to put step stdout into. If used, stdout won't appear
488 in annotator's stdout (and |allow_subannotations| is ignored).
489 stderr: Path to a file to put step stderr into. If used, stderr won't appear
490 in annotator's stderr.
491 stdin: Path to a file to read step stdin from.
492
493 Returns the returncode of the step.
494 """
495 if isinstance(cmd, basestring):
496 cmd = (cmd,)
497 cmd = map(str, cmd)
498
499 # For error reporting.
500 step_dict = kwargs.copy()
501 step_dict.update({
502 'name': name,
503 'cmd': cmd,
504 'cwd': cwd,
505 'env': env,
506 'allow_subannotations': allow_subannotations,
507 })
508 step_env = _merge_envs(os.environ, env)
509
510 step_annotation = stream.step(name)
511 step_annotation.step_started()
512
513 print_step(step_dict, step_env, stream)
514 returncode = 0
515 if cmd:
516 try:
517 # Open file handles for IO redirection based on file names in step_dict.
518 fhandles = {
519 'stdout': subprocess.PIPE,
520 'stderr': subprocess.PIPE,
521 'stdin': None,
522 }
523 for key in fhandles:
524 if key in step_dict:
525 fhandles[key] = open(step_dict[key],
526 'rb' if key == 'stdin' else 'wb')
527
528 if sys.platform.startswith('win'):
529 # Windows has a bad habit of opening a dialog when a console program
530 # crashes, rather than just letting it crash. Therefore, when a program
531 # crashes on Windows, we don't find out until the build step times out.
532 # This code prevents the dialog from appearing, so that we find out
533 # immediately and don't waste time waiting for a user to close the
534 # dialog.
535 import ctypes
536 # SetErrorMode(SEM_NOGPFAULTERRORBOX). For more information, see:
537 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms680621.aspx
538 ctypes.windll.kernel32.SetErrorMode(0x0002)
539 # CREATE_NO_WINDOW. For more information, see:
540 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
541 creationflags = 0x8000000
542 else:
543 creationflags = 0
544
545 with modify_lookup_path(step_env.get('PATH')):
546 proc = subprocess.Popen(
547 cmd,
548 env=step_env,
549 cwd=cwd,
550 universal_newlines=True,
551 creationflags=creationflags,
552 **fhandles)
553
554 # Safe to close file handles now that subprocess has inherited them.
555 for handle in fhandles.itervalues():
556 if isinstance(handle, file):
557 handle.close()
558
559 outlock = threading.Lock()
560 def filter_lines(lock, allow_subannotations, inhandle, outhandle):
561 while True:
562 line = inhandle.readline()
563 if not line:
564 break
565 lock.acquire()
566 try:
567 if not allow_subannotations and line.startswith('@@@'):
568 outhandle.write('!')
569 outhandle.write(line)
570 outhandle.flush()
571 finally:
572 lock.release()
573
574 # Pump piped stdio through filter_lines. IO going to files on disk is
575 # not filtered.
576 threads = []
577 for key in ('stdout', 'stderr'):
578 if fhandles[key] == subprocess.PIPE:
579 inhandle = getattr(proc, key)
580 outhandle = getattr(sys, key)
581 threads.append(threading.Thread(
582 target=filter_lines,
583 args=(outlock, allow_subannotations, inhandle, outhandle)))
584
585 for th in threads:
586 th.start()
587 proc.wait()
588 for th in threads:
589 th.join()
590 returncode = proc.returncode
591 except OSError:
592 # File wasn't found, error will be reported to stream when the exception
593 # crosses the context manager.
594 step_annotation.step_exception_occured(*sys.exc_info())
595 raise
596
597 # TODO(martiniss) move logic into own module?
598 if trigger_specs:
599 triggerBuilds(step_annotation, trigger_specs)
600
601 return step_annotation, returncode
602
603 def update_build_failure(failure, retcode, **_kwargs):
604 """Potentially moves failure from False to True, depending on returncode of
605 the run step and the step's configuration.
606
607 can_fail_build: A boolean indicating that a bad retcode for this step should
608 be intepreted as a build failure.
609
610 Returns new value for failure.
611
612 Called externally from annotated_run, which is why it's a separate function.
613 """
614 # TODO(iannucci): Allow step to specify "OK" return values besides 0?
615 return failure or retcode
616
617 def run_steps(steps, build_failure):
618 for step in steps:
619 error = _validate_step(step)
620 if error:
621 print 'Invalid step - %s\n%s' % (error, json.dumps(step, indent=2))
622 sys.exit(1)
623
624 stream = StructuredAnnotationStream()
625 ret_codes = []
626 build_failure = False
627 prev_annotation = None
628 for step in steps:
629 if build_failure and not step.get('always_run', False):
630 ret = None
631 else:
632 prev_annotation, ret = run_step(stream, **step)
633 stream = prev_annotation.annotation_stream
634 if ret > 0:
635 stream.step_cursor(stream.current_step)
636 stream.emit('step returned non-zero exit code: %d' % ret)
637 prev_annotation.step_failure()
638
639 prev_annotation.step_ended()
640 build_failure = update_build_failure(build_failure, ret)
641 ret_codes.append(ret)
642 if prev_annotation:
643 prev_annotation.step_ended()
644 return build_failure, ret_codes
645
646
647 def main():
648 usage = '%s <command list file or - for stdin>' % sys.argv[0]
649 parser = optparse.OptionParser(usage=usage)
650 _, args = parser.parse_args()
651 if not args:
652 parser.error('Must specify an input filename.')
653 if len(args) > 1:
654 parser.error('Too many arguments specified.')
655
656 steps = []
657
658 def force_list_str(lst):
659 ret = []
660 for v in lst:
661 if isinstance(v, basestring):
662 v = str(v)
663 elif isinstance(v, list):
664 v = force_list_str(v)
665 elif isinstance(v, dict):
666 v = force_dict_strs(v)
667 ret.append(v)
668 return ret
669
670 def force_dict_strs(obj):
671 ret = {}
672 for k, v in obj.iteritems():
673 if isinstance(v, basestring):
674 v = str(v)
675 elif isinstance(v, list):
676 v = force_list_str(v)
677 elif isinstance(v, dict):
678 v = force_dict_strs(v)
679 ret[str(k)] = v
680 return ret
681
682 if args[0] == '-':
683 steps.extend(json.load(sys.stdin, object_hook=force_dict_strs))
684 else:
685 with open(args[0], 'rb') as f:
686 steps.extend(json.load(f, object_hook=force_dict_strs))
687
688 return 1 if run_steps(steps, False)[0] else 0
689
690
691 if __name__ == '__main__':
692 sys.exit(main())
OLDNEW
« no previous file with comments | « no previous file | scripts/common/unittests/annotator_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698