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

Unified Diff: third_party/recipe_engine/recipe_test_api.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/recipe_api.py ('k') | third_party/recipe_engine/simulation_test.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
« no previous file with comments | « third_party/recipe_engine/recipe_api.py ('k') | third_party/recipe_engine/simulation_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698