merging the new function generators, addresses issue 2

- introduce a new pytest_genfuncruns hook for generating tests with multiple funcargs
- new and extended docs: doc/test/funcargs.txt
- factor all funcargs related code into py/test/funcargs.py
- remove request.maketempdir call (you can use request.config.mktemp)

--HG--
branch : trunk
This commit is contained in:
holger krekel 2009-05-11 19:23:57 +02:00
parent 1cb83de0ab
commit d9ad2cf761
15 changed files with 600 additions and 287 deletions

View File

@ -28,8 +28,10 @@ per-testrun temporary directories
------------------------------------------- -------------------------------------------
``py.test`` runs provide means to create per-test session ``py.test`` runs provide means to create per-test session
temporary (sub) directories. You can create such directories temporary (sub) directories through the config object.
like this: You can create directories like this:
.. XXX use a more local example, just with "config"
.. sourcecode: python .. sourcecode: python

View File

@ -1,52 +1,76 @@
====================================================== ======================================================
**funcargs**: powerful and simple test setup **funcargs**: powerful test setup and parametrization
====================================================== ======================================================
In version 1.0 py.test introduces a new mechanism for setting up test Since version 1.0 it is possible to provide arguments to test functions,
state for use by Python test functions. It is particularly useful often called "funcargs". The funcarg mechanisms were developed with
for functional and integration testing but also for unit testing. these goals in mind:
Using funcargs you can easily:
* write self-contained, simple to read and debug test functions * **no boilerplate**: cleanly encapsulate test setup and fixtures
* cleanly encapsulate glue code between your app and your tests * **flexibility**: easily setup test state depending on command line options or environment
* setup test state depending on command line options or environment * **readability**: write simple to read and debug test functions
* **parametrizing tests**: run a test function multiple times with different parameters
Using the funcargs mechanism will increase readability
and allow for easier refactoring of your application
and its test suites.
.. contents:: Contents: .. contents:: Contents:
:depth: 2 :depth: 2
The basic funcarg request/provide mechanism Basic mechanisms by example
============================================= =============================================
To use funcargs you only need to specify providing single function arguments as needed
a named argument for your test function: ---------------------------------------------------------
Let's look at a simple example of using funcargs within a test module:
.. sourcecode:: python .. sourcecode:: python
def test_function(myarg): def pytest_funcarg__myfuncarg(request):
# use myarg return 42
For each test function that requests this ``myarg`` def test_function(myfuncarg):
argument a matching so called funcarg provider assert myfuncarg == 42
will be invoked. A Funcarg provider for ``myarg``
is written down liks this: 1. To setup the running of the ``test_function()`` call, py.test
looks up a provider for the ``myfuncarg`` argument.
The provider method is recognized by its ``pytest_funcarg__`` prefix
followed by the requested function argument name.
The `request object`_ gives access to test context.
2. A ``test_function(42)`` call is executed. If the test fails
one can see the original provided value.
generating test runs with multiple function argument values
----------------------------------------------------------------------
You can parametrize multiple runs of a test function by
providing multiple values for function arguments. Here
is an example for running the same test function three times.
.. sourcecode:: python .. sourcecode:: python
def pytest_funcarg__myarg(self, request): def pytest_genfuncruns(runspec):
# return value for myarg here if "arg1" in runspec.funcargnames:
runspec.addfuncarg("arg1", 10)
runspec.addfuncarg("arg1", 20)
runspec.addfuncarg("arg1", 30)
Such a provider method can live on a test class, def test_function(arg1):
test module or on a local or global plugin. assert myfuncarg in (10, 20, 30)
The method is recognized by the ``pytest_funcarg__``
prefix and is correlated to the argument Here is what happens:
name which follows this prefix. The passed in
``request`` object allows to interact 1. The ``pytest_genfuncruns()`` hook will be called once for each test
with test configuration, test collection function. The if-statement makes sure that we only add function
and test running aspects. arguments (and runs) for functions that need it. The `runspec object`_
provides access to context information.
2. Subsequently the ``test_function()`` will be called three times
with three different values for ``arg1``.
Funcarg rules and support objects
====================================
.. _`request object`: .. _`request object`:
@ -65,11 +89,13 @@ Attributes of request objects
``request.function``: python function object requesting the argument ``request.function``: python function object requesting the argument
``request.fspath``: filesystem path of containing module ``request.cls``: class object where the test function is defined in or None.
``runspec.module``: module object where the test function is defined in.
``request.config``: access to command line opts and general config ``request.config``: access to command line opts and general config
finalizing after test function executed cleanup after test function execution
++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++
Request objects allow to **register a finalizer method** which is Request objects allow to **register a finalizer method** which is
@ -86,33 +112,8 @@ function finish:
request.addfinalizer(lambda: myfile.close()) request.addfinalizer(lambda: myfile.close())
return myfile return myfile
a unique temporary directory
++++++++++++++++++++++++++++++++++++++++
request objects allow to create unique temporary decorating other funcarg providers
directories. These directories will be created
as subdirectories under the `per-testsession
temporary directory`_. Each request object
receives its own unique subdirectory whose
basenames starts with the name of the function
that triggered the funcarg request. You
can further work with the provided `py.path.local`_
object to e.g. create subdirs or config files::
def pytest_funcarg__mysetup(self, request):
tmpdir = request.maketempdir()
tmpdir.mkdir("mysubdir")
tmpdir.join("config.ini").write("[default")
return tmpdir
Note that you do not need to perform finalization,
i.e. remove the temporary directory as this is
part of the global management of the base temporary
directory.
.. _`per-testsession temporary directory`: config.html#basetemp
decorating/adding to existing funcargs
++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++
If you want to **decorate a function argument** that is If you want to **decorate a function argument** that is
@ -131,33 +132,73 @@ is no next provider left. See the `decorator example`_
for a use of this method. for a use of this method.
.. _`funcarg lookup order`: .. _`lookup order`:
Order of funcarg provider lookup Order of provider and test generator lookup
---------------------------------------- ----------------------------------------------
For any funcarg argument request here is the Both test generators as well as funcarg providers
lookup order for provider methods: are looked up in the following three scopes:
1. test class (if we are executing a method)
2. test module
3. local plugins
4. global plugins
1. test module
2. local plugins
3. global plugins
Using multiple funcargs Using multiple funcargs
---------------------------------------- ----------------------------------------
A test function may receive more than one Test functions can have multiple arguments
function arguments. For each of the which can either come from a test generator
function arguments a lookup of a or from a provider.
matching provider will be performed.
.. _`runspec object`:
runspec objects
------------------------
Runspecs help to inspect a testfunction and
to generate tests with combinations of function argument values.
generating and combining funcargs
+++++++++++++++++++++++++++++++++++++++++++++++++++
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
++++++++++++++++++++++++++++++++++++++++
``runspec.funcargnames``: set of required function arguments for given function
``runspec.function``: underlying python test function
``runspec.cls``: class object where the test function is defined in or None.
``runspec.module``: the module object where the test function is defined in.
``runspec.config``: access to command line opts and general config
Funcarg Tutorial Examples Useful Funcarg Tutorial Examples
============================ =======================================
tutorial example: the "test/app-specific" setup pattern application specific test setup
--------------------------------------------------------- ---------------------------------------------------------
Here is a basic useful step-wise example for handling application Here is a basic useful step-wise example for handling application
@ -202,7 +243,7 @@ following code into a local ``conftest.py``:
return MyApp() return MyApp()
py.test finds the ``pytest_funcarg__mysetup`` method by py.test finds the ``pytest_funcarg__mysetup`` method by
name, see `funcarg lookup order`_ for more on this mechanism. name, see also `lookup order`_.
To run the example we put a pseudo MyApp object into ``myapp.py``: To run the example we put a pseudo MyApp object into ``myapp.py``:
@ -265,29 +306,8 @@ Now any test functions can use the ``mysetup.getsshconnection()`` method like th
conn = mysetup.getsshconnection() conn = mysetup.getsshconnection()
# work with conn # work with conn
Running this without the command line will yield this run result:: Running this without specifying a command line option will result in a skipped
test_function.
XXX fill in
Example: specifying funcargs in test modules or classes
---------------------------------------------------------
.. sourcecode:: python
def pytest_funcarg__mysetup(request):
result = request.call_next_provider()
result.extra = "..."
return result
You can put such a function into a test class like this:
.. sourcecode:: python
class TestClass:
def pytest_funcarg__mysetup(self, request):
# ...
#
.. _`accept example`: .. _`accept example`:
@ -309,16 +329,12 @@ example: specifying and selecting acceptance tests
def __init__(self, request): def __init__(self, request):
if not request.config.option.acceptance: if not request.config.option.acceptance:
py.test.skip("specify -A to run acceptance tests") py.test.skip("specify -A to run acceptance tests")
self.tmpdir = request.config.maketempdir(request.argname) self.tmpdir = request.config.mktemp(request.function.__name__, numbered=True)
self._old = self.tmpdir.chdir()
request.addfinalizer(self.finalize)
def run(self): def run(self, cmd):
return py.process.cmdexec("echo hello") """ called by test code to execute an acceptance test. """
self.tmpdir.chdir()
def finalize(self): return py.process.cmdexec(cmd)
self._old.chdir()
# cleanup any other resources
and the actual test function example: and the actual test function example:
@ -327,48 +343,116 @@ and the actual test function example:
def test_some_acceptance_aspect(accept): def test_some_acceptance_aspect(accept):
accept.tmpdir.mkdir("somesub") accept.tmpdir.mkdir("somesub")
result = accept.run() result = accept.run("ls -la")
assert result assert "somesub" in result
That's it! This test will get automatically skipped with
an appropriate message if you just run ``py.test``::
... OUTPUT of py.test on this example ...
If you run this test without specifying a command line option
the test will get skipped with an appropriate message. Otherwise
you can start to add convenience and test support methods
to your AcceptFuncarg and drive running of tools or
applications and provide ways to do assertions about
the output.
.. _`decorator example`: .. _`decorator example`:
example: decorating/extending a funcarg in a TestClass example: decorating a funcarg in a test module
-------------------------------------------------------------- --------------------------------------------------------------
For larger scale setups it's sometimes useful to decorare For larger scale setups it's sometimes useful to decorare
a funcarg just for a particular test module or even a funcarg just for a particular test module. We can
a particular test class. We can extend the `accept example`_ extend the `accept example`_ by putting this in our test class:
by putting this in our test class:
.. sourcecode:: python .. sourcecode:: python
class TestSpecialAcceptance:
def pytest_funcarg__accept(self, request): def pytest_funcarg__accept(self, request):
arg = request.call_next_provider() arg = request.call_next_provider()
# create a special layout in our tempdir # create a special layout in our tempdir
arg.tmpdir.mkdir("special") arg.tmpdir.mkdir("special")
return arg return arg
class TestSpecialAcceptance:
def test_sometest(self, accept): def test_sometest(self, accept):
assert accept.tmpdir.join("special").check() assert accept.tmpdir.join("special").check()
According to the `funcarg lookup order`_ our class-specific provider will According to the the `lookup order`_ our module level provider
be invoked first. Here, we just ask our request object to will be invoked first and it can ask ask its request object to
call the next provider and decorate its result. This simple call the next provider and then decorate its result. This
mechanism allows us to stay ignorant of how/where the mechanism allows us to stay ignorant of how/where the
function argument is provided. function argument is provided.
Note that we make use here of `py.path.local`_ objects sidenote: the temporary directory used here are instances of
that provide uniform access to the local filesystem. the `py.path.local`_ class which provides many of the os.path
methods in a convenient way.
.. _`py.path.local`: ../path.html#local .. _`py.path.local`: ../path.html#local
.. _`combine multiple funcarg values`:
parametrize test functions by combining generated funcargs
--------------------------------------------------------------------------
Adding different funcargs will generate test calls with
all combinations of added funcargs. 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)
# the actual test function
def test_function(arg1, arg2):
assert arg1 in (10, 20)
assert arg2 in (20, 30)
Running this test module will result in ``test_function``
being called four times, in the following order::
test_function(10, 20)
test_function(10, 21)
test_function(11, 20)
test_function(11, 21)
example: test functions with generated and provided funcargs
-------------------------------------------------------------------
You can mix generated function arguments and normally
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_funcarg__arg2(request):
return [10, 20]
def test_function(arg1, arg2):
assert arg1 in arg2
def test_function2(arg2):
assert args2 == [10, 20]
Running this test module will result in ``test_function``
being called twice, with these arguments::
test_function(10, [10, 20])
test_function(20, [10, 20])
Questions and Answers Questions and Answers
================================== ==================================
@ -377,14 +461,14 @@ Questions and Answers
Why ``pytest_funcarg__*`` methods? Why ``pytest_funcarg__*`` methods?
------------------------------------ ------------------------------------
When experimenting with funcargs we also considered an explicit When experimenting with funcargs we also
registration mechanism, i.e. calling a register method e.g. on the considered an explicit registration mechanism, i.e. calling a register
config object. But lacking a good use case for this indirection and method on the config object. But lacking a good use case for this
flexibility we decided to go for `Convention over Configuration`_ indirection and flexibility we decided to go for `Convention over
and allow to directly specify the provider. It has the Configuration`_ and allow to directly specify the provider. It has the
positive implication that you should be able to positive implication that you should be able to "grep" for
"grep" for ``pytest_funcarg__MYARG`` and will find all ``pytest_funcarg__MYARG`` and will find all providing sites (usually
providing sites (usually exactly one). exactly one).
.. _`Convention over Configuration`: http://en.wikipedia.org/wiki/Convention_over_Configuration .. _`Convention over Configuration`: http://en.wikipedia.org/wiki/Convention_over_Configuration

