| Index: third_party/recipe_engine/recipe_test_api.py
|
| diff --git a/third_party/recipe_engine/recipe_test_api.py b/third_party/recipe_engine/recipe_test_api.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..2ea3791725a276ddffca3035baf47a652db7bc92
|
| --- /dev/null
|
| +++ b/third_party/recipe_engine/recipe_test_api.py
|
| @@ -0,0 +1,462 @@
|
| +# Copyright 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.
|
| +
|
| +import collections
|
| +
|
| +from .util import ModuleInjectionSite, static_call, static_wraps
|
| +
|
| +def combineify(name, dest, a, b):
|
| + """
|
| + Combines dictionary members in two objects into a third one using addition.
|
| +
|
| + Args:
|
| + name - the name of the member
|
| + dest - the destination object
|
| + a - the first source object
|
| + b - the second source object
|
| + """
|
| + dest_dict = getattr(dest, name)
|
| + dest_dict.update(getattr(a, name))
|
| + for k, v in getattr(b, name).iteritems():
|
| + if k in dest_dict:
|
| + dest_dict[k] += v
|
| + else:
|
| + dest_dict[k] = v
|
| +
|
| +
|
| +class BaseTestData(object):
|
| + def __init__(self, enabled=True):
|
| + super(BaseTestData, self).__init__()
|
| + self._enabled = enabled
|
| +
|
| + @property
|
| + def enabled(self):
|
| + return self._enabled
|
| +
|
| +
|
| +class PlaceholderTestData(BaseTestData):
|
| + def __init__(self, data=None):
|
| + super(PlaceholderTestData, self).__init__()
|
| + self.data = data
|
| +
|
| + def __repr__(self):
|
| + return "PlaceholderTestData(%r)" % (self.data,)
|
| +
|
| +
|
| +class StepTestData(BaseTestData):
|
| + """
|
| + Mutable container for per-step test data.
|
| +
|
| + This data is consumed while running the recipe (during
|
| + annotated_run.run_steps).
|
| + """
|
| + def __init__(self):
|
| + super(StepTestData, self).__init__()
|
| + # { (module, placeholder) -> [data] }
|
| + self.placeholder_data = collections.defaultdict(list)
|
| + self.override = False
|
| + self._stdout = None
|
| + self._stderr = None
|
| + self._retcode = None
|
| +
|
| + def __add__(self, other):
|
| + assert isinstance(other, StepTestData)
|
| +
|
| + if other.override:
|
| + return other
|
| +
|
| + ret = StepTestData()
|
| +
|
| + combineify('placeholder_data', ret, self, other)
|
| +
|
| + # pylint: disable=W0212
|
| + ret._stdout = other._stdout or self._stdout
|
| + ret._stderr = other._stderr or self._stderr
|
| + ret._retcode = self._retcode
|
| + if other._retcode is not None:
|
| + assert ret._retcode is None
|
| + ret._retcode = other._retcode
|
| +
|
| + return ret
|
| +
|
| + def unwrap_placeholder(self):
|
| + # {(module, placeholder): [data]} => data.
|
| + assert len(self.placeholder_data) == 1
|
| + data_list = self.placeholder_data.items()[0][1]
|
| + assert len(data_list) == 1
|
| + return data_list[0]
|
| +
|
| + def pop_placeholder(self, name_pieces):
|
| + l = self.placeholder_data[name_pieces]
|
| + if l:
|
| + return l.pop(0)
|
| + else:
|
| + return PlaceholderTestData()
|
| +
|
| + @property
|
| + def retcode(self): # pylint: disable=E0202
|
| + return self._retcode or 0
|
| +
|
| + @retcode.setter
|
| + def retcode(self, value): # pylint: disable=E0202
|
| + self._retcode = value
|
| +
|
| + @property
|
| + def stdout(self):
|
| + return self._stdout or PlaceholderTestData(None)
|
| +
|
| + @stdout.setter
|
| + def stdout(self, value):
|
| + assert isinstance(value, PlaceholderTestData)
|
| + self._stdout = value
|
| +
|
| + @property
|
| + def stderr(self):
|
| + return self._stderr or PlaceholderTestData(None)
|
| +
|
| + @stderr.setter
|
| + def stderr(self, value):
|
| + assert isinstance(value, PlaceholderTestData)
|
| + self._stderr = value
|
| +
|
| + @property
|
| + def stdin(self): # pylint: disable=R0201
|
| + return PlaceholderTestData(None)
|
| +
|
| + def __repr__(self):
|
| + return "StepTestData(%r)" % ({
|
| + 'placeholder_data': dict(self.placeholder_data.iteritems()),
|
| + 'stdout': self._stdout,
|
| + 'stderr': self._stderr,
|
| + 'retcode': self._retcode,
|
| + 'override': self.override,
|
| + },)
|
| +
|
| +
|
| +class ModuleTestData(BaseTestData, dict):
|
| + """
|
| + Mutable container for test data for a specific module.
|
| +
|
| + This test data is consumed at module load time (i.e. when create_recipe_api
|
| + runs).
|
| + """
|
| + def __add__(self, other):
|
| + assert isinstance(other, ModuleTestData)
|
| + ret = ModuleTestData()
|
| + ret.update(self)
|
| + ret.update(other)
|
| + return ret
|
| +
|
| + def __repr__(self):
|
| + return "ModuleTestData(%r)" % super(ModuleTestData, self).__repr__()
|
| +
|
| +
|
| +class TestData(BaseTestData):
|
| + def __init__(self, name=None):
|
| + super(TestData, self).__init__()
|
| + self.name = name
|
| + self.properties = {} # key -> val
|
| + self.mod_data = collections.defaultdict(ModuleTestData)
|
| + self.step_data = collections.defaultdict(StepTestData)
|
| + self.expected_exception = None
|
| +
|
| + def __add__(self, other):
|
| + assert isinstance(other, TestData)
|
| + ret = TestData(self.name or other.name)
|
| +
|
| + ret.properties.update(self.properties)
|
| + ret.properties.update(other.properties)
|
| +
|
| + combineify('mod_data', ret, self, other)
|
| + combineify('step_data', ret, self, other)
|
| + ret.expected_exception = self.expected_exception
|
| + if other.expected_exception:
|
| + ret.expected_exception = other.expected_exception
|
| +
|
| + return ret
|
| +
|
| + def empty(self):
|
| + return not self.step_data
|
| +
|
| + def pop_step_test_data(self, step_name, step_test_data_fn):
|
| + step_test_data = step_test_data_fn()
|
| + if step_name in self.step_data:
|
| + step_test_data += self.step_data.pop(step_name)
|
| + return step_test_data
|
| +
|
| + def get_module_test_data(self, module_name):
|
| + return self.mod_data.get(module_name, ModuleTestData())
|
| +
|
| + def expect_exception(self, exception):
|
| + assert not self.expected_exception
|
| + self.expected_exception = exception
|
| +
|
| + def is_unexpected_exception(self, exception):
|
| + name = exception.__class__.__name__
|
| + return not (self.enabled and name == self.expected_exception)
|
| +
|
| + def __repr__(self):
|
| + return "TestData(%r)" % ({
|
| + 'name': self.name,
|
| + 'properties': self.properties,
|
| + 'mod_data': dict(self.mod_data.iteritems()),
|
| + 'step_data': dict(self.step_data.iteritems()),
|
| + 'expected_exception': self.expected_exception,
|
| + },)
|
| +
|
| +
|
| +class DisabledTestData(BaseTestData):
|
| + def __init__(self):
|
| + super(DisabledTestData, self).__init__(False)
|
| +
|
| + def __getattr__(self, name):
|
| + return self
|
| +
|
| + def pop_placeholder(self, _name_pieces):
|
| + return self
|
| +
|
| + def pop_step_test_data(self, _step_name, _step_test_data_fn):
|
| + return self
|
| +
|
| + def get_module_test_data(self, _module_name):
|
| + return self
|
| +
|
| + def is_unexpected_exception(self, exception): #pylint: disable=R0201
|
| + return False
|
| +
|
| +def mod_test_data(func):
|
| + @static_wraps(func)
|
| + def inner(self, *args, **kwargs):
|
| + assert isinstance(self, RecipeTestApi)
|
| + mod_name = self._module.NAME # pylint: disable=W0212
|
| + ret = TestData(None)
|
| + data = static_call(self, func, *args, **kwargs)
|
| + ret.mod_data[mod_name][inner.__name__] = data
|
| + return ret
|
| + return inner
|
| +
|
| +
|
| +def placeholder_step_data(func):
|
| + """Decorates RecipeTestApi member functions to allow those functions to
|
| + return just the placeholder data, instead of the normally required
|
| + StepTestData() object.
|
| +
|
| + The wrapped function may return either:
|
| + * <placeholder data>, <retcode or None>
|
| + * StepTestData containing exactly one PlaceholderTestData and possible a
|
| + retcode. This is useful for returning the result of another method which
|
| + is wrapped with placeholder_step_data.
|
| +
|
| + In either case, the wrapper function will return a StepTestData object with
|
| + the retcode and placeholder datum inserted with a name of:
|
| + (<Test module name>, <wrapped function name>)
|
| +
|
| + Say you had a 'foo_module' with the following RecipeTestApi:
|
| + class FooTestApi(RecipeTestApi):
|
| + @placeholder_step_data
|
| + @staticmethod
|
| + def cool_method(data, retcode=None):
|
| + return ("Test data (%s)" % data), retcode
|
| +
|
| + @placeholder_step_data
|
| + def other_method(self, retcode=None):
|
| + return self.cool_method('hammer time', retcode)
|
| +
|
| + Code calling cool_method('hello') would get a StepTestData:
|
| + StepTestData(
|
| + placeholder_data = {
|
| + ('foo_module', 'cool_method'): [
|
| + PlaceholderTestData('Test data (hello)')
|
| + ]
|
| + },
|
| + retcode = None
|
| + )
|
| +
|
| + Code calling other_method(50) would get a StepTestData:
|
| + StepTestData(
|
| + placeholder_data = {
|
| + ('foo_module', 'other_method'): [
|
| + PlaceholderTestData('Test data (hammer time)')
|
| + ]
|
| + },
|
| + retcode = 50
|
| + )
|
| + """
|
| + @static_wraps(func)
|
| + def inner(self, *args, **kwargs):
|
| + assert isinstance(self, RecipeTestApi)
|
| + mod_name = self._module.NAME # pylint: disable=W0212
|
| + data = static_call(self, func, *args, **kwargs)
|
| + if isinstance(data, StepTestData):
|
| + all_data = [i
|
| + for l in data.placeholder_data.values()
|
| + for i in l]
|
| + assert len(all_data) == 1, (
|
| + 'placeholder_step_data is only expecting a single placeholder datum. '
|
| + 'Got: %r' % data
|
| + )
|
| + placeholder_data, retcode = all_data[0], data.retcode
|
| + else:
|
| + placeholder_data, retcode = data
|
| + placeholder_data = PlaceholderTestData(placeholder_data)
|
| +
|
| + ret = StepTestData()
|
| + ret.placeholder_data[(mod_name, inner.__name__)].append(placeholder_data)
|
| + ret.retcode = retcode
|
| + return ret
|
| + return inner
|
| +
|
| +
|
| +class RecipeTestApi(object):
|
| + """Provides testing interface for GenTest method.
|
| +
|
| + There are two primary components to the test api:
|
| + * Test data creation methods (test and step_data)
|
| + * test_api's from all the modules in DEPS.
|
| +
|
| + Every test in GenTests(api) takes the form:
|
| + yield <instance of TestData>
|
| +
|
| + There are 4 basic pieces to TestData:
|
| + name - The name of the test.
|
| + properties - Simple key-value dictionary which is used as the combined
|
| + build_properties and factory_properties for this test.
|
| + mod_data - Module-specific testing data (see the platform module for a
|
| + good example). This is testing data which is only used once at
|
| + the start of the execution of the recipe. Modules should
|
| + provide methods to get their specific test information. See
|
| + the platform module's test_api for a good example of this.
|
| + step_data - Step-specific data. There are two major components to this.
|
| + retcode - The return code of the step
|
| + placeholder_data - A mapping from placeholder name to the a list of
|
| + PlaceholderTestData objects, one for each instance
|
| + of that kind of Placeholder in the step.
|
| + stdout, stderr - PlaceholderTestData objects for stdout and stderr.
|
| +
|
| + TestData objects are concatenatable, so it's convenient to phrase test cases
|
| + as a series of added TestData objects. For example:
|
| + DEPS = ['properties', 'platform', 'json']
|
| + def GenTests(api):
|
| + yield (
|
| + api.test('try_win64') +
|
| + api.properties.tryserver(power_level=9001) +
|
| + api.platform('win', 64) +
|
| + api.step_data(
|
| + 'some_step',
|
| + api.json.output("bobface"),
|
| + api.json.output({'key': 'value'})
|
| + )
|
| + )
|
| +
|
| + This example would run a single test (named 'try_win64') with the standard
|
| + tryserver properties (plus an extra property 'power_level' whose value was
|
| + over 9000). The test would run as if it were being run on a 64-bit windows
|
| + installation, and the step named 'some_step' would have its first json output
|
| + placeholder be mocked to return '"bobface"', and its second json output
|
| + placeholder be mocked to return '{"key": "value"}'.
|
| +
|
| + The properties.tryserver() call is documented in the 'properties' module's
|
| + test_api.
|
| + The platform() call is documented in the 'platform' module's test_api.
|
| + The json.output() call is documented in the 'json' module's test_api.
|
| + """
|
| + def __init__(self, module=None):
|
| + """Note: Injected dependencies are NOT available in __init__()."""
|
| + # If we're the 'root' api, inject directly into 'self'.
|
| + # Otherwise inject into 'self.m'
|
| + self.m = self if module is None else ModuleInjectionSite()
|
| + self._module = module
|
| +
|
| + @staticmethod
|
| + def test(name):
|
| + """Returns a new empty TestData with the name filled in.
|
| +
|
| + Use in GenTests:
|
| + def GenTests(api):
|
| + yield api.test('basic')
|
| + """
|
| + return TestData(name)
|
| +
|
| + @staticmethod
|
| + def empty_test_data():
|
| + """Returns a TestData with no information.
|
| +
|
| + This is the identity of the + operator for combining TestData.
|
| + """
|
| + return TestData(None)
|
| +
|
| + @staticmethod
|
| + def _step_data(name, *data, **kwargs):
|
| + """Returns a new TestData with the mock data filled in for a single step.
|
| +
|
| + Used by step_data and override_step_data.
|
| +
|
| + Args:
|
| + name - The name of the step we're providing data for
|
| + data - Zero or more StepTestData objects. These may fill in placeholder
|
| + data for zero or more modules, as well as possibly setting the
|
| + retcode for this step.
|
| + retcode=(int or None) - Override the retcode for this step, even if it
|
| + was set by |data|. This must be set as a keyword arg.
|
| + stdout - StepTestData object with placeholder data for a step's stdout.
|
| + stderr - StepTestData object with placeholder data for a step's stderr.
|
| + override=(bool) - This step data completely replaces any previously
|
| + generated step data, instead of adding on to it.
|
| +
|
| + Use in GenTests:
|
| + # Hypothetically, suppose that your recipe has default test data for two
|
| + # steps 'init' and 'sync' (probably via recipe_api.inject_test_data()).
|
| + # For this example, lets say that the default test data looks like:
|
| + # api.step_data('init', api.json.output({'some': ["cool", "json"]}))
|
| + # AND
|
| + # api.step_data('sync', api.json.output({'src': {'rev': 100}}))
|
| + # Then, your GenTests code may augment or replace this data like:
|
| +
|
| + def GenTests(api):
|
| + yield (
|
| + api.test('more') +
|
| + api.step_data( # Adds step data for a step with no default test data
|
| + 'mystep',
|
| + api.json.output({'legend': ['...', 'DARY!']})
|
| + ) +
|
| + api.step_data( # Adds retcode to default step_data for this step
|
| + 'init',
|
| + retcode=1
|
| + ) +
|
| + api.override_step_data( # Removes json output and overrides retcode
|
| + 'sync',
|
| + retcode=100
|
| + )
|
| + )
|
| + """
|
| + assert all(isinstance(d, StepTestData) for d in data)
|
| + ret = TestData(None)
|
| + if data:
|
| + ret.step_data[name] = reduce(sum, data)
|
| + if 'retcode' in kwargs:
|
| + ret.step_data[name].retcode = kwargs['retcode']
|
| + if 'override' in kwargs:
|
| + ret.step_data[name].override = kwargs['override']
|
| + for key in ('stdout', 'stderr'):
|
| + if key in kwargs:
|
| + stdio_test_data = kwargs[key]
|
| + assert isinstance(stdio_test_data, StepTestData)
|
| + setattr(ret.step_data[name], key, stdio_test_data.unwrap_placeholder())
|
| + return ret
|
| +
|
| + def step_data(self, name, *data, **kwargs):
|
| + """See _step_data()"""
|
| + return self._step_data(name, *data, **kwargs)
|
| + step_data.__doc__ = _step_data.__doc__
|
| +
|
| + def override_step_data(self, name, *data, **kwargs):
|
| + """See _step_data()"""
|
| + kwargs['override'] = True
|
| + return self._step_data(name, *data, **kwargs)
|
| + override_step_data.__doc__ = _step_data.__doc__
|
| +
|
| + def expect_exception(self, exc_type): #pylint: disable=R0201
|
| + ret = TestData(None)
|
| + ret.expect_exception(exc_type)
|
| + return ret
|
|
|