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

Unified Diff: third_party/recipe_engine/main.py

Issue 1347263002: Revert of Cross-repo recipe package system. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: 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 side-by-side diff with in-line comments
Download patch
« 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 »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: third_party/recipe_engine/main.py
diff --git a/third_party/recipe_engine/main.py b/third_party/recipe_engine/main.py
new file mode 100755
index 0000000000000000000000000000000000000000..97d0fb5c092bce0e824ca8f7f4ab896e1f8e49ad
--- /dev/null
+++ b/third_party/recipe_engine/main.py
@@ -0,0 +1,939 @@
+# 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()
+
+
« 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