View File

@ -11,14 +11,18 @@ quickstart_: for getting started immediately.
features_: a walk through basic features and usage. features_: a walk through basic features and usage.
funcargs_: powerful parametrized test function setup
`distributed testing`_: distribute test runs to other machines and platforms.
plugins_: using available plugins. plugins_: using available plugins.
extend_: writing plugins and advanced configuration. extend_: writing plugins and advanced configuration.
`distributed testing`_ how to distribute test runs to other machines and platforms.
.. _quickstart: quickstart.html .. _quickstart: quickstart.html
.. _features: features.html .. _features: features.html
.. _funcargs: funcargs.html
.. _plugins: plugins.html .. _plugins: plugins.html
.. _extend: ext.html .. _extend: ext.html
.. _`distributed testing`: dist.html .. _`distributed testing`: dist.html

View File

@ -123,10 +123,14 @@ class Hooks:
return "<Hooks %r %r>" %(self._hookspecs, self._plugins) return "<Hooks %r %r>" %(self._hookspecs, self._plugins)
class HookCall: class HookCall:
def __init__(self, registry, name, firstresult): def __init__(self, registry, name, firstresult, extralookup=None):
self.registry = registry self.registry = registry
self.name = name self.name = name
self.firstresult = firstresult self.firstresult = firstresult
self.extralookup = extralookup and [extralookup] or ()
def clone(self, extralookup):
return HookCall(self.registry, self.name, self.firstresult, extralookup)
def __repr__(self): def __repr__(self):
mode = self.firstresult and "firstresult" or "each" mode = self.firstresult and "firstresult" or "each"
@ -136,7 +140,8 @@ class HookCall:
if args: if args:
raise TypeError("only keyword arguments allowed " raise TypeError("only keyword arguments allowed "
"for api call to %r" % self.name) "for api call to %r" % self.name)
mc = MultiCall(self.registry.listattr(self.name), **kwargs) attr = self.registry.listattr(self.name, extra=self.extralookup)
mc = MultiCall(attr, **kwargs)
return mc.execute(firstresult=self.firstresult) return mc.execute(firstresult=self.firstresult)
comregistry = Registry() comregistry = Registry()

