blob: 2137124b7b10c216e4429cb8756f2d853034b3b4 [file] [log] [blame]
# Copyright 2019 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.
import attr
from google.protobuf.message import Message
from ..attr_util import attr_list_type, attr_type, attr_dict_type
from ..stream import StreamEngine
from ...recipe_test_api import BaseTestData
from ...step_data import ExecutionResult
@attr.s(frozen=True)
class Step:
"""Step is the full definition of a step to run for a StepRunner."""
# The full command line as a list of strings. cmd0 will be an absolute path to
# an executable.
cmd = attr.ib(validator=attr_list_type(str))
# The absolute path for the step's current working directory.
cwd = attr.ib(validator=attr_type(str))
# File path or None. If None, stdin for the subprocess should be closed.
stdin = attr.ib(validator=attr_type((str, type(None))))
# File path, Stream or a file descriptor.
#
# Note that Streams may return a file .fileno() if they support subprocess
# redirection. Otherwise the step runner implementation is expected to read
# the output from the step and then write it into the Stream.
stdout = attr.ib(validator=attr_type((str, StreamEngine.Stream)))
stderr = attr.ib(validator=attr_type((str, StreamEngine.Stream)))
# The full environment that this step should execute with.
env = attr.ib(validator=attr_dict_type(str, str))
# The sectionname->Message mapping of LUCI_CONTEXT modifications.
luci_context = attr.ib(validator=attr_dict_type(str, Message))
class StepRunner:
"""A StepRunner is the interface to actually run steps and resolve
placeholders.
NONE of the methods in this class should raise exceptions. If they do, it will
be treated as an Engine Crash and the whole recipe will be aborted (with
appropriate logging, of course).
"""
# pylint: disable=no-self-use
# pylint: disable=unused-argument
def register_step_config(self, name_token, step_config):
"""Called to register the precursor of the step (the StepConfig).
Only used for the simulation API.
TODO(iannucci): Change all step expectations to instead reflect the engine's
intent (i.e. the Step object passed to `run`). Currently this is used to
provide env_prefixes, env_suffixes as distinct from env. However, it may be
"just fine" to instead only record env in the test expectations (i.e. using
FakeEnviron as a basis environment).
Args:
* name_tokens (List[str]) - The full name of the step.
* step_config (StepConfig) - The full precursor of the step.
"""
pass
def placeholder(self, name_tokens, placeholder):
"""Returns PlaceholderTestData for the given step and placeholder
combination.
Note: This may be called multiple times for the same step/placeholder
combination. It should always return the same test data.
Args:
* name_tokens (List[str]) - The full name of the step.
* placeholder (Placeholder) - The actual placeholder to resolve for.
This may inspect the placeholder's namespaces and/or name.
Returns PlaceholderTestData (or BaseTestData with enabled=False).
"""
return BaseTestData(False)
def handle_placeholder(self, name_tokens, handle_name):
"""Returns PlaceholderTestData for the given step and handle name
combination.
Note: This may be called multiple times for the same step/handle
combination. It should always return the same test data.
Args:
* name_tokens (List[str]) - The full name of the step.
* handle_name ('stdout'|'stderr'|'stdin') - The name of the handle we're
inquiring for.
Returns PlaceholderTestData (or BaseTestData with enabled=False).
"""
return BaseTestData(False)
def isabs(self, name_tokens, path):
"""Return True iff `path` is os.path.isabs."""
return True
def isdir(self, name_tokens, path):
"""Return True iff `path` is os.path.isdir."""
return True
def access(self, name_tokens, path, mode):
"""Return True iff `path` is os.access(path, mode)."""
return True
def resolve_cmd0(self, name_tokens, debug_log, cmd0, cwd, paths):
"""Should resolve the 0th argument of the command (`cmd0`) to an absolute
path to the intended executable.
Args:
* name_tokens (List[str]) - The full name of the step.
* debug_log (Stream) - The log where debugging information about the
StepRunner's thought process should go.
* cmd0 (str) - The executable to resolve. Note that this may be a relative
path (e.g. './foo').
* cwd (str) - The absolute cwd for the step.
* paths (List[str]) - The current split value of $PATH.
Returns the absolute path to the intended executable, if found, or None if
it couldn't be discovered.
"""
return cmd0
def now(self):
"""Should return time.time().
Used as the basis for adjusting the LUCI_CONTEXT['deadline'] section with
the step's timeout.
"""
raise NotImplementedError()
def write_luci_context(self, section_values):
"""Writes a mapping of str->dict to disk (as a temp file), returning that
path.
`section_values` represents the 'diff' against LUCI_CONTEXT for the
recipe engine process. The standard LUCI_CONTEXT merge rules should apply.
"""
raise NotImplementedError()
def run(self, name_tokens, debug_log, step):
"""Runs the step defined by step_config.
Args:
* name_tokens (List[str]) - The full name of the step.
* debug_log (Stream) - The log where debugging information about the
StepRunner's thought process should go.
* step (Step) - The step to run.
Returns recipe_engine.step_data.ExecutionResult.
"""
raise NotImplementedError()
def run_noop(self, name_tokens, debug_log):
"""Runs a no-op step.
This may occur because the recipe needs to establish some step for UI
purposes, but is also used for some recipes which run test steps without
actual content (and so the simulations need an API point to return mocked
ExecutionResult data).
Args:
* name_tokens (List[str]) - The full name of the step.
* debug_log (Stream) - The log where debugging information about the
StepRunner's thought process should go.
Returns recipe_engine.step_data.ExecutionResult.
"""
return ExecutionResult(retcode=0)