Index: scripts/slave/recipe_test_api.py |
diff --git a/scripts/slave/recipe_test_api.py b/scripts/slave/recipe_test_api.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..2c24cd2abca4cba5313fe72ad75f67a0388d3558 |
--- /dev/null |
+++ b/scripts/slave/recipe_test_api.py |
@@ -0,0 +1,225 @@ |
+import collections |
+import functools |
+ |
+from .recipe_util import ModuleInjectionSite |
+ |
+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 PlaceholderTestData(object): |
+ def __init__(self, data=None): |
+ self.data = data |
+ self.enabled = True |
agable
2013/09/21 02:05:31
What's enabled for?
iannucci
2013/09/21 03:12:34
Any time a TestData-ish object is passed to anythi
|
+ |
+ def __repr__(self): |
+ return "PlaceholderTestData(%s)" % self.data |
+ |
+ |
+class StepTestData(object): |
+ """ |
+ Mutable container for per-step test data. |
+ |
+ This data is consumed while running the recipe (during |
+ annotated_run.run_steps). |
+ """ |
+ def __init__(self): |
+ # { (module, placeholder) -> [data] } |
+ self.placeholder_data = collections.defaultdict(list) |
+ self._retcode = None |
+ self.enabled = True |
+ |
+ def __add__(self, other): |
agable
2013/09/21 02:05:31
__add__ doesn't have to be simple concatenation. W
iannucci
2013/09/21 03:12:34
So for StepTestData, there are two axes of concate
agable
2013/09/23 22:57:58
I still don't really like this. If I have { (a): [
|
+ assert isinstance(other, StepTestData) |
+ ret = StepTestData() |
+ |
+ # We don't use combineify to merge placeholder_data here because |
+ # simply concatenating placeholder_data's list value is not meaningful to |
+ # consumers of this object. |
+ # Producers of this object should use the append() method. |
+ ret.placeholder_data.update(self.placeholder_data) |
+ for k, v in other.placeholder_data.iteritems(): |
+ assert k not in ret.placeholder_data |
+ ret.placeholder_data[k] = v |
+ |
+ # pylint: disable=W0212 |
+ ret._retcode = self._retcode |
+ if other._retcode is not None: |
+ assert ret._retcode is None |
+ ret._retcode = other._retcode |
+ return ret |
+ |
+ def append(self, other): |
+ self._retcode = self._retcode or other._retcode # pylint: disable=W0212 |
+ combineify('placeholder_data', self, self, other) |
+ return self |
+ |
+ 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 |
+ |
+ def __repr__(self): |
+ return "StepTestData(%s)" % str({ |
+ 'placeholder_data': dict(self.placeholder_data.iteritems()), |
+ 'retcode': self._retcode, |
+ }) |
+ |
+ |
+class ModuleTestData(dict): |
+ """ |
+ Mutable container for test data for a specific module. |
+ |
+ This test data is consumed at module load time (i.e. when CreateRecipeApi |
+ runs). |
+ """ |
+ def __init__(self): |
+ super(ModuleTestData, self).__init__() |
+ self.enabled = True |
+ |
+ def __add__(self, other): |
+ assert isinstance(other, ModuleTestData) |
+ ret = ModuleTestData() |
+ ret.update(self) |
+ ret.update(other) |
+ return ret |
+ |
+ def __repr__(self): |
+ return "ModuleTestData(%s)" % super(ModuleTestData, self).__repr__() |
+ |
+ |
+class TestData(object): |
+ def __init__(self, name=None): |
+ self.name = name |
+ self.properties = {} # key -> val |
+ self.mod_data = collections.defaultdict(ModuleTestData) |
+ self.step_data = collections.defaultdict(StepTestData) |
+ self.enabled = True |
+ |
+ 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) |
+ |
+ return ret |
+ |
+ def empty(self): |
+ return not self.step_data |
+ |
+ def __repr__(self): |
+ return "TestData(%s)" % str({ |
+ 'name': self.name, |
+ 'properties': self.properties, |
+ 'mod_data': dict(self.mod_data.iteritems()), |
+ 'step_data': dict(self.step_data.iteritems()), |
+ }) |
+ |
+ |
+class DisabledTestData(object): |
+ def __init__(self): |
+ self.enabled = False |
+ |
+ def __getattr__(self, name): |
+ return self |
+ |
+ def pop_placeholder(self, _name_pieces): |
+ return self |
+ |
+ |
+def static_wraps(func): |
+ wrapped_fn = func |
+ if isinstance(func, staticmethod): |
+ wrapped_fn = func.__func__ |
+ return functools.wraps(wrapped_fn) |
+ |
+ |
+def static_call(obj, func, *args, **kwargs): |
+ if isinstance(func, staticmethod): |
+ return func.__get__(obj)(*args, **kwargs) |
+ else: |
+ return func(obj, *args, **kwargs) |
+ |
+ |
+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): |
+ @static_wraps(func) |
+ def inner(self, *args, **kwargs): |
+ assert isinstance(self, RecipeTestApi) |
+ mod_name = self._module.NAME # pylint: disable=W0212 |
+ ret = StepTestData() |
+ placeholder_data, retcode = static_call(self, func, *args, **kwargs) |
+ ret.placeholder_data[(mod_name, inner.__name__)].append( |
+ PlaceholderTestData(placeholder_data)) |
+ ret.retcode = retcode |
+ return ret |
+ return inner |
+ |
+ |
+class RecipeTestApi(object): |
+ def __init__(self, module=None, test_data=DisabledTestData()): |
+ """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 |
+ |
+ assert isinstance(test_data, (ModuleTestData, DisabledTestData)) |
+ self._test_data = test_data |
+ |
+ @staticmethod |
+ def Test(name): |
agable
2013/09/21 02:05:31
The capitalization in these is disconcerting compa
iannucci
2013/09/21 03:12:34
yep, you're right. This is vestigial. Fixed (and n
|
+ return TestData(name) |
+ |
+ @staticmethod |
+ def Properties(**kwargs): |
+ ret = TestData(None) |
+ ret.properties.update(kwargs) |
+ return ret |
+ |
+ @staticmethod |
+ def StepData(name, *data): |
+ assert all(isinstance(d, StepTestData) for d in data) |
+ ret = TestData(None) |
+ ret.step_data[name] = reduce(sum, data) |
+ return ret |