View File

@ -114,7 +114,7 @@ class TestGatewayManagerPopen:
class pytest_funcarg__mysetup: class pytest_funcarg__mysetup:
def __init__(self, request): def __init__(self, request):
tmp = request.maketempdir() tmp = request.config.mktemp(request.function.__name__, numbered=True)
self.source = tmp.mkdir("source") self.source = tmp.mkdir("source")
self.dest = tmp.mkdir("dest") self.dest = tmp.mkdir("dest")

View File

@ -190,3 +190,23 @@ class TestHooks:
class Api: pass class Api: pass
mcm = Hooks(hookspecs=Api) mcm = Hooks(hookspecs=Api)
assert mcm.registry == py._com.comregistry assert mcm.registry == py._com.comregistry
def test_hooks_extra_plugins(self):
registry = Registry()
class Api:
def hello(self, arg):
pass
hook_hello = Hooks(hookspecs=Api, registry=registry).hello
class Plugin:
def hello(self, arg):
return arg + 1
registry.register(Plugin())
class Plugin2:
def hello(self, arg):
return arg + 2
newhook = hook_hello.clone(extralookup=Plugin2())
l = newhook(arg=3)
assert l == [5, 4]
l2 = hook_hello(arg=3)
assert l2 == [4]

View File

@ -3,8 +3,9 @@ from py.__.test.dist.nodemanage import NodeManager
class pytest_funcarg__mysetup: class pytest_funcarg__mysetup:
def __init__(self, request): def __init__(self, request):
basetemp = request.maketempdir() basetemp = request.config.mktemp(
basetemp = basetemp.mkdir(request.function.__name__) "mysetup:%s" % request.function.__name__,
numbered=True)
self.source = basetemp.mkdir("source") self.source = basetemp.mkdir("source")
self.dest = basetemp.mkdir("dest") self.dest = basetemp.mkdir("dest")

