Bug 1047101 - Provide a way to do data-driven tests with Marionette test runner. r=jgriffin
authorJulien Pagès <j.parkouss@gmail.com>
Sat, 30 Aug 2014 09:55:00 -0400
changeset 203413 9906ac54c8fcb541e1132ba5df0d501fd18fe36d
parent 203412 2643d34b1dde22729918a3b52d5c8088f18e83f9
child 203414 128ee74ae45d4c7bbb4d02d8bd59b8a5d5487250
push id27425
push userryanvm@gmail.com
push dateWed, 03 Sep 2014 20:38:59 +0000
treeherdermozilla-central@acbdce59da2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjgriffin
bugs1047101
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1047101 - Provide a way to do data-driven tests with Marionette test runner. r=jgriffin
testing/marionette/client/marionette/marionette_test.py
testing/marionette/client/marionette/tests/unit/test_data_driven.py
testing/marionette/client/marionette/tests/unit/unit-tests.ini
--- a/testing/marionette/client/marionette/marionette_test.py
+++ b/testing/marionette/client/marionette/marionette_test.py
@@ -80,18 +80,111 @@ def expectedFailure(func):
 def skip_if_b2g(target):
     def wrapper(self, *args, **kwargs):
         if not hasattr(self.marionette, 'b2g') or not self.marionette.b2g:
             return target(self, *args, **kwargs)
         else:
             sys.stderr.write('skipping ... ')
     return wrapper
 
+def parameterized(func_suffix, *args, **kwargs):
+    """
+    A decorator that can generate methods given a base method and some data.
+
+    **func_suffix** is used as a suffix for the new created method and must be
+    unique given a base method. if **func_suffix** countains characters that
+    are not allowed in normal python function name, these characters will be
+    replaced with "_".
+
+    This decorator can be used more than once on a single base method. The class
+    must have a metaclass of :class:`MetaParameterized`.
+
+    Example::
+
+      # This example will generate two methods:
+      #
+      # - MyTestCase.test_it_1
+      # - MyTestCase.test_it_2
+      #
+      class MyTestCase(MarionetteTestCase):
+          @parameterized("1", 5, named='name')
+          @parameterized("2", 6, named='name2')
+          def test_it(self, value, named=None):
+              print value, named
+
+    :param func_suffix: will be used as a suffix for the new method
+    :param \*args: arguments to pass to the new method
+    :param \*\*kwargs: named arguments to pass to the new method
+    """
+    def wrapped(func):
+        if not hasattr(func, 'metaparameters'):
+            func.metaparameters = []
+        func.metaparameters.append((func_suffix, args, kwargs))
+        return func
+    return wrapped
+
+def with_parameters(parameters):
+    """
+    A decorator that can generate methods given a base method and some data.
+    Acts like :func:`parameterized`, but define all methods in one call.
+
+    Example::
+
+      # This example will generate two methods:
+      #
+      # - MyTestCase.test_it_1
+      # - MyTestCase.test_it_2
+      #
+
+      DATA = [("1", [5], {'named':'name'}), ("2", [6], {'named':'name2'})]
+
+      class MyTestCase(MarionetteTestCase):
+          @with_parameters(DATA)
+          def test_it(self, value, named=None):
+              print value, named
+
+    :param parameters: list of tuples (**func_suffix**, **args**, **kwargs**)
+                       defining parameters like in :func:`todo`.
+    """
+    def wrapped(func):
+        func.metaparameters = parameters
+        return func
+    return wrapped
+
+def wraps_parameterized(func, func_suffix, args, kwargs):
+    """Internal: for MetaParameterized"""
+    def wrapper(self):
+        return func(self, *args, **kwargs)
+    wrapper.__name__ = func.__name__ + '_' + str(func_suffix)
+    wrapper.__doc__ = '[%s] %s' % (func_suffix, func.__doc__)
+    return wrapper
+
+class MetaParameterized(type):
+    """
+    A metaclass that allow a class to use decorators like :func:`parameterized`
+    or :func:`with_parameters` to generate new methods.
+    """
+    RE_ESCAPE_BAD_CHARS = re.compile(r'[\.\(\) -/]')
+    def __new__(cls, name, bases, attrs):
+        for k, v in attrs.items():
+            if callable(v) and hasattr(v, 'metaparameters'):
+                for func_suffix, args, kwargs in v.metaparameters:
+                    func_suffix = cls.RE_ESCAPE_BAD_CHARS.sub('_', func_suffix)
+                    wrapper = wraps_parameterized(v, func_suffix, args, kwargs)
+                    if wrapper.__name__ in attrs:
+                        raise KeyError("%s is already a defined method on %s" %
+                                        (wrapper.__name__, name))
+                    attrs[wrapper.__name__] = wrapper
+                del attrs[k]
+
+        return type.__new__(cls, name, bases, attrs)
+
 class CommonTestCase(unittest.TestCase):
 
