author | Julien Pagès <j.parkouss@gmail.com> |
Sat, 30 Aug 2014 09:55:00 -0400 | |
changeset 203413 | 9906ac54c8fcb541e1132ba5df0d501fd18fe36d |
parent 203412 | 2643d34b1dde22729918a3b52d5c8088f18e83f9 |
child 203414 | 128ee74ae45d4c7bbb4d02d8bd59b8a5d5487250 |
push id | 27425 |
push user | ryanvm@gmail.com |
push date | Wed, 03 Sep 2014 20:38:59 +0000 |
treeherder | mozilla-central@acbdce59da2f [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | jgriffin |
bugs | 1047101 |
milestone | 35.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
|
--- 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