115
py/test/funcargs.py Normal file
View File

@ -0,0 +1,115 @@
import py
def getfuncargnames(function):
argnames = py.std.inspect.getargs(function.func_code)[0]
startindex = hasattr(function, 'im_self') and 1 or 0
numdefaults = len(function.func_defaults or ())
if numdefaults:
return argnames[startindex:-numdefaults]
return argnames[startindex:]
def fillfuncargs(function):
""" fill missing funcargs. """
if function._args:
# functions yielded from a generator: we don't want
# to support that because we want to go here anyway:
# http://bitbucket.org/hpk42/py-trunk/issue/2/next-generation-generative-tests
pass
else:
# standard Python Test function/method case
for argname in getfuncargnames(function.obj):
if argname not in function.funcargs:
request = FuncargRequest(pyfuncitem=function, argname=argname)
try:
function.funcargs[argname] = request.call_next_provider()
except request.Error:
request._raiselookupfailed()
class RunSpecs:
def __init__(self, function, config=None, cls=None, module=None):
self.config = config
self.module = module
self.function = function
self.funcargnames = getfuncargnames(function)
self.cls = cls
self.module = module
self._combinations = []
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)
class FunctionCollector(py.test.collect.Collector):
def __init__(self, name, parent, combinations):
super(FunctionCollector, self).__init__(name, parent)
self.combinations = combinations
self.obj = getattr(self.parent.obj, name)
def collect(self):
l = []
for i, funcargs in py.builtin.enumerate(self.combinations):
function = self.parent.Function(name="%s[%s]" %(self.name, i),
parent=self, funcargs=funcargs, callobj=self.obj)
l.append(function)
return l
class FuncargRequest:
_argprefix = "pytest_funcarg__"
class Error(LookupError):
""" error on performing funcarg request. """
def __init__(self, pyfuncitem, argname):
self._pyfuncitem = pyfuncitem
self.argname = argname
self.function = pyfuncitem.obj
self.module = pyfuncitem.getmodulecollector().obj
self.cls = getattr(self.function, 'im_class', None)
self.config = pyfuncitem.config
self.fspath = pyfuncitem.fspath
self._plugins = self.config.pluginmanager.getplugins()
self._plugins.append(pyfuncitem.getmodulecollector().obj)
self._provider = self.config.pluginmanager.listattr(
plugins=self._plugins,
attrname=self._argprefix + str(argname)
)
def __repr__(self):
return "<FuncargRequest %r for %r>" %(self.argname, self._pyfuncitem)
def call_next_provider(self):
if not self._provider:
raise self.Error("no provider methods left")
next_provider = self._provider.pop()
return next_provider(request=self)
def addfinalizer(self, finalizer):
self._pyfuncitem.addfinalizer(finalizer)
def _raiselookupfailed(self):
available = []
for plugin in self._plugins:
for name in vars(plugin.__class__):
if name.startswith(self._argprefix):
name = name[len(self._argprefix):]
if name not in available:
available.append(name)
fspath, lineno, msg = self._pyfuncitem.metainfo()
line = "%s:%s" %(fspath, lineno)
msg = "funcargument %r not found for: %s" %(self.argname, line)
msg += "\n available funcargs: %s" %(", ".join(available),)
raise LookupError(msg)