+    __metaclass__ = MetaParameterized
     match_re = None
     failureException = AssertionError
 
     def __init__(self, methodName, **kwargs):
         unittest.TestCase.__init__(self, methodName)
         self.loglines = []
         self.duration = 0
         self.start_time = 0
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_data_driven.py
@@ -0,0 +1,59 @@
+from marionette_test import parameterized, with_parameters, MetaParameterized, \
+                            MarionetteTestCase
+
+class Parameterizable(object):
+    __metaclass__ = MetaParameterized
+
+class TestDataDriven(MarionetteTestCase):
+    def test_parameterized(self):
+        class Test(Parameterizable):
+            def __init__(self):
+                self.parameters = []
+
+            @parameterized('1', 'thing', named=43)
+            @parameterized('2', 'thing2')
+            def test(self, thing, named=None):
+                self.parameters.append((thing, named))
+
+        self.assertFalse(hasattr(Test, 'test'))
+        self.assertTrue(hasattr(Test, 'test_1'))
+        self.assertTrue(hasattr(Test, 'test_2'))
+
+        test = Test()
+        test.test_1()
+        test.test_2()
+
+        self.assertEquals(test.parameters, [('thing', 43), ('thing2', None)])
+
+    def test_with_parameters(self):
+        DATA = [('1', ('thing',), {'named': 43}),
+                ('2', ('thing2',), {'named': None})]
+
+        class Test(Parameterizable):
+            def __init__(self):
+                self.parameters = []
+
+            @with_parameters(DATA)
+            def test(self, thing, named=None):
+                self.parameters.append((thing, named))
+
+        self.assertFalse(hasattr(Test, 'test'))
+        self.assertTrue(hasattr(Test, 'test_1'))
+        self.assertTrue(hasattr(Test, 'test_2'))
+
+        test = Test()
+        test.test_1()
+        test.test_2()
+
+        self.assertEquals(test.parameters, [('thing', 43), ('thing2', None)])
+
+    def test_parameterized_same_name_raises_error(self):
+        with self.assertRaises(KeyError):
+            class Test(Parameterizable):
+                @parameterized('1', 'thing', named=43)
+                @parameterized('1', 'thing2')
+                def test(self, thing, named=None):
+                    pass
+
+    def test_marionette_test_case_is_parameterizable(self):
+        self.assertTrue(issubclass(MarionetteTestCase.__metaclass__, MetaParameterized))
--- a/testing/marionette/client/marionette/tests/unit/unit-tests.ini
+++ b/testing/marionette/client/marionette/tests/unit/unit-tests.ini
@@ -6,16 +6,17 @@ qemu = false
 browser = true
 
 ; true if the test is compatible with b2g, otherwise false
 b2g = true
 
 ; true if the test should be skipped
 skip = false
 
+[test_data_driven.py]
 [test_session.py]
 [test_capabilities.py]
 
 [test_expectedfail.py]
 expected = fail
 [test_import_script.py]
 [test_import_script_reuse_window.py]
 b2g = false