Index: third_party/recipe_engine/main.py |
diff --git a/third_party/recipe_engine/main.py b/third_party/recipe_engine/main.py |
deleted file mode 100755 |
index 97d0fb5c092bce0e824ca8f7f4ab896e1f8e49ad..0000000000000000000000000000000000000000 |
--- a/third_party/recipe_engine/main.py |
+++ /dev/null |
@@ -1,939 +0,0 @@ |
-# Copyright (c) 2013-2015 The Chromium Authors. All rights reserved. |
-# Use of this source code is governed by a BSD-style license that can be |
-# found in the LICENSE file. |
- |
-"""Entry point for fully-annotated builds. |
- |
-This script is part of the effort to move all builds to annotator-based |
-systems. Any builder configured to use the AnnotatorFactory.BaseFactory() |
-found in scripts/master/factory/annotator_factory.py executes a single |
-AddAnnotatedScript step. That step (found in annotator_commands.py) calls |
-this script with the build- and factory-properties passed on the command |
-line. |
- |
-The main mode of operation is for factory_properties to contain a single |
-property 'recipe' whose value is the basename (without extension) of a python |
-script in one of the following locations (looked up in this order): |
- * build_internal/scripts/slave-internal/recipes |
- * build_internal/scripts/slave/recipes |
- * build/scripts/slave/recipes |
- |
-For example, these factory_properties would run the 'run_presubmit' recipe |
-located in build/scripts/slave/recipes: |
- { 'recipe': 'run_presubmit' } |
- |
-TODO(vadimsh, iannucci, luqui): The following docs are very outdated. |
- |
-Annotated_run.py will then import the recipe and expect to call a function whose |
-signature is: |
- RunSteps(api, properties) -> None. |
- |
-properties is a merged view of factory_properties with build_properties. |
- |
-Items in iterable_of_things must be one of: |
- * A step dictionary (as accepted by annotator.py) |
- * A sequence of step dictionaries |
- * A step generator |
-Iterable_of_things is also permitted to be a raw step generator. |
- |
-A step generator is called with the following protocol: |
- * The generator is initialized with 'step_history' and 'failed'. |
- * Each iteration of the generator is passed the current value of 'failed'. |
- |
-On each iteration, a step generator may yield: |
- * A single step dictionary |
- * A sequence of step dictionaries |
- * If a sequence of dictionaries is yielded, and the first step dictionary |
- does not have a 'seed_steps' key, the first step will be augmented with |
- a 'seed_steps' key containing the names of all the steps in the sequence. |
- |
-For steps yielded by the generator, if annotated_run enters the failed state, |
-it will only continue to call the generator if the generator sets the |
-'keep_going' key on the steps which it has produced. Otherwise annotated_run |
-will cease calling the generator and move on to the next item in |
-iterable_of_things. |
- |
-'step_history' is an OrderedDict of {stepname -> StepData}, always representing |
- the current history of what steps have run, what they returned, and any |
- json data they emitted. Additionally, the OrderedDict has the following |
- convenience functions defined: |
- * last_step - Returns the last step that ran or None |
- * nth_step(n) - Returns the N'th step that ran or None |
- |
-'failed' is a boolean representing if the build is in a 'failed' state. |
-""" |
- |
-import collections |
-import contextlib |
-import copy |
-import functools |
-import json |
-import os |
-import subprocess |
-import sys |
-import threading |
-import traceback |
- |
-import cStringIO |
- |
- |
-from . import loader |
-from . import recipe_api |
-from . import recipe_test_api |
-from . import util |
- |
- |
-SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) |
- |
-BUILDBOT_MAGIC_ENV = set([ |
- 'BUILDBOT_BLAMELIST', |
- 'BUILDBOT_BRANCH', |
- 'BUILDBOT_BUILDBOTURL', |
- 'BUILDBOT_BUILDERNAME', |
- 'BUILDBOT_BUILDNUMBER', |
- 'BUILDBOT_CLOBBER', |
- 'BUILDBOT_GOT_REVISION', |
- 'BUILDBOT_MASTERNAME', |
- 'BUILDBOT_REVISION', |
- 'BUILDBOT_SCHEDULER', |
- 'BUILDBOT_SLAVENAME', |
-]) |
- |
-ENV_WHITELIST_WIN = BUILDBOT_MAGIC_ENV | set([ |
- 'APPDATA', |
- 'AWS_CREDENTIAL_FILE', |
- 'BOTO_CONFIG', |
- 'BUILDBOT_ARCHIVE_FORCE_SSH', |
- 'CHROME_HEADLESS', |
- 'CHROMIUM_BUILD', |
- 'COMMONPROGRAMFILES', |
- 'COMMONPROGRAMFILES(X86)', |
- 'COMMONPROGRAMW6432', |
- 'COMSPEC', |
- 'COMPUTERNAME', |
- 'DBUS_SESSION_BUS_ADDRESS', |
- 'DEPOT_TOOLS_GIT_BLEEDING', |
- # TODO(maruel): Remove once everyone is on 2.7.5. |
- 'DEPOT_TOOLS_PYTHON_275', |
- 'DXSDK_DIR', |
- 'GIT_USER_AGENT', |
- 'HOME', |
- 'HOMEDRIVE', |
- 'HOMEPATH', |
- 'LOCALAPPDATA', |
- 'NUMBER_OF_PROCESSORS', |
- 'OS', |
- 'PATH', |
- 'PATHEXT', |
- 'PROCESSOR_ARCHITECTURE', |
- 'PROCESSOR_ARCHITEW6432', |
- 'PROCESSOR_IDENTIFIER', |
- 'PROGRAMFILES', |
- 'PROGRAMW6432', |
- 'PWD', |
- 'PYTHONPATH', |
- 'SYSTEMDRIVE', |
- 'SYSTEMROOT', |
- 'TEMP', |
- 'TESTING_MASTER', |
- 'TESTING_MASTER_HOST', |
- 'TESTING_SLAVENAME', |
- 'TMP', |
- 'USERNAME', |
- 'USERDOMAIN', |
- 'USERPROFILE', |
- 'VS100COMNTOOLS', |
- 'VS110COMNTOOLS', |
- 'WINDIR', |
-]) |
- |
-ENV_WHITELIST_POSIX = BUILDBOT_MAGIC_ENV | set([ |
- 'AWS_CREDENTIAL_FILE', |
- 'BOTO_CONFIG', |
- 'CCACHE_DIR', |
- 'CHROME_ALLOCATOR', |
- 'CHROME_HEADLESS', |
- 'CHROME_VALGRIND_NUMCPUS', |
- 'DISPLAY', |
- 'DISTCC_DIR', |
- 'GIT_USER_AGENT', |
- 'HOME', |
- 'HOSTNAME', |
- 'HTTP_PROXY', |
- 'http_proxy', |
- 'HTTPS_PROXY', |
- 'LANG', |
- 'LOGNAME', |
- 'PAGER', |
- 'PATH', |
- 'PWD', |
- 'PYTHONPATH', |
- 'SHELL', |
- 'SSH_AGENT_PID', |
- 'SSH_AUTH_SOCK', |
- 'SSH_CLIENT', |
- 'SSH_CONNECTION', |
- 'SSH_TTY', |
- 'TESTING_MASTER', |
- 'TESTING_MASTER_HOST', |
- 'TESTING_SLAVENAME', |
- 'USER', |
- 'USERNAME', |
-]) |
- |
- |
-def _isolate_environment(): |
- """Isolate the environment to a known subset set.""" |
- if sys.platform.startswith('win'): |
- whitelist = ENV_WHITELIST_WIN |
- elif sys.platform in ('darwin', 'posix', 'linux2'): |
- whitelist = ENV_WHITELIST_POSIX |
- else: |
- print ('WARNING: unknown platform %s, not isolating environment.' % |
- sys.platform) |
- return |
- |
- for k in os.environ.keys(): |
- if k not in whitelist: |
- del os.environ[k] |
- |
- |
-class StepPresentation(object): |
- STATUSES = set(('SUCCESS', 'FAILURE', 'WARNING', 'EXCEPTION')) |
- |
- def __init__(self): |
- self._finalized = False |
- |
- self._logs = collections.OrderedDict() |
- self._links = collections.OrderedDict() |
- self._perf_logs = collections.OrderedDict() |
- self._status = None |
- self._step_summary_text = '' |
- self._step_text = '' |
- self._properties = {} |
- |
- # (E0202) pylint bug: http://www.logilab.org/ticket/89092 |
- @property |
- def status(self): # pylint: disable=E0202 |
- return self._status |
- |
- @status.setter |
- def status(self, val): # pylint: disable=E0202 |
- assert not self._finalized |
- assert val in self.STATUSES |
- self._status = val |
- |
- @property |
- def step_text(self): |
- return self._step_text |
- |
- @step_text.setter |
- def step_text(self, val): |
- assert not self._finalized |
- self._step_text = val |
- |
- @property |
- def step_summary_text(self): |
- return self._step_summary_text |
- |
- @step_summary_text.setter |
- def step_summary_text(self, val): |
- assert not self._finalized |
- self._step_summary_text = val |
- |
- @property |
- def logs(self): |
- if not self._finalized: |
- return self._logs |
- else: |
- return copy.deepcopy(self._logs) |
- |
- @property |
- def links(self): |
- if not self._finalized: |
- return self._links |
- else: |
- return copy.deepcopy(self._links) |
- |
- @property |
- def perf_logs(self): |
- if not self._finalized: |
- return self._perf_logs |
- else: |
- return copy.deepcopy(self._perf_logs) |
- |
- @property |
- def properties(self): # pylint: disable=E0202 |
- if not self._finalized: |
- return self._properties |
- else: |
- return copy.deepcopy(self._properties) |
- |
- @properties.setter |
- def properties(self, val): # pylint: disable=E0202 |
- assert not self._finalized |
- assert isinstance(val, dict) |
- self._properties = val |
- |
- def finalize(self, annotator_step): |
- self._finalized = True |
- if self.step_text: |
- annotator_step.step_text(self.step_text) |
- if self.step_summary_text: |
- annotator_step.step_summary_text(self.step_summary_text) |
- for name, lines in self.logs.iteritems(): |
- annotator_step.write_log_lines(name, lines) |
- for name, lines in self.perf_logs.iteritems(): |
- annotator_step.write_log_lines(name, lines, perf=True) |
- for label, url in self.links.iteritems(): |
- annotator_step.step_link(label, url) |
- status_mapping = { |
- 'WARNING': annotator_step.step_warnings, |
- 'FAILURE': annotator_step.step_failure, |
- 'EXCEPTION': annotator_step.step_exception, |
- } |
- status_mapping.get(self.status, lambda: None)() |
- for key, value in self._properties.iteritems(): |
- annotator_step.set_build_property(key, json.dumps(value, sort_keys=True)) |
- |
- |
-class StepDataAttributeError(AttributeError): |
- """Raised when a non-existent attributed is accessed on a StepData object.""" |
- def __init__(self, step, attr): |
- self.step = step |
- self.attr = attr |
- message = ('The recipe attempted to access missing step data "%s" for step ' |
- '"%s". Please examine that step for errors.' % (attr, step)) |
- super(StepDataAttributeError, self).__init__(message) |
- |
- |
-class StepData(object): |
- def __init__(self, step, retcode): |
- self._retcode = retcode |
- self._step = step |
- |
- self._presentation = StepPresentation() |
- self.abort_reason = None |
- |
- @property |
- def step(self): |
- return copy.deepcopy(self._step) |
- |
- @property |
- def retcode(self): |
- return self._retcode |
- |
- @property |
- def presentation(self): |
- return self._presentation |
- |
- def __getattr__(self, name): |
- raise StepDataAttributeError(self._step['name'], name) |
- |
- |
-# TODO(martiniss) update comment |
-# Result of 'render_step', fed into 'step_callback'. |
-Placeholders = collections.namedtuple( |
- 'Placeholders', ['cmd', 'stdout', 'stderr', 'stdin']) |
- |
- |
-def render_step(step, step_test): |
- """Renders a step so that it can be fed to annotator.py. |
- |
- Args: |
- step_test: The test data json dictionary for this step, if any. |
- Passed through unaltered to each placeholder. |
- |
- Returns any placeholder instances that were found while rendering the step. |
- """ |
- # Process 'cmd', rendering placeholders there. |
- placeholders = collections.defaultdict(lambda: collections.defaultdict(list)) |
- new_cmd = [] |
- for item in step.get('cmd', []): |
- if isinstance(item, util.Placeholder): |
- module_name, placeholder_name = item.name_pieces |
- tdata = step_test.pop_placeholder(item.name_pieces) |
- new_cmd.extend(item.render(tdata)) |
- placeholders[module_name][placeholder_name].append((item, tdata)) |
- else: |
- new_cmd.append(item) |
- step['cmd'] = new_cmd |
- |
- # Process 'stdout', 'stderr' and 'stdin' placeholders, if given. |
- stdio_placeholders = {} |
- for key in ('stdout', 'stderr', 'stdin'): |
- placeholder = step.get(key) |
- tdata = None |
- if placeholder: |
- assert isinstance(placeholder, util.Placeholder), key |
- tdata = getattr(step_test, key) |
- placeholder.render(tdata) |
- assert placeholder.backing_file |
- step[key] = placeholder.backing_file |
- stdio_placeholders[key] = (placeholder, tdata) |
- |
- return Placeholders(cmd=placeholders, **stdio_placeholders) |
- |
- |
-def get_placeholder_results(step_result, placeholders): |
- class BlankObject(object): |
- pass |
- |
- # Placeholders inside step |cmd|. |
- for module_name, pholders in placeholders.cmd.iteritems(): |
- assert not hasattr(step_result, module_name) |
- o = BlankObject() |
- setattr(step_result, module_name, o) |
- |
- for placeholder_name, items in pholders.iteritems(): |
- lst = [ph.result(step_result.presentation, td) for ph, td in items] |
- setattr(o, placeholder_name+"_all", lst) |
- setattr(o, placeholder_name, lst[0]) |
- |
- # Placeholders that are used with IO redirection. |
- for key in ('stdout', 'stderr', 'stdin'): |
- assert not hasattr(step_result, key) |
- ph, td = getattr(placeholders, key) |
- result = ph.result(step_result.presentation, td) if ph else None |
- setattr(step_result, key, result) |
- |
- |
-def get_callable_name(func): |
- """Returns __name__ of a callable, handling functools.partial types.""" |
- if isinstance(func, functools.partial): |
- return get_callable_name(func.func) |
- else: |
- return func.__name__ |
- |
- |
-# Return value of run_steps and RecipeEngine.run. |
-RecipeExecutionResult = collections.namedtuple( |
- 'RecipeExecutionResult', 'status_code steps_ran') |
- |
- |
-def run_steps(properties, |
- stream, |
- universe, |
- test_data=recipe_test_api.DisabledTestData()): |
- """Returns a tuple of (status_code, steps_ran). |
- |
- Only one of these values will be set at a time. This is mainly to support the |
- testing interface used by unittests/recipes_test.py. |
- """ |
- stream.honor_zero_return_code() |
- |
- # TODO(iannucci): Stop this when blamelist becomes sane data. |
- if ('blamelist_real' in properties and |
- 'blamelist' in properties): |
- properties['blamelist'] = properties['blamelist_real'] |
- del properties['blamelist_real'] |
- |
- # NOTE(iannucci): 'root' was a terribly bad idea and has been replaced by |
- # 'patch_project'. 'root' had Rietveld knowing about the implementation of |
- # the builders. 'patch_project' lets the builder (recipe) decide its own |
- # destiny. |
- properties.pop('root', None) |
- |
- # TODO(iannucci): A much better way to do this would be to dynamically |
- # detect if the mirrors are actually available during the execution of the |
- # recipe. |
- if ('use_mirror' not in properties and ( |
- 'TESTING_MASTERNAME' in os.environ or |
- 'TESTING_SLAVENAME' in os.environ)): |
- properties['use_mirror'] = False |
- |
- engine = RecipeEngine(stream, properties, test_data) |
- |
- # Create all API modules and top level RunSteps function. It doesn't launch |
- # any recipe code yet; RunSteps needs to be called. |
- api = None |
- with stream.step('setup_build') as s: |
- assert 'recipe' in properties # Should be ensured by get_recipe_properties. |
- recipe = properties['recipe'] |
- |
- properties_to_print = properties.copy() |
- if 'use_mirror' in properties: |
- del properties_to_print['use_mirror'] |
- |
- run_recipe_help_lines = [ |
- 'To repro this locally, run the following line from a build checkout:', |
- '', |
- './scripts/tools/run_recipe.py %s --properties-file - <<EOF' % recipe, |
- repr(properties_to_print), |
- 'EOF', |
- '', |
- 'To run on Windows, you can put the JSON in a file and redirect the', |
- 'contents of the file into run_recipe.py, with the < operator.', |
- ] |
- |
- for line in run_recipe_help_lines: |
- s.step_log_line('run_recipe', line) |
- s.step_log_end('run_recipe') |
- |
- _isolate_environment() |
- |
- # Find and load the recipe to run. |
- try: |
- recipe_module = universe.load_recipe(recipe) |
- stream.emit('Running recipe with %s' % (properties,)) |
- prop_defs = recipe_module.PROPERTIES |
- |
- api = loader.create_recipe_api(recipe_module.LOADED_DEPS, |
- engine, |
- test_data) |
- |
- s.step_text('<br/>running recipe: "%s"' % recipe) |
- except loader.NoSuchRecipe as e: |
- s.step_text('<br/>recipe not found: %s' % e) |
- s.step_failure() |
- return RecipeExecutionResult(2, None) |
- |
- # Run the steps emitted by a recipe via the engine, emitting annotations |
- # into |stream| along the way. |
- return engine.run(recipe_module.RunSteps, api, prop_defs) |
- |
- |
-def _merge_envs(original, override): |
- """Merges two environments. |
- |
- Returns a new environment dict with entries from |override| overwriting |
- corresponding entries in |original|. Keys whose value is None will completely |
- remove the environment variable. Values can contain %(KEY)s strings, which |
- will be substituted with the values from the original (useful for amending, as |
- opposed to overwriting, variables like PATH). |
- """ |
- result = original.copy() |
- if not override: |
- return result |
- for k, v in override.items(): |
- if v is None: |
- if k in result: |
- del result[k] |
- else: |
- result[str(k)] = str(v) % original |
- return result |
- |
- |
-def _print_step(step, env, stream): |
- """Prints the step command and relevant metadata. |
- |
- Intended to be similar to the information that Buildbot prints at the |
- beginning of each non-annotator step. |
- """ |
- step_info_lines = [] |
- step_info_lines.append(' '.join(step['cmd'])) |
- step_info_lines.append('in dir %s:' % (step['cwd'] or os.getcwd())) |
- for key, value in sorted(step.items()): |
- if value is not None: |
- if callable(value): |
- # This prevents functions from showing up as: |
- # '<function foo at 0x7f523ec7a410>' |
- # which is tricky to test. |
- value = value.__name__+'(...)' |
- step_info_lines.append(' %s: %s' % (key, value)) |
- step_info_lines.append('full environment:') |
- for key, value in sorted(env.items()): |
- step_info_lines.append(' %s: %s' % (key, value)) |
- step_info_lines.append('') |
- stream.emit('\n'.join(step_info_lines)) |
- |
- |
-@contextlib.contextmanager |
-def _modify_lookup_path(path): |
- """Places the specified path into os.environ. |
- |
- Necessary because subprocess.Popen uses os.environ to perform lookup on the |
- supplied command, and only uses the |env| kwarg for modifying the environment |
- of the child process. |
- """ |
- saved_path = os.environ['PATH'] |
- try: |
- if path is not None: |
- os.environ['PATH'] = path |
- yield |
- finally: |
- os.environ['PATH'] = saved_path |
- |
- |
-def _normalize_change(change): |
- assert isinstance(change, dict), 'Change is not a dict' |
- change = change.copy() |
- |
- # Convert when_timestamp to UNIX timestamp. |
- when = change.get('when_timestamp') |
- if isinstance(when, datetime.datetime): |
- when = calendar.timegm(when.utctimetuple()) |
- change['when_timestamp'] = when |
- |
- return change |
- |
- |
-def _trigger_builds(step, trigger_specs): |
- assert trigger_specs is not None |
- for trig in trigger_specs: |
- builder_name = trig.get('builder_name') |
- if not builder_name: |
- raise ValueError('Trigger spec: builder_name is not set') |
- |
- changes = trig.get('buildbot_changes', []) |
- assert isinstance(changes, list), 'buildbot_changes must be a list' |
- changes = map(_normalize_change, changes) |
- |
- step.step_trigger(json.dumps({ |
- 'builderNames': [builder_name], |
- 'bucket': trig.get('bucket'), |
- 'changes': changes, |
- 'properties': trig.get('properties'), |
- }, sort_keys=True)) |
- |
- |
-def _run_annotated_step( |
- stream, name, cmd, cwd=None, env=None, allow_subannotations=False, |
- trigger_specs=None, nest_level=0, **kwargs): |
- """Runs a single step. |
- |
- Context: |
- stream: StructuredAnnotationStream to use to emit step |
- |
- Step parameters: |
- name: name of the step, will appear in buildbots waterfall |
- cmd: command to run, list of one or more strings |
- cwd: absolute path to working directory for the command |
- env: dict with overrides for environment variables |
- allow_subannotations: if True, lets the step emit its own annotations |
- trigger_specs: a list of trigger specifications, which are dict with keys: |
- properties: a dict of properties. |
- Buildbot requires buildername property. |
- |
- Known kwargs: |
- stdout: Path to a file to put step stdout into. If used, stdout won't appear |
- in annotator's stdout (and |allow_subannotations| is ignored). |
- stderr: Path to a file to put step stderr into. If used, stderr won't appear |
- in annotator's stderr. |
- stdin: Path to a file to read step stdin from. |
- |
- Returns the returncode of the step. |
- """ |
- if isinstance(cmd, basestring): |
- cmd = (cmd,) |
- cmd = map(str, cmd) |
- |
- # For error reporting. |
- step_dict = kwargs.copy() |
- step_dict.update({ |
- 'name': name, |
- 'cmd': cmd, |
- 'cwd': cwd, |
- 'env': env, |
- 'allow_subannotations': allow_subannotations, |
- }) |
- step_env = _merge_envs(os.environ, env) |
- |
- step_annotation = stream.step(name) |
- step_annotation.step_started() |
- |
- if nest_level: |
- step_annotation.step_nest_level(nest_level) |
- |
- _print_step(step_dict, step_env, stream) |
- returncode = 0 |
- if cmd: |
- try: |
- # Open file handles for IO redirection based on file names in step_dict. |
- fhandles = { |
- 'stdout': subprocess.PIPE, |
- 'stderr': subprocess.PIPE, |
- 'stdin': None, |
- } |
- for key in fhandles: |
- if key in step_dict: |
- fhandles[key] = open(step_dict[key], |
- 'rb' if key == 'stdin' else 'wb') |
- |
- if sys.platform.startswith('win'): |
- # Windows has a bad habit of opening a dialog when a console program |
- # crashes, rather than just letting it crash. Therefore, when a program |
- # crashes on Windows, we don't find out until the build step times out. |
- # This code prevents the dialog from appearing, so that we find out |
- # immediately and don't waste time waiting for a user to close the |
- # dialog. |
- import ctypes |
- # SetErrorMode(SEM_NOGPFAULTERRORBOX). For more information, see: |
- # https://msdn.microsoft.com/en-us/library/windows/desktop/ms680621.aspx |
- ctypes.windll.kernel32.SetErrorMode(0x0002) |
- # CREATE_NO_WINDOW. For more information, see: |
- # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx |
- creationflags = 0x8000000 |
- else: |
- creationflags = 0 |
- |
- with _modify_lookup_path(step_env.get('PATH')): |
- proc = subprocess.Popen( |
- cmd, |
- env=step_env, |
- cwd=cwd, |
- universal_newlines=True, |
- creationflags=creationflags, |
- **fhandles) |
- |
- # Safe to close file handles now that subprocess has inherited them. |
- for handle in fhandles.itervalues(): |
- if isinstance(handle, file): |
- handle.close() |
- |
- outlock = threading.Lock() |
- def filter_lines(lock, allow_subannotations, inhandle, outhandle): |
- while True: |
- line = inhandle.readline() |
- if not line: |
- break |
- lock.acquire() |
- try: |
- if not allow_subannotations and line.startswith('@@@'): |
- outhandle.write('!') |
- outhandle.write(line) |
- outhandle.flush() |
- finally: |
- lock.release() |
- |
- # Pump piped stdio through filter_lines. IO going to files on disk is |
- # not filtered. |
- threads = [] |
- for key in ('stdout', 'stderr'): |
- if fhandles[key] == subprocess.PIPE: |
- inhandle = getattr(proc, key) |
- outhandle = getattr(sys, key) |
- threads.append(threading.Thread( |
- target=filter_lines, |
- args=(outlock, allow_subannotations, inhandle, outhandle))) |
- |
- for th in threads: |
- th.start() |
- proc.wait() |
- for th in threads: |
- th.join() |
- returncode = proc.returncode |
- except OSError: |
- # File wasn't found, error will be reported to stream when the exception |
- # crosses the context manager. |
- step_annotation.step_exception_occured(*sys.exc_info()) |
- raise |
- |
- # TODO(martiniss) move logic into own module? |
- if trigger_specs: |
- _trigger_builds(step_annotation, trigger_specs) |
- |
- return step_annotation, returncode |
- |
-class RecipeEngine(object): |
- """ |
- Knows how to execute steps emitted by a recipe, holds global state such as |
- step history and build properties. Each recipe module API has a reference to |
- this object. |
- |
- Recipe modules that are aware of the engine: |
- * properties - uses engine.properties. |
- * step_history - uses engine.step_history. |
- * step - uses engine.create_step(...). |
- |
- """ |
- def __init__(self, stream, properties, test_data): |
- self._stream = stream |
- self._properties = properties |
- self._test_data = test_data |
- self._step_history = collections.OrderedDict() |
- |
- self._previous_step_annotation = None |
- self._previous_step_result = None |
- self._api = None |
- |
- @property |
- def properties(self): |
- return self._properties |
- |
- @property |
- def previous_step_result(self): |
- """Allows api.step to get the active result from any context.""" |
- return self._previous_step_result |
- |
- def _emit_results(self): |
- """Internal helper used to emit results.""" |
- annotation = self._previous_step_annotation |
- step_result = self._previous_step_result |
- |
- self._previous_step_annotation = None |
- self._previous_step_result = None |
- |
- if not annotation or not step_result: |
- return |
- |
- step_result.presentation.finalize(annotation) |
- if self._test_data.enabled: |
- val = annotation.stream.getvalue() |
- lines = filter(None, val.splitlines()) |
- if lines: |
- # note that '~' sorts after 'z' so that this will be last on each |
- # step. also use _step to get access to the mutable step |
- # dictionary. |
- # pylint: disable=w0212 |
- step_result._step['~followup_annotations'] = lines |
- annotation.step_ended() |
- |
- def run_step(self, step): |
- """ |
- Runs a step. |
- |
- Args: |
- step: The step to run. |
- |
- Returns: |
- A StepData object containing the result of running the step. |
- """ |
- ok_ret = step.pop('ok_ret') |
- infra_step = step.pop('infra_step') |
- nest_level = step.pop('step_nest_level') |
- |
- test_data_fn = step.pop('step_test_data', recipe_test_api.StepTestData) |
- step_test = self._test_data.pop_step_test_data(step['name'], |
- test_data_fn) |
- placeholders = render_step(step, step_test) |
- |
- self._step_history[step['name']] = step |
- self._emit_results() |
- |
- step_result = None |
- |
- if not self._test_data.enabled: |
- self._previous_step_annotation, retcode = _run_annotated_step( |
- self._stream, nest_level=nest_level, **step) |
- |
- step_result = StepData(step, retcode) |
- self._stream.step_cursor(step['name']) |
- else: |
- self._previous_step_annotation = annotation = self._stream.step( |
- step['name']) |
- annotation.step_started() |
- try: |
- annotation.stream = cStringIO.StringIO() |
- if nest_level: |
- annotation.step_nest_level(nest_level) |
- |
- step_result = StepData(step, step_test.retcode) |
- except OSError: |
- exc_type, exc_value, exc_tb = sys.exc_info() |
- trace = traceback.format_exception(exc_type, exc_value, exc_tb) |
- trace_lines = ''.join(trace).split('\n') |
- annotation.write_log_lines('exception', filter(None, trace_lines)) |
- annotation.step_exception() |
- |
- get_placeholder_results(step_result, placeholders) |
- self._previous_step_result = step_result |
- |
- if step_result.retcode in ok_ret: |
- step_result.presentation.status = 'SUCCESS' |
- return step_result |
- else: |
- if not infra_step: |
- state = 'FAILURE' |
- exc = recipe_api.StepFailure |
- else: |
- state = 'EXCEPTION' |
- exc = recipe_api.InfraFailure |
- |
- step_result.presentation.status = state |
- if step_test.enabled: |
- # To avoid cluttering the expectations, don't emit this in testmode. |
- self._previous_step_annotation.emit( |
- 'step returned non-zero exit code: %d' % step_result.retcode) |
- |
- raise exc(step['name'], step_result) |
- |
- |
- def run(self, steps_function, api, prop_defs): |
- """Run a recipe represented by top level RunSteps function. |
- |
- This function blocks until recipe finishes. |
- |
- Args: |
- steps_function: function that runs the steps. |
- api: The api, with loaded module dependencies. |
- Used by the some special modules. |
- prop_defs: Property definitions for this recipe. |
- |
- Returns: |
- RecipeExecutionResult with status code and list of steps ran. |
- """ |
- self._api = api |
- retcode = None |
- final_result = None |
- |
- try: |
- try: |
- retcode = loader.invoke_with_properties( |
- steps_function, api._engine.properties, prop_defs, api=api) |
- assert retcode is None, ( |
- "Non-None return from RunSteps is not supported yet") |
- |
- assert not self._test_data.enabled or not self._test_data.step_data, ( |
- "Unconsumed test data! %s" % (self._test_data.step_data,)) |
- finally: |
- self._emit_results() |
- except recipe_api.StepFailure as f: |
- retcode = f.retcode or 1 |
- final_result = { |
- "name": "$final_result", |
- "reason": f.reason, |
- "status_code": retcode |
- } |
- except StepDataAttributeError as ex: |
- unexpected_exception = self._test_data.is_unexpected_exception(ex) |
- |
- retcode = -1 |
- final_result = { |
- "name": "$final_result", |
- "reason": "Invalid Step Data Access: %r" % ex, |
- "status_code": retcode |
- } |
- |
- with self._stream.step('Invalid Step Data Access') as s: |
- s.step_exception() |
- s.write_log_lines('exception', traceback.format_exc().splitlines()) |
- |
- if unexpected_exception: |
- raise |
- |
- except Exception as ex: |
- unexpected_exception = self._test_data.is_unexpected_exception(ex) |
- |
- retcode = -1 |
- final_result = { |
- "name": "$final_result", |
- "reason": "Uncaught Exception: %r" % ex, |
- "status_code": retcode |
- } |
- |
- with self._stream.step('Uncaught Exception') as s: |
- s.step_exception() |
- s.write_log_lines('exception', traceback.format_exc().splitlines()) |
- |
- if unexpected_exception: |
- raise |
- |
- if final_result is not None: |
- self._step_history[final_result['name']] = final_result |
- |
- return RecipeExecutionResult(retcode, self._step_history) |
- |
- def create_step(self, step): # pylint: disable=R0201 |
- """Called by step module to instantiate a new step. |
- |
- Args: |
- step: ConfigGroup object with information about the step, see |
- recipe_modules/step/config.py. |
- |
- Returns: |
- Opaque engine specific object that is understood by 'run_steps' method. |
- """ |
- return step.as_jsonish() |
- |
- |