View File

@ -53,13 +53,15 @@ class PluginHooks:
""" return custom item/collector for a python object in a module, or None. """ """ return custom item/collector for a python object in a module, or None. """
pytest_pycollect_obj.firstresult = True pytest_pycollect_obj.firstresult = True
def pytest_genfuncruns(self, runspec):
""" generate (multiple) parametrized calls to a test function."""
def pytest_collectstart(self, collector): def pytest_collectstart(self, collector):
""" collector starts collecting. """ """ collector starts collecting. """
def pytest_collectreport(self, rep): def pytest_collectreport(self, rep):
""" collector finished collecting. """ """ collector finished collecting. """
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# runtest related hooks # runtest related hooks
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View File

@ -142,7 +142,7 @@ class ReSTSyntaxTest(py.test.collect.Item):
directives.register_directive('sourcecode', pygments_directive) directives.register_directive('sourcecode', pygments_directive)
def resolve_linkrole(self, name, text, check=True): def resolve_linkrole(self, name, text, check=True):
apigen_relpath = self.project.hookgen_relpath apigen_relpath = self.project.apigen_relpath
if name == 'api': if name == 'api':
if text == 'py': if text == 'py':

View File

@ -27,9 +27,10 @@ def test_generic(plugintester):
plugintester.hookcheck(TmpdirPlugin) plugintester.hookcheck(TmpdirPlugin)
def test_funcarg(testdir): def test_funcarg(testdir):
from py.__.test.funcargs import FuncargRequest
item = testdir.getitem("def test_func(tmpdir): pass") item = testdir.getitem("def test_func(tmpdir): pass")
plugin = TmpdirPlugin() plugin = TmpdirPlugin()
p = plugin.pytest_funcarg__tmpdir(item.getrequest("tmpdir")) p = plugin.pytest_funcarg__tmpdir(FuncargRequest(item, "tmpdir"))
assert p.check() assert p.check()
bn = p.basename.strip("0123456789-") bn = p.basename.strip("0123456789-")
assert bn.endswith("test_func") assert bn.endswith("test_func")

View File

