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 |