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
temporary (sub) directories. You can create such directories
like this:
temporary (sub) directories through the config object.
You can create directories like this:
.. XXX use a more local example, just with "config"
.. 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
state for use by Python test functions. It is particularly useful
for functional and integration testing but also for unit testing.
Using funcargs you can easily:
Since version 1.0 it is possible to provide arguments to test functions,
often called "funcargs". The funcarg mechanisms were developed with
these goals in mind:
* write self-contained, simple to read and debug test functions
* cleanly encapsulate glue code between your app and your tests
* setup test state depending on command line options or environment
* **no boilerplate**: cleanly encapsulate test setup and fixtures
* **flexibility**: easily 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:
:depth: 2
The basic funcarg request/provide mechanism
Basic mechanisms by example
=============================================
To use funcargs you only need to specify
a named argument for your test function:
providing single function arguments as needed
---------------------------------------------------------
Let's look at a simple example of using funcargs within a test module:
.. sourcecode:: python
def test_function(myarg):
# use myarg
def pytest_funcarg__myfuncarg(request):
return 42
For each test function that requests this ``myarg``
argument a matching so called funcarg provider
will be invoked. A Funcarg provider for ``myarg``
is written down liks this:
def test_function(myfuncarg):
assert myfuncarg == 42
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
def pytest_funcarg__myarg(self, request):
# return value for myarg here
def pytest_genfuncruns(runspec):
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,
test module or on a local or global plugin.
The method is recognized by the ``pytest_funcarg__``
prefix and is correlated to the argument
name which follows this prefix. The passed in
``request`` object allows to interact
with test configuration, test collection
and test running aspects.
def test_function(arg1):
assert myfuncarg in (10, 20, 30)
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.
2. Subsequently the ``test_function()`` will be called three times
with three different values for ``arg1``.
Funcarg rules and support objects
====================================
.. _`request object`:
@ -65,11 +89,13 @@ Attributes of request objects
``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
finalizing after test function executed
cleanup after test function execution
++++++++++++++++++++++++++++++++++++++++
Request objects allow to **register a finalizer method** which is
@ -86,33 +112,8 @@ function finish:
request.addfinalizer(lambda: myfile.close())
return myfile
a unique temporary directory
++++++++++++++++++++++++++++++++++++++++
request objects allow to create unique temporary
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
decorating other funcarg providers
++++++++++++++++++++++++++++++++++++++++
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.
.. _`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
lookup order for provider methods:
1. test class (if we are executing a method)
2. test module
3. local plugins
4. global plugins
Both test generators as well as funcarg providers
are looked up in the following three scopes:
1. test module
2. local plugins
3. global plugins
Using multiple funcargs
----------------------------------------
A test function may receive more than one
function arguments. For each of the
function arguments a lookup of a
matching provider will be performed.
Test functions can have multiple arguments
which can either come from a test generator
or from a provider.
.. _`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
@ -202,7 +243,7 @@ following code into a local ``conftest.py``:
return MyApp()
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``:
@ -265,29 +306,8 @@ Now any test functions can use the ``mysetup.getsshconnection()`` method like th
conn = mysetup.getsshconnection()
# work with conn
Running this without the command line will yield this run result::
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):
# ...
#
Running this without specifying a command line option will result in a skipped
test_function.
.. _`accept example`:
@ -309,16 +329,12 @@ example: specifying and selecting acceptance tests
def __init__(self, request):
if not request.config.option.acceptance:
py.test.skip("specify -A to run acceptance tests")
self.tmpdir = request.config.maketempdir(request.argname)
self._old = self.tmpdir.chdir()
request.addfinalizer(self.finalize)
self.tmpdir = request.config.mktemp(request.function.__name__, numbered=True)
def run(self):
return py.process.cmdexec("echo hello")
def finalize(self):
self._old.chdir()
# cleanup any other resources
def run(self, cmd):
""" called by test code to execute an acceptance test. """
self.tmpdir.chdir()
return py.process.cmdexec(cmd)
and the actual test function example:
@ -327,48 +343,116 @@ and the actual test function example:
def test_some_acceptance_aspect(accept):
accept.tmpdir.mkdir("somesub")
result = accept.run()
assert 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 ...
result = accept.run("ls -la")
assert "somesub" in result
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`:
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
a funcarg just for a particular test module or even
a particular test class. We can extend the `accept example`_
by putting this in our test class:
a funcarg just for a particular test module. We can
extend the `accept example`_ by putting this in our test class:
.. sourcecode:: python
class TestSpecialAcceptance:
def pytest_funcarg__accept(self, request):
arg = request.call_next_provider()
# create a special layout in our tempdir
arg.tmpdir.mkdir("special")
return arg
class TestSpecialAcceptance:
def test_sometest(self, accept):
assert accept.tmpdir.join("special").check()
According to the `funcarg lookup order`_ our class-specific provider will
be invoked first. Here, we just ask our request object to
call the next provider and decorate its result. This simple
According to the the `lookup order`_ our module level provider
will be invoked first and it can ask ask its request object to
call the next provider and then decorate its result. This
mechanism allows us to stay ignorant of how/where the
function argument is provided.
Note that we make use here of `py.path.local`_ objects
that provide uniform access to the local filesystem.
sidenote: the temporary directory used here are instances of
the `py.path.local`_ class which provides many of the os.path
methods in a convenient way.
.. _`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
==================================
@ -377,14 +461,14 @@ Questions and Answers
Why ``pytest_funcarg__*`` methods?
------------------------------------
When experimenting with funcargs we also considered an explicit
registration mechanism, i.e. calling a register method e.g. on the
config object. But lacking a good use case for this indirection and
flexibility we decided to go for `Convention over Configuration`_
and allow to directly specify the provider. It has the
positive implication that you should be able to
"grep" for ``pytest_funcarg__MYARG`` and will find all
providing sites (usually exactly one).
When experimenting with funcargs we also
considered an explicit registration mechanism, i.e. calling a register
method on the config object. But lacking a good use case for this
indirection and flexibility we decided to go for `Convention over
Configuration`_ and allow to directly specify the provider. It has the
positive implication that you should be able to "grep" for
``pytest_funcarg__MYARG`` and will find all providing sites (usually
exactly one).
.. _`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.
funcargs_: powerful parametrized test function setup
`distributed testing`_: distribute test runs to other machines and platforms.
plugins_: using available plugins.
extend_: writing plugins and advanced configuration.
`distributed testing`_ how to distribute test runs to other machines and platforms.
.. _quickstart: quickstart.html
.. _features: features.html
.. _funcargs: funcargs.html
.. _plugins: plugins.html
.. _extend: ext.html
.. _`distributed testing`: dist.html

View File

@ -123,10 +123,14 @@ class Hooks:
return "<Hooks %r %r>" %(self._hookspecs, self._plugins)
class HookCall:
def __init__(self, registry, name, firstresult):
def __init__(self, registry, name, firstresult, extralookup=None):
self.registry = registry
self.name = name
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):
mode = self.firstresult and "firstresult" or "each"
@ -136,7 +140,8 @@ class HookCall:
if args:
raise TypeError("only keyword arguments allowed "
"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)
comregistry = Registry()

View File

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

View File

@ -190,3 +190,23 @@ class TestHooks:
class Api: pass
mcm = Hooks(hookspecs=Api)
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:
def __init__(self, request):
basetemp = request.maketempdir()
basetemp = basetemp.mkdir(request.function.__name__)
basetemp = request.config.mktemp(
"mysetup:%s" % request.function.__name__,
numbered=True)
self.source = basetemp.mkdir("source")
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. """
pytest_pycollect_obj.firstresult = True
def pytest_genfuncruns(self, runspec):
""" generate (multiple) parametrized calls to a test function."""
def pytest_collectstart(self, collector):
""" collector starts collecting. """
def pytest_collectreport(self, rep):
""" collector finished collecting. """
# ------------------------------------------------------------------------------
# runtest related hooks
# ------------------------------------------------------------------------------

View File

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

View File

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

View File

@ -20,6 +20,7 @@ import py
from py.__.test.collect import configproperty, warnoldcollect
from py.__.code.source import findsource
pydir = py.path.local(py.__file__).dirpath()
from py.__.test import funcargs
class PyobjMixin(object):
def obj():
@ -37,6 +38,16 @@ class PyobjMixin(object):
def _getobj(self):
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):
""" return python path relative to the containing module. """
chain = self.listchain()
@ -150,9 +161,24 @@ class PyCollectorMixin(PyobjMixin, py.test.collect.Collector):
if res is not None:
return res
if obj.func_code.co_flags & 32: # generator function
# XXX deprecation warning
return self.Generator(name, parent=self)
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 funcargs.FunctionCollector(name=name,
parent=self, combinations=runspec._combinations)
class Module(py.test.collect.File, PyCollectorMixin):
def _getobj(self):
@ -320,11 +346,13 @@ class Function(FunctionMixin, py.test.collect.Item):
""" a Function Item is responsible for setting up
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)
self._finalizers = []
self._args = args
self.funcargs = {}
if funcargs is None:
funcargs = {}
self.funcargs = funcargs
if callobj is not _dummy:
self._obj = callobj
@ -350,31 +378,7 @@ class Function(FunctionMixin, py.test.collect.Item):
def setup(self):
super(Function, self).setup()
self._setupfuncargs()
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()
funcargs.fillfuncargs(self)
def __eq__(self, other):
try:
@ -385,74 +389,7 @@ class Function(FunctionMixin, py.test.collect.Item):
except AttributeError:
pass
return False
def __ne__(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
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):
testdir.makeconftest("""
class ConftestPlugin:
@ -8,7 +24,7 @@ class TestFuncargs:
return 42
""")
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)
assert s.find("xyzsomething") != -1
@ -18,21 +34,9 @@ class TestFuncargs:
def pytest_funcarg__some(self, request):
return request.function.__name__
item.config.pluginmanager.register(Provider())
item._setupfuncargs()
funcargs.fillfuncargs(item)
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):
item = testdir.getitem("def test_func(some, other): pass")
class Provider:
@ -41,7 +45,7 @@ class TestFuncargs:
def pytest_funcarg__other(self, request):
return 42
item.config.pluginmanager.register(Provider())
item._setupfuncargs()
funcargs.fillfuncargs(item)
assert len(item.funcargs) == 2
assert item.funcargs['some'] == "test_func"
assert item.funcargs['other'] == 42
@ -58,9 +62,9 @@ class TestFuncargs:
pass
""")
item1, item2 = testdir.genitems([modcol])
item1._setupfuncargs()
funcargs.fillfuncargs(item1)
assert item1.funcargs['something'] == "test_method"
item2._setupfuncargs()
funcargs.fillfuncargs(item2)
assert item2.funcargs['something'] == "test_func"
class TestRequest:
@ -69,37 +73,44 @@ class TestRequest:
def pytest_funcarg__something(request): pass
def test_func(something): pass
""")
req = item.getrequest("other")
req = funcargs.FuncargRequest(item, argname="other")
assert req.argname == "other"
assert req.function == item.obj
assert hasattr(req.module, 'test_func')
assert req.cls is None
assert req.function.__name__ == "test_func"
assert req.config == item.config
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("""
def pytest_funcarg__something(request):
pass
class TestClass:
def pytest_funcarg__something(self, request):
pass
def test_method(self, something):
pass
""")
item1, = testdir.genitems([modcol])
assert item1.name == "test_method"
methods = item1.getrequest("something")._methods
assert len(methods) == 2
method1, method2 = methods
assert not hasattr(method1, 'im_self')
assert method2.im_self is not None
provider = funcargs.FuncargRequest(item1, "something")._provider
assert len(provider) == 1
assert provider[0].__name__ == "pytest_funcarg__something"
def test_request_call_next_provider(self, testdir):
item = testdir.getitem("""
def pytest_funcarg__something(request): pass
def test_func(something): pass
""")
req = item.getrequest("something")
req = funcargs.FuncargRequest(item, "something")
val = req.call_next_provider()
assert val is None
py.test.raises(req.Error, "req.call_next_provider()")
@ -109,22 +120,147 @@ class TestRequest:
def pytest_funcarg__something(request): pass
def test_func(something): pass
""")
req = item.getrequest("something")
req = funcargs.FuncargRequest(item, "something")
l = [1]
req.addfinalizer(l.pop)
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):
modcol = testdir.getmodulecol("def test_somefunc(): pass")
item, = testdir.genitems([modcol])
req = item.getrequest("hello")
req = funcargs.FuncargRequest(item, "xxx")
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)
return p2config
class TestImmutablePickling:
pytest_funcarg__pickletransport = ImmutablePickleTransport
class TestImmutablePickling:
def test_pickle_config(self, testdir, pickletransport):
config1 = testdir.parseconfig()
assert config1.topdir == testdir.tmpdir

View File

@ -215,6 +215,12 @@ class TestGenerator:
assert not skipped and not failed
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):
config = py.test.config._reparse([tmpdir])
f1 = py.test.collect.Function(name="name", config=config,