@ -20,6 +20,7 @@ import py
from py.__.test.collect import configproperty, warnoldcollect from py.__.test.collect import configproperty, warnoldcollect
from py.__.code.source import findsource from py.__.code.source import findsource
pydir = py.path.local(py.__file__).dirpath() pydir = py.path.local(py.__file__).dirpath()
from py.__.test import funcargs
class PyobjMixin(object): class PyobjMixin(object):
def obj(): def obj():
@ -37,6 +38,16 @@ class PyobjMixin(object):
def _getobj(self): def _getobj(self):
return getattr(self.parent.obj, self.name) return getattr(self.parent.obj, self.name)
def getmodulecollector(self):
return self._getparent(Module)
def getclasscollector(self):
return self._getparent(Class)
def _getparent(self, cls):
current = self
while current and not isinstance(current, cls):
current = current.parent
return current
def getmodpath(self, stopatmodule=True, includemodule=False): def getmodpath(self, stopatmodule=True, includemodule=False):
""" return python path relative to the containing module. """ """ return python path relative to the containing module. """
chain = self.listchain() chain = self.listchain()
@ -150,9 +161,24 @@ class PyCollectorMixin(PyobjMixin, py.test.collect.Collector):
if res is not None: if res is not None:
return res return res
if obj.func_code.co_flags & 32: # generator function if obj.func_code.co_flags & 32: # generator function
# XXX deprecation warning
return self.Generator(name, parent=self) return self.Generator(name, parent=self)
else: else:
return self._genfunctions(name, obj)
def _genfunctions(self, name, funcobj):
module = self.getmodulecollector().obj
# due to _buildname2items funcobj is the raw function, we need
# 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:
return self.Function(name, parent=self) return self.Function(name, parent=self)
return funcargs.FunctionCollector(name=name,
parent=self, combinations=runspec._combinations)
class Module(py.test.collect.File, PyCollectorMixin): class Module(py.test.collect.File, PyCollectorMixin):
def _getobj(self): def _getobj(self):
@ -320,11 +346,13 @@ class Function(FunctionMixin, py.test.collect.Item):
""" a Function Item is responsible for setting up """ a Function Item is responsible for setting up
and executing a Python callable test object. and executing a Python callable test object.
""" """
def __init__(self, name, parent=None, config=None, args=(), callobj=_dummy): def __init__(self, name, parent=None, config=None, args=(), funcargs=None, callobj=_dummy):
super(Function, self).__init__(name, parent, config=config) super(Function, self).__init__(name, parent, config=config)
self._finalizers = [] self._finalizers = []
self._args = args self._args = args
self.funcargs = {} if funcargs is None:
funcargs = {}
self.funcargs = funcargs
if callobj is not _dummy: if callobj is not _dummy:
self._obj = callobj self._obj = callobj
@ -350,31 +378,7 @@ class Function(FunctionMixin, py.test.collect.Item):
def setup(self): def setup(self):
super(Function, self).setup() super(Function, self).setup()
self._setupfuncargs() funcargs.fillfuncargs(self)
def _setupfuncargs(self):
if self._args:
# functions yielded from a generator: we don't want
# to support that because we want to go here anyway:
# http://bitbucket.org/hpk42/py-trunk/issue/2/next-generation-generative-tests
pass
else:
# standard Python Test function/method case
funcobj = self.obj
startindex = getattr(funcobj, 'im_self', None) and 1 or 0
argnames = py.std.inspect.getargs(self.obj.func_code)[0]
for i, argname in py.builtin.enumerate(argnames):
if i < startindex:
continue
request = self.getrequest(argname)
try:
self.funcargs[argname] = request.call_next_provider()
except request.Error:
numdefaults = len(funcobj.func_defaults or ())
if i + numdefaults >= len(argnames):
continue # our args have defaults XXX issue warning?
else:
request._raiselookupfailed()
def __eq__(self, other): def __eq__(self, other):
try: try:
@ -385,74 +389,7 @@ class Function(FunctionMixin, py.test.collect.Item):
except AttributeError: except AttributeError:
pass pass
return False return False
def __ne__(self, other): def __ne__(self, other):
return not self == other return not self == other
def getrequest(self, argname):
return FuncargRequest(pyfuncitem=self, argname=argname)
class FuncargRequest:
_argprefix = "pytest_funcarg__"
class Error(LookupError):
""" error on performing funcarg request. """
def __init__(self, pyfuncitem, argname):
# XXX make pyfuncitem _pyfuncitem
self._pyfuncitem = pyfuncitem
self.argname = argname
self.function = pyfuncitem.obj
self.config = pyfuncitem.config
self.fspath = pyfuncitem.fspath
self._plugins = self._getplugins()
self._methods = self.config.pluginmanager.listattr(
plugins=self._plugins,
attrname=self._argprefix + str(argname)
)
def __repr__(self):
return "<FuncargRequest %r for %r>" %(self.argname, self._pyfuncitem)
def _getplugins(self):
plugins = []
current = self._pyfuncitem
while not isinstance(current, Module):
current = current.parent
if isinstance(current, (Instance, Module)):
plugins.insert(0, current.obj)
return self.config.pluginmanager.getplugins() + plugins
def call_next_provider(self):
if not self._methods:
raise self.Error("no provider methods left")
nextmethod = self._methods.pop()
return nextmethod(request=self)
def addfinalizer(self, finalizer):
self._pyfuncitem.addfinalizer(finalizer)
def maketempdir(self):
basetemp = self.config.getbasetemp()
tmp = py.path.local.make_numbered_dir(
prefix=self.function.__name__ + "_",
keep=0, rootdir=basetemp)
return tmp
def _raiselookupfailed(self):
available = []
for plugin in self._plugins:
for name in vars(plugin.__class__):
if name.startswith(self._argprefix):
name = name[len(self._argprefix):]
if name not in available:
available.append(name)
fspath, lineno, msg = self._pyfuncitem.metainfo()
line = "%s:%s" %(fspath, lineno)
msg = "funcargument %r not found for: %s" %(self.argname, line)
msg += "\n available funcargs: %s" %(", ".join(available),)
raise LookupError(msg)

View File

