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

Side by Side Diff: third_party/recipe_engine/main.py

Issue 1241323004: Cross-repo recipe package system. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Roll to latest recipes-py Created 5 years, 3 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 | « third_party/recipe_engine/loader.py ('k') | third_party/recipe_engine/recipe_api.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright (c) 2013-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 """Entry point for fully-annotated builds.
6
7 This script is part of the effort to move all builds to annotator-based
8 systems. Any builder configured to use the AnnotatorFactory.BaseFactory()
9 found in scripts/master/factory/annotator_factory.py executes a single
10 AddAnnotatedScript step. That step (found in annotator_commands.py) calls
11 this script with the build- and factory-properties passed on the command
12 line.
13
14 The main mode of operation is for factory_properties to contain a single
15 property 'recipe' whose value is the basename (without extension) of a python
16 script in one of the following locations (looked up in this order):
17 * build_internal/scripts/slave-internal/recipes
18 * build_internal/scripts/slave/recipes
19 * build/scripts/slave/recipes
20
21 For example, these factory_properties would run the 'run_presubmit' recipe
22 located in build/scripts/slave/recipes:
23 { 'recipe': 'run_presubmit' }
24
25 TODO(vadimsh, iannucci, luqui): The following docs are very outdated.
26
27 Annotated_run.py will then import the recipe and expect to call a function whose
28 signature is:
29 RunSteps(api, properties) -> None.
30
31 properties is a merged view of factory_properties with build_properties.
32
33 Items in iterable_of_things must be one of:
34 * A step dictionary (as accepted by annotator.py)
35 * A sequence of step dictionaries
36 * A step generator
37 Iterable_of_things is also permitted to be a raw step generator.
38
39 A step generator is called with the following protocol:
40 * The generator is initialized with 'step_history' and 'failed'.
41 * Each iteration of the generator is passed the current value of 'failed'.
42
43 On each iteration, a step generator may yield:
44 * A single step dictionary
45 * A sequence of step dictionaries
46 * If a sequence of dictionaries is yielded, and the first step dictionary
47 does not have a 'seed_steps' key, the first step will be augmented with
48 a 'seed_steps' key containing the names of all the steps in the sequence.
49
50 For steps yielded by the generator, if annotated_run enters the failed state,
51 it will only continue to call the generator if the generator sets the
52 'keep_going' key on the steps which it has produced. Otherwise annotated_run
53 will cease calling the generator and move on to the next item in
54 iterable_of_things.
55
56 'step_history' is an OrderedDict of {stepname -> StepData}, always representing
57 the current history of what steps have run, what they returned, and any
58 json data they emitted. Additionally, the OrderedDict has the following
59 convenience functions defined:
60 * last_step - Returns the last step that ran or None
61 * nth_step(n) - Returns the N'th step that ran or None
62
63 'failed' is a boolean representing if the build is in a 'failed' state.
64 """
65
66 import collections
67 import contextlib
68 import copy
69 import functools
70 import json
71 import os
72 import subprocess
73 import sys
74 import threading
75 import traceback
76
77 import cStringIO
78
79
80 from . import loader
81 from . import recipe_api
82 from . import recipe_test_api
83 from . import util
84
85
86 SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
87
88 BUILDBOT_MAGIC_ENV = set([
89 'BUILDBOT_BLAMELIST',
90 'BUILDBOT_BRANCH',
91 'BUILDBOT_BUILDBOTURL',
92 'BUILDBOT_BUILDERNAME',
93 'BUILDBOT_BUILDNUMBER',
94 'BUILDBOT_CLOBBER',
95 'BUILDBOT_GOT_REVISION',
96 'BUILDBOT_MASTERNAME',
97 'BUILDBOT_REVISION',
98 'BUILDBOT_SCHEDULER',
99 'BUILDBOT_SLAVENAME',
100 ])
101
102 ENV_WHITELIST_WIN = BUILDBOT_MAGIC_ENV | set([
103 'APPDATA',
104 'AWS_CREDENTIAL_FILE',
105 'BOTO_CONFIG',
106 'BUILDBOT_ARCHIVE_FORCE_SSH',
107 'CHROME_HEADLESS',
108 'CHROMIUM_BUILD',
109 'COMMONPROGRAMFILES',
110 'COMMONPROGRAMFILES(X86)',
111 'COMMONPROGRAMW6432',
112 'COMSPEC',
113 'COMPUTERNAME',
114 'DBUS_SESSION_BUS_ADDRESS',
115 'DEPOT_TOOLS_GIT_BLEEDING',
116 # TODO(maruel): Remove once everyone is on 2.7.5.
117 'DEPOT_TOOLS_PYTHON_275',
118 'DXSDK_DIR',
119 'GIT_USER_AGENT',
120 'HOME',
121 'HOMEDRIVE',
122 'HOMEPATH',
123 'LOCALAPPDATA',
124 'NUMBER_OF_PROCESSORS',
125 'OS',
126 'PATH',
127 'PATHEXT',
128 'PROCESSOR_ARCHITECTURE',
129 'PROCESSOR_ARCHITEW6432',
130 'PROCESSOR_IDENTIFIER',
131 'PROGRAMFILES',
132 'PROGRAMW6432',
133 'PWD',
134 'PYTHONPATH',
135 'SYSTEMDRIVE',
136 'SYSTEMROOT',
137 'TEMP',
138 'TESTING_MASTER',
139 'TESTING_MASTER_HOST',
140 'TESTING_SLAVENAME',
141 'TMP',
142 'USERNAME',
143 'USERDOMAIN',
144 'USERPROFILE',
145 'VS100COMNTOOLS',
146 'VS110COMNTOOLS',
147 'WINDIR',
148 ])
149
150 ENV_WHITELIST_POSIX = BUILDBOT_MAGIC_ENV | set([
151 'AWS_CREDENTIAL_FILE',
152 'BOTO_CONFIG',
153 'CCACHE_DIR',
154 'CHROME_ALLOCATOR',
155 'CHROME_HEADLESS',
156 'CHROME_VALGRIND_NUMCPUS',
157 'DISPLAY',
158 'DISTCC_DIR',
159 'GIT_USER_AGENT',
160 'HOME',
161 'HOSTNAME',
162 'HTTP_PROXY',
163 'http_proxy',
164 'HTTPS_PROXY',
165 'LANG',
166 'LOGNAME',
167 'PAGER',
168 'PATH',
169 'PWD',
170 'PYTHONPATH',
171 'SHELL',
172 'SSH_AGENT_PID',
173 'SSH_AUTH_SOCK',
174 'SSH_CLIENT',
175 'SSH_CONNECTION',
176 'SSH_TTY',
177 'TESTING_MASTER',
178 'TESTING_MASTER_HOST',
179 'TESTING_SLAVENAME',
180 'USER',
181 'USERNAME',
182 ])
183
184
185 def _isolate_environment():
186 """Isolate the environment to a known subset set."""
187 if sys.platform.startswith('win'):
188 whitelist = ENV_WHITELIST_WIN
189 elif sys.platform in ('darwin', 'posix', 'linux2'):
190 whitelist = ENV_WHITELIST_POSIX
191 else:
192 print ('WARNING: unknown platform %s, not isolating environment.' %
193 sys.platform)
194 return
195
196 for k in os.environ.keys():
197 if k not in whitelist:
198 del os.environ[k]
199
200
201 class StepPresentation(object):
202 STATUSES = set(('SUCCESS', 'FAILURE', 'WARNING', 'EXCEPTION'))
203
204 def __init__(self):
205 self._finalized = False
206
207 self._logs = collections.OrderedDict()
208 self._links = collections.OrderedDict()
209 self._perf_logs = collections.OrderedDict()
210 self._status = None
211 self._step_summary_text = ''
212 self._step_text = ''
213 self._properties = {}
214
215 # (E0202) pylint bug: http://www.logilab.org/ticket/89092
216 @property
217 def status(self): # pylint: disable=E0202
218 return self._status
219
220 @status.setter
221 def status(self, val): # pylint: disable=E0202
222 assert not self._finalized
223 assert val in self.STATUSES
224 self._status = val
225
226 @property
227 def step_text(self):
228 return self._step_text
229
230 @step_text.setter
231 def step_text(self, val):
232 assert not self._finalized
233 self._step_text = val
234
235 @property
236 def step_summary_text(self):
237 return self._step_summary_text
238
239 @step_summary_text.setter
240 def step_summary_text(self, val):
241 assert not self._finalized
242 self._step_summary_text = val
243
244 @property
245 def logs(self):
246 if not self._finalized:
247 return self._logs
248 else:
249 return copy.deepcopy(self._logs)
250
251 @property
252 def links(self):
253 if not self._finalized:
254 return self._links
255 else:
256 return copy.deepcopy(self._links)
257
258 @property
259 def perf_logs(self):
260 if not self._finalized:
261 return self._perf_logs
262 else:
263 return copy.deepcopy(self._perf_logs)
264
265 @property
266 def properties(self): # pylint: disable=E0202
267 if not self._finalized:
268 return self._properties
269 else:
270 return copy.deepcopy(self._properties)
271
272 @properties.setter
273 def properties(self, val): # pylint: disable=E0202
274 assert not self._finalized
275 assert isinstance(val, dict)
276 self._properties = val
277
278 def finalize(self, annotator_step):
279 self._finalized = True
280 if self.step_text:
281 annotator_step.step_text(self.step_text)
282 if self.step_summary_text:
283 annotator_step.step_summary_text(self.step_summary_text)
284 for name, lines in self.logs.iteritems():
285 annotator_step.write_log_lines(name, lines)
286 for name, lines in self.perf_logs.iteritems():
287 annotator_step.write_log_lines(name, lines, perf=True)
288 for label, url in self.links.iteritems():
289 annotator_step.step_link(label, url)
290 status_mapping = {
291 'WARNING': annotator_step.step_warnings,
292 'FAILURE': annotator_step.step_failure,
293 'EXCEPTION': annotator_step.step_exception,
294 }
295 status_mapping.get(self.status, lambda: None)()
296 for key, value in self._properties.iteritems():
297 annotator_step.set_build_property(key, json.dumps(value, sort_keys=True))
298
299
300 class StepDataAttributeError(AttributeError):
301 """Raised when a non-existent attributed is accessed on a StepData object."""
302 def __init__(self, step, attr):
303 self.step = step
304 self.attr = attr
305 message = ('The recipe attempted to access missing step data "%s" for step '
306 '"%s". Please examine that step for errors.' % (attr, step))
307 super(StepDataAttributeError, self).__init__(message)
308
309
310 class StepData(object):
311 def __init__(self, step, retcode):
312 self._retcode = retcode
313 self._step = step
314
315 self._presentation = StepPresentation()
316 self.abort_reason = None
317
318 @property
319 def step(self):
320 return copy.deepcopy(self._step)
321
322 @property
323 def retcode(self):
324 return self._retcode
325
326 @property
327 def presentation(self):
328 return self._presentation
329
330 def __getattr__(self, name):
331 raise StepDataAttributeError(self._step['name'], name)
332
333
334 # TODO(martiniss) update comment
335 # Result of 'render_step', fed into 'step_callback'.
336 Placeholders = collections.namedtuple(
337 'Placeholders', ['cmd', 'stdout', 'stderr', 'stdin'])
338
339
340 def render_step(step, step_test):
341 """Renders a step so that it can be fed to annotator.py.
342
343 Args:
344 step_test: The test data json dictionary for this step, if any.
345 Passed through unaltered to each placeholder.
346
347 Returns any placeholder instances that were found while rendering the step.
348 """
349 # Process 'cmd', rendering placeholders there.
350 placeholders = collections.defaultdict(lambda: collections.defaultdict(list))
351 new_cmd = []
352 for item in step.get('cmd', []):
353 if isinstance(item, util.Placeholder):
354 module_name, placeholder_name = item.name_pieces
355 tdata = step_test.pop_placeholder(item.name_pieces)
356 new_cmd.extend(item.render(tdata))
357 placeholders[module_name][placeholder_name].append((item, tdata))
358 else:
359 new_cmd.append(item)
360 step['cmd'] = new_cmd
361
362 # Process 'stdout', 'stderr' and 'stdin' placeholders, if given.
363 stdio_placeholders = {}
364 for key in ('stdout', 'stderr', 'stdin'):
365 placeholder = step.get(key)
366 tdata = None
367 if placeholder:
368 assert isinstance(placeholder, util.Placeholder), key
369 tdata = getattr(step_test, key)
370 placeholder.render(tdata)
371 assert placeholder.backing_file
372 step[key] = placeholder.backing_file
373 stdio_placeholders[key] = (placeholder, tdata)
374
375 return Placeholders(cmd=placeholders, **stdio_placeholders)
376
377
378 def get_placeholder_results(step_result, placeholders):
379 class BlankObject(object):
380 pass
381
382 # Placeholders inside step |cmd|.
383 for module_name, pholders in placeholders.cmd.iteritems():
384 assert not hasattr(step_result, module_name)
385 o = BlankObject()
386 setattr(step_result, module_name, o)
387
388 for placeholder_name, items in pholders.iteritems():
389 lst = [ph.result(step_result.presentation, td) for ph, td in items]
390 setattr(o, placeholder_name+"_all", lst)
391 setattr(o, placeholder_name, lst[0])
392
393 # Placeholders that are used with IO redirection.
394 for key in ('stdout', 'stderr', 'stdin'):
395 assert not hasattr(step_result, key)
396 ph, td = getattr(placeholders, key)
397 result = ph.result(step_result.presentation, td) if ph else None
398 setattr(step_result, key, result)
399
400
401 def get_callable_name(func):
402 """Returns __name__ of a callable, handling functools.partial types."""
403 if isinstance(func, functools.partial):
404 return get_callable_name(func.func)
405 else:
406 return func.__name__
407
408
409 # Return value of run_steps and RecipeEngine.run.
410 RecipeExecutionResult = collections.namedtuple(
411 'RecipeExecutionResult', 'status_code steps_ran')
412
413
414 def run_steps(properties,
415 stream,
416 universe,
417 test_data=recipe_test_api.DisabledTestData()):
418 """Returns a tuple of (status_code, steps_ran).
419
420 Only one of these values will be set at a time. This is mainly to support the
421 testing interface used by unittests/recipes_test.py.
422 """
423 stream.honor_zero_return_code()
424
425 # TODO(iannucci): Stop this when blamelist becomes sane data.
426 if ('blamelist_real' in properties and
427 'blamelist' in properties):
428 properties['blamelist'] = properties['blamelist_real']
429 del properties['blamelist_real']
430
431 # NOTE(iannucci): 'root' was a terribly bad idea and has been replaced by
432 # 'patch_project'. 'root' had Rietveld knowing about the implementation of
433 # the builders. 'patch_project' lets the builder (recipe) decide its own
434 # destiny.
435 properties.pop('root', None)
436
437 # TODO(iannucci): A much better way to do this would be to dynamically
438 # detect if the mirrors are actually available during the execution of the
439 # recipe.
440 if ('use_mirror' not in properties and (
441 'TESTING_MASTERNAME' in os.environ or
442 'TESTING_SLAVENAME' in os.environ)):
443 properties['use_mirror'] = False
444
445 engine = RecipeEngine(stream, properties, test_data)
446
447 # Create all API modules and top level RunSteps function. It doesn't launch
448 # any recipe code yet; RunSteps needs to be called.
449 api = None
450 with stream.step('setup_build') as s:
451 assert 'recipe' in properties # Should be ensured by get_recipe_properties.
452 recipe = properties['recipe']
453
454 properties_to_print = properties.copy()
455 if 'use_mirror' in properties:
456 del properties_to_print['use_mirror']
457
458 run_recipe_help_lines = [
459 'To repro this locally, run the following line from a build checkout:',
460 '',
461 './scripts/tools/run_recipe.py %s --properties-file - <<EOF' % recipe,
462 repr(properties_to_print),
463 'EOF',
464 '',
465 'To run on Windows, you can put the JSON in a file and redirect the',
466 'contents of the file into run_recipe.py, with the < operator.',
467 ]
468
469 for line in run_recipe_help_lines:
470 s.step_log_line('run_recipe', line)
471 s.step_log_end('run_recipe')
472
473 _isolate_environment()
474
475 # Find and load the recipe to run.
476 try:
477 recipe_module = universe.load_recipe(recipe)
478 stream.emit('Running recipe with %s' % (properties,))
479 prop_defs = recipe_module.PROPERTIES
480
481 api = loader.create_recipe_api(recipe_module.LOADED_DEPS,
482 engine,
483 test_data)
484
485 s.step_text('<br/>running recipe: "%s"' % recipe)
486 except loader.NoSuchRecipe as e:
487 s.step_text('<br/>recipe not found: %s' % e)
488 s.step_failure()
489 return RecipeExecutionResult(2, None)
490
491 # Run the steps emitted by a recipe via the engine, emitting annotations
492 # into |stream| along the way.
493 return engine.run(recipe_module.RunSteps, api, prop_defs)
494
495
496 def _merge_envs(original, override):
497 """Merges two environments.
498
499 Returns a new environment dict with entries from |override| overwriting
500 corresponding entries in |original|. Keys whose value is None will completely
501 remove the environment variable. Values can contain %(KEY)s strings, which
502 will be substituted with the values from the original (useful for amending, as
503 opposed to overwriting, variables like PATH).
504 """
505 result = original.copy()
506 if not override:
507 return result
508 for k, v in override.items():
509 if v is None:
510 if k in result:
511 del result[k]
512 else:
513 result[str(k)] = str(v) % original
514 return result
515
516
517 def _print_step(step, env, stream):
518 """Prints the step command and relevant metadata.
519
520 Intended to be similar to the information that Buildbot prints at the
521 beginning of each non-annotator step.
522 """
523 step_info_lines = []
524 step_info_lines.append(' '.join(step['cmd']))
525 step_info_lines.append('in dir %s:' % (step['cwd'] or os.getcwd()))
526 for key, value in sorted(step.items()):
527 if value is not None:
528 if callable(value):
529 # This prevents functions from showing up as:
530 # '<function foo at 0x7f523ec7a410>'
531 # which is tricky to test.
532 value = value.__name__+'(...)'
533 step_info_lines.append(' %s: %s' % (key, value))
534 step_info_lines.append('full environment:')
535 for key, value in sorted(env.items()):
536 step_info_lines.append(' %s: %s' % (key, value))
537 step_info_lines.append('')
538 stream.emit('\n'.join(step_info_lines))
539
540
541 @contextlib.contextmanager
542 def _modify_lookup_path(path):
543 """Places the specified path into os.environ.
544
545 Necessary because subprocess.Popen uses os.environ to perform lookup on the
546 supplied command, and only uses the |env| kwarg for modifying the environment
547 of the child process.
548 """
549 saved_path = os.environ['PATH']
550 try:
551 if path is not None:
552 os.environ['PATH'] = path
553 yield
554 finally:
555 os.environ['PATH'] = saved_path
556
557
558 def _normalize_change(change):
559 assert isinstance(change, dict), 'Change is not a dict'
560 change = change.copy()
561
562 # Convert when_timestamp to UNIX timestamp.
563 when = change.get('when_timestamp')
564 if isinstance(when, datetime.datetime):
565 when = calendar.timegm(when.utctimetuple())
566 change['when_timestamp'] = when
567
568 return change
569
570
571 def _trigger_builds(step, trigger_specs):
572 assert trigger_specs is not None
573 for trig in trigger_specs:
574 builder_name = trig.get('builder_name')
575 if not builder_name:
576 raise ValueError('Trigger spec: builder_name is not set')
577
578 changes = trig.get('buildbot_changes', [])
579 assert isinstance(changes, list), 'buildbot_changes must be a list'
580 changes = map(_normalize_change, changes)
581
582 step.step_trigger(json.dumps({
583 'builderNames': [builder_name],
584 'bucket': trig.get('bucket'),
585 'changes': changes,
586 'properties': trig.get('properties'),
587 }, sort_keys=True))
588
589
590 def _run_annotated_step(
591 stream, name, cmd, cwd=None, env=None, allow_subannotations=False,
592 trigger_specs=None, nest_level=0, **kwargs):
593 """Runs a single step.
594
595 Context:
596 stream: StructuredAnnotationStream to use to emit step
597
598 Step parameters:
599 name: name of the step, will appear in buildbots waterfall
600 cmd: command to run, list of one or more strings
601 cwd: absolute path to working directory for the command
602 env: dict with overrides for environment variables
603 allow_subannotations: if True, lets the step emit its own annotations
604 trigger_specs: a list of trigger specifications, which are dict with keys:
605 properties: a dict of properties.
606 Buildbot requires buildername property.
607
608 Known kwargs:
609 stdout: Path to a file to put step stdout into. If used, stdout won't appear
610 in annotator's stdout (and |allow_subannotations| is ignored).
611 stderr: Path to a file to put step stderr into. If used, stderr won't appear
612 in annotator's stderr.
613 stdin: Path to a file to read step stdin from.
614
615 Returns the returncode of the step.
616 """
617 if isinstance(cmd, basestring):
618 cmd = (cmd,)
619 cmd = map(str, cmd)
620
621 # For error reporting.
622 step_dict = kwargs.copy()
623 step_dict.update({
624 'name': name,
625 'cmd': cmd,
626 'cwd': cwd,
627 'env': env,
628 'allow_subannotations': allow_subannotations,
629 })
630 step_env = _merge_envs(os.environ, env)
631
632 step_annotation = stream.step(name)
633 step_annotation.step_started()
634
635 if nest_level:
636 step_annotation.step_nest_level(nest_level)
637
638 _print_step(step_dict, step_env, stream)
639 returncode = 0
640 if cmd:
641 try:
642 # Open file handles for IO redirection based on file names in step_dict.
643 fhandles = {
644 'stdout': subprocess.PIPE,
645 'stderr': subprocess.PIPE,
646 'stdin': None,
647 }
648 for key in fhandles:
649 if key in step_dict:
650 fhandles[key] = open(step_dict[key],
651 'rb' if key == 'stdin' else 'wb')
652
653 if sys.platform.startswith('win'):
654 # Windows has a bad habit of opening a dialog when a console program
655 # crashes, rather than just letting it crash. Therefore, when a program
656 # crashes on Windows, we don't find out until the build step times out.
657 # This code prevents the dialog from appearing, so that we find out
658 # immediately and don't waste time waiting for a user to close the
659 # dialog.
660 import ctypes
661 # SetErrorMode(SEM_NOGPFAULTERRORBOX). For more information, see:
662 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms680621.aspx
663 ctypes.windll.kernel32.SetErrorMode(0x0002)
664 # CREATE_NO_WINDOW. For more information, see:
665 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
666 creationflags = 0x8000000
667 else:
668 creationflags = 0
669
670 with _modify_lookup_path(step_env.get('PATH')):
671 proc = subprocess.Popen(
672 cmd,
673 env=step_env,
674 cwd=cwd,
675 universal_newlines=True,
676 creationflags=creationflags,
677 **fhandles)
678
679 # Safe to close file handles now that subprocess has inherited them.
680 for handle in fhandles.itervalues():
681 if isinstance(handle, file):
682 handle.close()
683
684 outlock = threading.Lock()
685 def filter_lines(lock, allow_subannotations, inhandle, outhandle):
686 while True:
687 line = inhandle.readline()
688 if not line:
689 break
690 lock.acquire()
691 try:
692 if not allow_subannotations and line.startswith('@@@'):
693 outhandle.write('!')
694 outhandle.write(line)
695 outhandle.flush()
696 finally:
697 lock.release()
698
699 # Pump piped stdio through filter_lines. IO going to files on disk is
700 # not filtered.
701 threads = []
702 for key in ('stdout', 'stderr'):
703 if fhandles[key] == subprocess.PIPE:
704 inhandle = getattr(proc, key)
705 outhandle = getattr(sys, key)
706 threads.append(threading.Thread(
707 target=filter_lines,
708 args=(outlock, allow_subannotations, inhandle, outhandle)))
709
710 for th in threads:
711 th.start()
712 proc.wait()
713 for th in threads:
714 th.join()
715 returncode = proc.returncode
716 except OSError:
717 # File wasn't found, error will be reported to stream when the exception
718 # crosses the context manager.
719 step_annotation.step_exception_occured(*sys.exc_info())
720 raise
721
722 # TODO(martiniss) move logic into own module?
723 if trigger_specs:
724 _trigger_builds(step_annotation, trigger_specs)
725
726 return step_annotation, returncode
727
728 class RecipeEngine(object):
729 """
730 Knows how to execute steps emitted by a recipe, holds global state such as
731 step history and build properties. Each recipe module API has a reference to
732 this object.
733
734 Recipe modules that are aware of the engine:
735 * properties - uses engine.properties.
736 * step_history - uses engine.step_history.
737 * step - uses engine.create_step(...).
738
739 """
740 def __init__(self, stream, properties, test_data):
741 self._stream = stream
742 self._properties = properties
743 self._test_data = test_data
744 self._step_history = collections.OrderedDict()
745
746 self._previous_step_annotation = None
747 self._previous_step_result = None
748 self._api = None
749
750 @property
751 def properties(self):
752 return self._properties
753
754 @property
755 def previous_step_result(self):
756 """Allows api.step to get the active result from any context."""
757 return self._previous_step_result
758
759 def _emit_results(self):
760 """Internal helper used to emit results."""
761 annotation = self._previous_step_annotation
762 step_result = self._previous_step_result
763
764 self._previous_step_annotation = None
765 self._previous_step_result = None
766
767 if not annotation or not step_result:
768 return
769
770 step_result.presentation.finalize(annotation)
771 if self._test_data.enabled:
772 val = annotation.stream.getvalue()
773 lines = filter(None, val.splitlines())
774 if lines:
775 # note that '~' sorts after 'z' so that this will be last on each
776 # step. also use _step to get access to the mutable step
777 # dictionary.
778 # pylint: disable=w0212
779 step_result._step['~followup_annotations'] = lines
780 annotation.step_ended()
781
782 def run_step(self, step):
783 """
784 Runs a step.
785
786 Args:
787 step: The step to run.
788
789 Returns:
790 A StepData object containing the result of running the step.
791 """
792 ok_ret = step.pop('ok_ret')
793 infra_step = step.pop('infra_step')
794 nest_level = step.pop('step_nest_level')
795
796 test_data_fn = step.pop('step_test_data', recipe_test_api.StepTestData)
797 step_test = self._test_data.pop_step_test_data(step['name'],
798 test_data_fn)
799 placeholders = render_step(step, step_test)
800
801 self._step_history[step['name']] = step
802 self._emit_results()
803
804 step_result = None
805
806 if not self._test_data.enabled:
807 self._previous_step_annotation, retcode = _run_annotated_step(
808 self._stream, nest_level=nest_level, **step)
809
810 step_result = StepData(step, retcode)
811 self._stream.step_cursor(step['name'])
812 else:
813 self._previous_step_annotation = annotation = self._stream.step(
814 step['name'])
815 annotation.step_started()
816 try:
817 annotation.stream = cStringIO.StringIO()
818 if nest_level:
819 annotation.step_nest_level(nest_level)
820
821 step_result = StepData(step, step_test.retcode)
822 except OSError:
823 exc_type, exc_value, exc_tb = sys.exc_info()
824 trace = traceback.format_exception(exc_type, exc_value, exc_tb)
825 trace_lines = ''.join(trace).split('\n')
826 annotation.write_log_lines('exception', filter(None, trace_lines))
827 annotation.step_exception()
828
829 get_placeholder_results(step_result, placeholders)
830 self._previous_step_result = step_result
831
832 if step_result.retcode in ok_ret:
833 step_result.presentation.status = 'SUCCESS'
834 return step_result
835 else:
836 if not infra_step:
837 state = 'FAILURE'
838 exc = recipe_api.StepFailure
839 else:
840 state = 'EXCEPTION'
841 exc = recipe_api.InfraFailure
842
843 step_result.presentation.status = state
844 if step_test.enabled:
845 # To avoid cluttering the expectations, don't emit this in testmode.
846 self._previous_step_annotation.emit(
847 'step returned non-zero exit code: %d' % step_result.retcode)
848
849 raise exc(step['name'], step_result)
850
851
852 def run(self, steps_function, api, prop_defs):
853 """Run a recipe represented by top level RunSteps function.
854
855 This function blocks until recipe finishes.
856
857 Args:
858 steps_function: function that runs the steps.
859 api: The api, with loaded module dependencies.
860 Used by the some special modules.
861 prop_defs: Property definitions for this recipe.
862
863 Returns:
864 RecipeExecutionResult with status code and list of steps ran.
865 """
866 self._api = api
867 retcode = None
868 final_result = None
869
870 try:
871 try:
872 retcode = loader.invoke_with_properties(
873 steps_function, api._engine.properties, prop_defs, api=api)
874 assert retcode is None, (
875 "Non-None return from RunSteps is not supported yet")
876
877 assert not self._test_data.enabled or not self._test_data.step_data, (
878 "Unconsumed test data! %s" % (self._test_data.step_data,))
879 finally:
880 self._emit_results()
881 except recipe_api.StepFailure as f:
882 retcode = f.retcode or 1
883 final_result = {
884 "name": "$final_result",
885 "reason": f.reason,
886 "status_code": retcode
887 }
888 except StepDataAttributeError as ex:
889 unexpected_exception = self._test_data.is_unexpected_exception(ex)
890
891 retcode = -1
892 final_result = {
893 "name": "$final_result",
894 "reason": "Invalid Step Data Access: %r" % ex,
895 "status_code": retcode
896 }
897
898 with self._stream.step('Invalid Step Data Access') as s:
899 s.step_exception()
900 s.write_log_lines('exception', traceback.format_exc().splitlines())
901
902 if unexpected_exception:
903 raise
904
905 except Exception as ex:
906 unexpected_exception = self._test_data.is_unexpected_exception(ex)
907
908 retcode = -1
909 final_result = {
910 "name": "$final_result",
911 "reason": "Uncaught Exception: %r" % ex,
912 "status_code": retcode
913 }
914
915 with self._stream.step('Uncaught Exception') as s:
916 s.step_exception()
917 s.write_log_lines('exception', traceback.format_exc().splitlines())
918
919 if unexpected_exception:
920 raise
921
922 if final_result is not None:
923 self._step_history[final_result['name']] = final_result
924
925 return RecipeExecutionResult(retcode, self._step_history)
926
927 def create_step(self, step): # pylint: disable=R0201
928 """Called by step module to instantiate a new step.
929
930 Args:
931 step: ConfigGroup object with information about the step, see
932 recipe_modules/step/config.py.
933
934 Returns:
935 Opaque engine specific object that is understood by 'run_steps' method.
936 """
937 return step.as_jsonish()
938
939
OLDNEW
« no previous file with comments | « third_party/recipe_engine/loader.py ('k') | third_party/recipe_engine/recipe_api.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698