diff --git a/doc/test/funcargs.txt b/doc/test/funcargs.txt index 8af832c44..023818c2e 100644 --- a/doc/test/funcargs.txt +++ b/doc/test/funcargs.txt @@ -50,21 +50,20 @@ is an example for running the same test function three times. .. sourcecode:: python - def pytest_genfuncruns(runspec): - if "arg1" in runspec.funcargnames: - runspec.addfuncarg("arg1", 10) - runspec.addfuncarg("arg1", 20) - runspec.addfuncarg("arg1", 30) + def pytest_genfunc(funcspec): + if "arg1" in funcspec.funcargnames: + for value in range(3): + funcspec.addcall(arg1=value) def test_function(arg1): - assert myfuncarg in (10, 20, 30) + assert myfuncarg in (0, 1, 2) Here is what happens: -1. The ``pytest_genfuncruns()`` hook will be called once for each test - function. The if-statement makes sure that we only add function - arguments (and runs) for functions that need it. The `runspec object`_ - provides access to context information. +1. The ``pytest_genfunc()`` hook will be called once for each test + function. The if-statement makes sure that we only add calls + for functions that actually need the provided value. + The `funcspec object`_ provides access to context information. 2. Subsequently the ``test_function()`` will be called three times with three different values for ``arg1``. @@ -91,7 +90,7 @@ Attributes of request objects ``request.cls``: class object where the test function is defined in or None. -``runspec.module``: module object where the test function is defined in. +``funcspec.module``: module object where the test function is defined in. ``request.config``: access to command line opts and general config @@ -151,48 +150,31 @@ Test functions can have multiple arguments which can either come from a test generator or from a provider. -.. _`runspec object`: +.. _`funcspec object`: -runspec objects +funcspec objects ------------------------ Runspecs help to inspect a testfunction and to generate tests with combinations of function argument values. -generating and combining funcargs -+++++++++++++++++++++++++++++++++++++++++++++++++++ +Calling ``funcspec.addcall(**funcargs)`` will add +a tests function call using the given dictionary +of function arguments. This addition of a call +happens during test collection. -Calling ``runspec.addfuncarg(argname, value)`` will trigger -tests function calls with the given function -argument value. For each already existing -funcarg combination, the added funcarg value will - -* be merged to the existing funcarg combination if the - new argument name isn't part of the funcarg combination yet. - -* otherwise generate a new test call where the existing - funcarg combination is copied and updated - with the newly added funcarg value. - -For simple usage, e.g. test functions with a single -generated function argument, each call to ``addfuncarg`` -will just trigger a new call. - -This scheme allows two sources to generate -function arguments independently from each other. - -Attributes of runspec objects +Attributes of funcspec objects ++++++++++++++++++++++++++++++++++++++++ -``runspec.funcargnames``: set of required function arguments for given function +``funcspec.funcargnames``: set of required function arguments for given function -``runspec.function``: underlying python test function +``funcspec.function``: underlying python test function -``runspec.cls``: class object where the test function is defined in or None. +``funcspec.cls``: class object where the test function is defined in or None. -``runspec.module``: the module object where the test function is defined in. +``funcspec.module``: the module object where the test function is defined in. -``runspec.config``: access to command line opts and general config +``funcspec.config``: access to command line opts and general config Useful Funcarg Tutorial Examples @@ -386,45 +368,35 @@ methods in a convenient way. .. _`py.path.local`: ../path.html#local -.. _`combine multiple funcarg values`: - -parametrize test functions by combining generated funcargs +parametrize test functions with multiple func args -------------------------------------------------------------------------- -Adding different funcargs will generate test calls with -all combinations of added funcargs. Consider this example: +You can trigger calling test functions which take more +than one function argument. Consider this example: .. sourcecode:: python - def makearg1(runspec): - runspec.addfuncarg("arg1", 10) - runspec.addfuncarg("arg1", 11) - - def makearg2(runspec): - runspec.addfuncarg("arg2", 20) - runspec.addfuncarg("arg2", 21) - - def pytest_genfuncruns(runspec): - makearg1(runspec) - makearg2(runspec) + def pytest_genfunc(funcspec): + for arg1 in range(2): + for arg2 in (10, 20): + funcspec.addcall(arg1=arg1, arg2=arg2) # the actual test function def test_function(arg1, arg2): - assert arg1 in (10, 20) - assert arg2 in (20, 30) + pass Running this test module will result in ``test_function`` -being called four times, in the following order:: +being called four times, with the following arguments:: - test_function(10, 20) - test_function(10, 21) - test_function(11, 20) - test_function(11, 21) + test_function(0, 10) + test_function(0, 20) + test_function(1, 10) + test_function(2, 20) -example: test functions with generated and provided funcargs +example: test functions with pre-generated and provided funcargs ------------------------------------------------------------------- You can mix generated function arguments and normally @@ -432,10 +404,10 @@ provided ones. Consider this module: .. sourcecode:: python - def pytest_genfuncruns(runspec): - if "arg1" in runspec.funcargnames: # test_function2 does not have it - runspec.addfuncarg("arg1", 10) - runspec.addfuncarg("arg1", 20) + def pytest_genfunc(funcspec): + if "arg1" in funcspec.funcargnames: # test_function2 does not have it + funcspec.addcall(arg1=10) + funcspec.addcall(arg1=20) def pytest_funcarg__arg2(request): return [10, 20] diff --git a/py/test/funcargs.py b/py/test/funcargs.py index 1e4e36f9a..fe1120c50 100644 --- a/py/test/funcargs.py +++ b/py/test/funcargs.py @@ -25,7 +25,7 @@ def fillfuncargs(function): except request.Error: request._raiselookupfailed() -class RunSpecs: +class FuncSpecs: def __init__(self, function, config=None, cls=None, module=None): self.config = config self.module = module @@ -33,22 +33,14 @@ class RunSpecs: self.funcargnames = getfuncargnames(function) self.cls = cls self.module = module - self._combinations = [] + self._calls = [] - def addfuncarg(self, argname, value): - if argname not in self.funcargnames: - raise ValueError("function %r has no funcarg %r" %( - self.function, argname)) - newcombi = [] - if not self._combinations: - newcombi.append({argname:value}) - else: - for combi in self._combinations: - if argname in combi: - combi = combi.copy() - newcombi.append(combi) - combi[argname] = value - self._combinations.extend(newcombi) + def addcall(self, **kwargs): + for argname in kwargs: + if argname not in self.funcargnames: + raise ValueError("function %r has no funcarg %r" %( + self.function, argname)) + self._calls.append(kwargs) class FunctionCollector(py.test.collect.Collector): def __init__(self, name, parent, combinations): diff --git a/py/test/plugin/api.py b/py/test/plugin/api.py index f5a9be729..7f057b3fb 100644 --- a/py/test/plugin/api.py +++ b/py/test/plugin/api.py @@ -53,7 +53,7 @@ class PluginHooks: """ return custom item/collector for a python object in a module, or None. """ pytest_pycollect_obj.firstresult = True - def pytest_genfuncruns(self, runspec): + def pytest_genfunc(self, funcspec): """ generate (multiple) parametrized calls to a test function.""" def pytest_collectstart(self, collector): diff --git a/py/test/pycollect.py b/py/test/pycollect.py index 86b7c1ff6..1f6871e81 100644 --- a/py/test/pycollect.py +++ b/py/test/pycollect.py @@ -172,13 +172,13 @@ class PyCollectorMixin(PyobjMixin, py.test.collect.Collector): # to work to get at the class clscol = self.getclasscollector() cls = clscol and clscol.obj or None - runspec = funcargs.RunSpecs(funcobj, config=self.config, cls=cls, module=module) - gentesthook = self.config.hook.pytest_genfuncruns.clone(extralookup=module) - gentesthook(runspec=runspec) - if not runspec._combinations: + funcspec = funcargs.FuncSpecs(funcobj, config=self.config, cls=cls, module=module) + gentesthook = self.config.hook.pytest_genfunc.clone(extralookup=module) + gentesthook(funcspec=funcspec) + if not funcspec._calls: return self.Function(name, parent=self) return funcargs.FunctionCollector(name=name, - parent=self, combinations=runspec._combinations) + parent=self, combinations=funcspec._calls) class Module(py.test.collect.File, PyCollectorMixin): def _getobj(self): diff --git a/py/test/testing/test_funcargs.py b/py/test/testing/test_funcargs.py index a25571c6e..78a865cb3 100644 --- a/py/test/testing/test_funcargs.py +++ b/py/test/testing/test_funcargs.py @@ -131,71 +131,58 @@ class TestRequest: req = funcargs.FuncargRequest(item, "xxx") assert req.fspath == modcol.fspath -class TestRunSpecs: +class TestFuncSpecs: def test_no_funcargs(self, testdir): def function(): pass - runspec = funcargs.RunSpecs(function) - assert not runspec.funcargnames + funcspec = funcargs.FuncSpecs(function) + assert not funcspec.funcargnames def test_function_basic(self): def func(arg1, arg2="qwe"): pass - runspec = funcargs.RunSpecs(func) - assert len(runspec.funcargnames) == 1 - assert 'arg1' in runspec.funcargnames - assert runspec.function is func - assert runspec.cls is None + funcspec = funcargs.FuncSpecs(func) + assert len(funcspec.funcargnames) == 1 + assert 'arg1' in funcspec.funcargnames + assert funcspec.function is func + assert funcspec.cls is None - def test_addfuncarg_basic(self): + def test_addcall_basic(self): def func(arg1): pass - runspec = funcargs.RunSpecs(func) + funcspec = funcargs.FuncSpecs(func) py.test.raises(ValueError, """ - runspec.addfuncarg("notexists", 100) + funcspec.addcall(notexists=100) """) - runspec.addfuncarg("arg1", 100) - assert len(runspec._combinations) == 1 - assert runspec._combinations[0] == {'arg1': 100} + funcspec.addcall(arg1=100) + assert len(funcspec._calls) == 1 + assert funcspec._calls[0] == {'arg1': 100} - def test_addfuncarg_two(self): + def test_addcall_two(self): def func(arg1): pass - runspec = funcargs.RunSpecs(func) - runspec.addfuncarg("arg1", 100) - runspec.addfuncarg("arg1", 101) - assert len(runspec._combinations) == 2 - assert runspec._combinations[0] == {'arg1': 100} - assert runspec._combinations[1] == {'arg1': 101} - - def test_addfuncarg_combined(self): - runspec = funcargs.RunSpecs(lambda arg1, arg2: 0) - runspec.addfuncarg('arg1', 1) - runspec.addfuncarg('arg1', 2) - runspec.addfuncarg('arg2', 100) - combinations = runspec._combinations - assert len(combinations) == 2 - assert combinations[0] == {'arg1': 1, 'arg2': 100} - assert combinations[1] == {'arg1': 2, 'arg2': 100} - runspec.addfuncarg('arg2', 101) - assert len(combinations) == 4 - assert combinations[-1] == {'arg1': 2, 'arg2': 101} + funcspec = funcargs.FuncSpecs(func) + funcspec.addcall(arg1=100) + funcspec.addcall(arg1=101) + assert len(funcspec._calls) == 2 + assert funcspec._calls[0] == {'arg1': 100} + assert funcspec._calls[1] == {'arg1': 101} class TestGenfuncFunctional: def test_attributes(self, testdir): p = testdir.makepyfile(""" import py - def pytest_genfuncruns(runspec): - runspec.addfuncarg("runspec", runspec) + def pytest_genfunc(funcspec): + funcspec.addcall(funcspec=funcspec) - def test_function(runspec): - assert runspec.config == py.test.config - assert runspec.module.__name__ == __name__ - assert runspec.function == test_function - assert runspec.cls is None + def test_function(funcspec): + assert funcspec.config == py.test.config + assert funcspec.module.__name__ == __name__ + assert funcspec.function == test_function + assert funcspec.cls is None class TestClass: - def test_method(self, runspec): - assert runspec.config == py.test.config - assert runspec.module.__name__ == __name__ + def test_method(self, funcspec): + assert funcspec.config == py.test.config + assert funcspec.module.__name__ == __name__ # XXX actually have the unbound test function here? - assert runspec.function == TestClass.test_method.im_func - assert runspec.cls == TestClass + assert funcspec.function == TestClass.test_method.im_func + assert funcspec.cls == TestClass """) result = testdir.runpytest(p, "-v") result.stdout.fnmatch_lines([ @@ -205,10 +192,10 @@ class TestGenfuncFunctional: def test_arg_twice(self, testdir): testdir.makeconftest(""" class ConftestPlugin: - def pytest_genfuncruns(self, runspec): - assert "arg" in runspec.funcargnames - runspec.addfuncarg("arg", 10) - runspec.addfuncarg("arg", 20) + def pytest_genfunc(self, funcspec): + assert "arg" in funcspec.funcargnames + funcspec.addcall(arg=10) + funcspec.addcall(arg=20) """) p = testdir.makepyfile(""" def test_myfunc(arg): @@ -223,9 +210,9 @@ class TestGenfuncFunctional: def test_two_functions(self, testdir): p = testdir.makepyfile(""" - def pytest_genfuncruns(runspec): - runspec.addfuncarg("arg1", 10) - runspec.addfuncarg("arg1", 20) + def pytest_genfunc(funcspec): + funcspec.addcall(arg1=10) + funcspec.addcall(arg1=20) def test_func1(arg1): assert arg1 == 10 @@ -243,20 +230,17 @@ class TestGenfuncFunctional: def test_genfuncarg_inmodule(self, testdir): testdir.makeconftest(""" class ConftestPlugin: - def pytest_genfuncruns(self, runspec): - assert "arg" in runspec.funcargnames - runspec.addfuncarg("arg", 10) + def pytest_genfunc(self, funcspec): + assert "arg1" in funcspec.funcargnames + funcspec.addcall(arg1=1, arg2=2) """) p = testdir.makepyfile(""" - def pytest_genfuncruns(runspec): - runspec.addfuncarg("arg2", 10) - runspec.addfuncarg("arg2", 20) - runspec.addfuncarg("classarg", 17) + def pytest_genfunc(funcspec): + funcspec.addcall(arg1=10, arg2=10) class TestClass: - def test_myfunc(self, arg, arg2, classarg): - assert classarg == 17 - assert arg == arg2 + def test_myfunc(self, arg1, arg2): + assert arg1 == arg2 """) result = testdir.runpytest("-v", p) assert result.stdout.fnmatch_lines([