@ -1,6 +1,22 @@
import py import py
from py.__.test import funcargs
class TestFuncargs: def test_getfuncargnames():
def f(): pass
assert not funcargs.getfuncargnames(f)
def g(arg): pass
assert funcargs.getfuncargnames(g) == ['arg']
def h(arg1, arg2="hello"): pass
assert funcargs.getfuncargnames(h) == ['arg1']
def h(arg1, arg2, arg3="hello"): pass
assert funcargs.getfuncargnames(h) == ['arg1', 'arg2']
class A:
def f(self, arg1, arg2="hello"):
pass
assert funcargs.getfuncargnames(A().f) == ['arg1']
assert funcargs.getfuncargnames(A.f) == ['arg1']
class TestFillFuncArgs:
def test_funcarg_lookupfails(self, testdir): def test_funcarg_lookupfails(self, testdir):
testdir.makeconftest(""" testdir.makeconftest("""
class ConftestPlugin: class ConftestPlugin:
@ -8,7 +24,7 @@ class TestFuncargs:
return 42 return 42
""") """)
item = testdir.getitem("def test_func(some): pass") item = testdir.getitem("def test_func(some): pass")
exc = py.test.raises(LookupError, "item._setupfuncargs()") exc = py.test.raises(LookupError, "funcargs.fillfuncargs(item)")
s = str(exc.value) s = str(exc.value)
assert s.find("xyzsomething") != -1 assert s.find("xyzsomething") != -1
@ -18,21 +34,9 @@ class TestFuncargs:
def pytest_funcarg__some(self, request): def pytest_funcarg__some(self, request):
return request.function.__name__ return request.function.__name__
item.config.pluginmanager.register(Provider()) item.config.pluginmanager.register(Provider())
item._setupfuncargs() funcargs.fillfuncargs(item)
assert len(item.funcargs) == 1 assert len(item.funcargs) == 1
def test_funcarg_lookup_default_gets_overriden(self, testdir):
item = testdir.getitem("def test_func(some=42, other=13): pass")
class Provider:
def pytest_funcarg__other(self, request):
return request.function.__name__
item.config.pluginmanager.register(Provider())
item._setupfuncargs()
assert len(item.funcargs) == 1
name, value = item.funcargs.popitem()
assert name == "other"
assert value == item.name
def test_funcarg_basic(self, testdir): def test_funcarg_basic(self, testdir):
item = testdir.getitem("def test_func(some, other): pass") item = testdir.getitem("def test_func(some, other): pass")
class Provider: class Provider:
@ -41,7 +45,7 @@ class TestFuncargs:
def pytest_funcarg__other(self, request): def pytest_funcarg__other(self, request):
return 42 return 42
item.config.pluginmanager.register(Provider()) item.config.pluginmanager.register(Provider())
item._setupfuncargs() funcargs.fillfuncargs(item)
assert len(item.funcargs) == 2 assert len(item.funcargs) == 2
assert item.funcargs['some'] == "test_func" assert item.funcargs['some'] == "test_func"
assert item.funcargs['other'] == 42 assert item.funcargs['other'] == 42
@ -58,9 +62,9 @@ class TestFuncargs:
pass pass
""") """)
item1, item2 = testdir.genitems([modcol]) item1, item2 = testdir.genitems([modcol])
item1._setupfuncargs() funcargs.fillfuncargs(item1)
assert item1.funcargs['something'] == "test_method" assert item1.funcargs['something'] == "test_method"
item2._setupfuncargs() funcargs.fillfuncargs(item2)
assert item2.funcargs['something'] == "test_func" assert item2.funcargs['something'] == "test_func"
class TestRequest: class TestRequest:
@ -69,37 +73,44 @@ class TestRequest:
def pytest_funcarg__something(request): pass def pytest_funcarg__something(request): pass
def test_func(something): pass def test_func(something): pass
""") """)
req = item.getrequest("other") req = funcargs.FuncargRequest(item, argname="other")
assert req.argname == "other" assert req.argname == "other"
assert req.function == item.obj assert req.function == item.obj
assert hasattr(req.module, 'test_func')
assert req.cls is None
assert req.function.__name__ == "test_func" assert req.function.__name__ == "test_func"
assert req.config == item.config assert req.config == item.config
assert repr(req).find(req.function.__name__) != -1 assert repr(req).find(req.function.__name__) != -1
def test_request_contains_funcargs_methods(self, testdir): def test_request_attributes_method(self, testdir):
item, = testdir.getitems("""
class TestB:
def test_func(self, something):
pass
""")
req = funcargs.FuncargRequest(item, argname="something")
assert req.cls.__name__ == "TestB"
def test_request_contains_funcargs_provider(self, testdir):
modcol = testdir.getmodulecol(""" modcol = testdir.getmodulecol("""
def pytest_funcarg__something(request): def pytest_funcarg__something(request):
pass pass
class TestClass: class TestClass:
def pytest_funcarg__something(self, request):
pass
def test_method(self, something): def test_method(self, something):
pass pass
""") """)
item1, = testdir.genitems([modcol]) item1, = testdir.genitems([modcol])
assert item1.name == "test_method" assert item1.name == "test_method"
methods = item1.getrequest("something")._methods provider = funcargs.FuncargRequest(item1, "something")._provider
assert len(methods) == 2 assert len(provider) == 1
method1, method2 = methods assert provider[0].__name__ == "pytest_funcarg__something"
assert not hasattr(method1, 'im_self')
assert method2.im_self is not None
def test_request_call_next_provider(self, testdir): def test_request_call_next_provider(self, testdir):
item = testdir.getitem(""" item = testdir.getitem("""
def pytest_funcarg__something(request): pass def pytest_funcarg__something(request): pass
def test_func(something): pass def test_func(something): pass
""") """)
req = item.getrequest("something") req = funcargs.FuncargRequest(item, "something")
val = req.call_next_provider() val = req.call_next_provider()
assert val is None assert val is None
py.test.raises(req.Error, "req.call_next_provider()") py.test.raises(req.Error, "req.call_next_provider()")
@ -109,22 +120,147 @@ class TestRequest:
def pytest_funcarg__something(request): pass def pytest_funcarg__something(request): pass
def test_func(something): pass def test_func(something): pass
""") """)
req = item.getrequest("something") req = funcargs.FuncargRequest(item, "something")
l = [1] l = [1]
req.addfinalizer(l.pop) req.addfinalizer(l.pop)
item.teardown() item.teardown()
def test_request_maketemp(self, testdir):
item = testdir.getitem("def test_func(): pass")
req = item.getrequest("xxx")
tmpdir = req.maketempdir()
tmpdir2 = req.maketempdir()
assert tmpdir != tmpdir2
assert tmpdir.basename.startswith("test_func")
assert tmpdir2.basename.startswith("test_func")
def test_request_getmodulepath(self, testdir): def test_request_getmodulepath(self, testdir):
modcol = testdir.getmodulecol("def test_somefunc(): pass") modcol = testdir.getmodulecol("def test_somefunc(): pass")
item, = testdir.genitems([modcol]) item, = testdir.genitems([modcol])
req = item.getrequest("hello") req = funcargs.FuncargRequest(item, "xxx")
assert req.fspath == modcol.fspath assert req.fspath == modcol.fspath
class TestRunSpecs:
def test_no_funcargs(self, testdir):
def function(): pass
runspec = funcargs.RunSpecs(function)
assert not runspec.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
def test_addfuncarg_basic(self):
def func(arg1): pass
runspec = funcargs.RunSpecs(func)
py.test.raises(ValueError, """
runspec.addfuncarg("notexists", 100)
""")
runspec.addfuncarg("arg1", 100)
assert len(runspec._combinations) == 1
assert runspec._combinations[0] == {'arg1': 100}
def test_addfuncarg_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}
class TestGenfuncFunctional:
def test_attributes(self, testdir):
p = testdir.makepyfile("""
import py
def pytest_genfuncruns(runspec):
runspec.addfuncarg("runspec", runspec)
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
class TestClass:
def test_method(self, runspec):
assert runspec.config == py.test.config
assert runspec.module.__name__ == __name__
# XXX actually have the unbound test function here?
assert runspec.function == TestClass.test_method.im_func
assert runspec.cls == TestClass
""")
result = testdir.runpytest(p, "-v")
result.stdout.fnmatch_lines([
"*2 passed in*",
])
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)
""")
p = testdir.makepyfile("""
def test_myfunc(arg):
assert arg == 10
""")
result = testdir.runpytest("-v", p)
assert result.stdout.fnmatch_lines([
"*test_myfunc*PASS*", # case for 10
"*test_myfunc*FAIL*", # case for 20
"*1 failed, 1 passed*"
])
def test_two_functions(self, testdir):
p = testdir.makepyfile("""
def pytest_genfuncruns(runspec):
runspec.addfuncarg("arg1", 10)
runspec.addfuncarg("arg1", 20)
def test_func1(arg1):
assert arg1 == 10
def test_func2(arg1):
assert arg1 in (10, 20)
""")
result = testdir.runpytest("-v", p)
assert result.stdout.fnmatch_lines([
"*test_func1*0*PASS*",
"*test_func1*1*FAIL*",
"*test_func2*PASS*",
"*1 failed, 3 passed*"
])
def test_genfuncarg_inmodule(self, testdir):
testdir.makeconftest("""
class ConftestPlugin:
def pytest_genfuncruns(self, runspec):
assert "arg" in runspec.funcargnames
runspec.addfuncarg("arg", 10)
""")
p = testdir.makepyfile("""
def pytest_genfuncruns(runspec):
runspec.addfuncarg("arg2", 10)
runspec.addfuncarg("arg2", 20)
runspec.addfuncarg("classarg", 17)
class TestClass:
def test_myfunc(self, arg, arg2, classarg):
assert classarg == 17
assert arg == arg2
""")
result = testdir.runpytest("-v", p)
assert result.stdout.fnmatch_lines([
"*test_myfunc*0*PASS*",
"*test_myfunc*1*FAIL*",
"*1 failed, 1 passed*"
])

View File

@ -34,9 +34,9 @@ class ImmutablePickleTransport:
p2config._initafterpickle(config.topdir) p2config._initafterpickle(config.topdir)
return p2config return p2config
class TestImmutablePickling: pytest_funcarg__pickletransport = ImmutablePickleTransport
pytest_funcarg__pickletransport = ImmutablePickleTransport
class TestImmutablePickling:
def test_pickle_config(self, testdir, pickletransport): def test_pickle_config(self, testdir, pickletransport):
config1 = testdir.parseconfig() config1 = testdir.parseconfig()
assert config1.topdir == testdir.tmpdir assert config1.topdir == testdir.tmpdir

View File

@ -215,6 +215,12 @@ class TestGenerator:
assert not skipped and not failed assert not skipped and not failed
class TestFunction: class TestFunction:
def test_getmodulecollector(self, testdir):
item = testdir.getitem("def test_func(): pass")
modcol = item.getmodulecollector()
assert isinstance(modcol, py.test.collect.Module)
assert hasattr(modcol.obj, 'test_func')
def test_function_equality(self, tmpdir): def test_function_equality(self, tmpdir):
config = py.test.config._reparse([tmpdir]) config = py.test.config._reparse([tmpdir])
f1 = py.test.collect.Function(name="name", config=config, f1 = py.test.collect.Function(name="name", config=config,