From e9b8e4141a66e618d84170256eaf8d18326eb389 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 13 Apr 2009 15:00:00 +0200 Subject: [PATCH 01/18] update docs --HG-- branch : trunk --- doc/path.txt | 2 + doc/test/ext.txt | 19 ++- doc/test/funcargs.txt | 351 ++++++++++++++++++++++++++++++------------ 3 files changed, 271 insertions(+), 101 deletions(-) diff --git a/doc/path.txt b/doc/path.txt index a4716922e..bab3eea64 100644 --- a/doc/path.txt +++ b/doc/path.txt @@ -11,6 +11,8 @@ provides a number of implementations of this API. Path implementations provided by :api:`py.path` =============================================== +.. _`local`: + :api:`py.path.local` -------------------- diff --git a/doc/test/ext.txt b/doc/test/ext.txt index edf1d7438..3ae7c7b15 100644 --- a/doc/test/ext.txt +++ b/doc/test/ext.txt @@ -1,7 +1,20 @@ +====================================== +Writing plugins and extensions +====================================== + + +.. _`local plugin`: + +Local Plugins +================================== + +You can easily specify a project-specific or "local" +plugin by defining a ``ConftestPlugin`` in a ``conftest.py`` +file like this:: + + class ConftestPlugin: + """ my local plugin. """ -=============== -Writing plugins -=============== Learning by examples ===================== diff --git a/doc/test/funcargs.txt b/doc/test/funcargs.txt index 28bc0b264..a24b97e9c 100644 --- a/doc/test/funcargs.txt +++ b/doc/test/funcargs.txt @@ -1,142 +1,244 @@ +====================================================== +**funcargs**: powerful and simple test setup +====================================================== -===================================== -Python test function arguments -===================================== +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: -py.test enables a new way to separate test configuration -and test setup from actual test code in test functions. -When it runs a test functions it will lookup function -arguments by name and provide a value. -Here is a simple example for such a test function: +* write self-contained, simple to read and debug test functions +* cleanly encapsulate glue code between your app and your tests +* do test scenario setup dependent on command line opts or environment - def test_function(mysetup): - # work with mysetup - -To provide a value py.test looks for a ``pytest_funcargs`` -dictionary in the test module, for example:: +The basic funcarg request/provide mechanism +============================================= - class MySetup: - def __init__(self, pyfuncitem): - self.pyfuncitem = pyfuncitem - pytest_funcargs = {'mysetup': MySetup} +All you need to do from a test function or test method +is to specify an argument for your test function: -This is already enough to run the test. Of course -up until now our ``mysetup`` does not provide -much value. But it is now easy to add new -methods on the ``MySetup`` class that have -full access to the test collection process. +.. sourcecode:: python -Plugins can register their funcargs via -the config object, usually upon initial configure:: + def test_function(myarg): + # use myarg + +For each test function that requests the ``myarg`` +argument a matching so called funcarg provider +will be invoked. A Funcarg provider for ``myarg`` +is written down liks this: + +.. sourcecode:: python + + def pytest_funcarg__myarg(self, request): + # return value for myarg here + +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. Because it +has access to the "request" object a provider +method is a uniquely powerful place for +containing setup up of test scenarios and +test configuration. + +.. _`request object`: + +request objects +------------------------ + +Request objects give access to command line options, +the underlying python function and the test running +process. Each funcarg provider method receives a ``request`` object +that allows interaction with the test method and test +running process. Basic attributes:: + + argname: requested argument name + function: python function object requesting the argument + config: access to command line opts and general config + +Request objects have a ``addfinalizer`` method that +allows to **register a finalizer method** which is +called after a test function has finished running. +This is useful for tearing down or cleaning up +test state. Here is a basic example for providing +a ``myfile`` object that will be closed upon test +function finish: + +.. sourcecode:: python + + def pytest_funcarg__myfile(self, request): + # ... create and open a "myfile" object ... + request.addfinalizer(lambda: myfile.close()) + return myfile + +If you want to **decorate a function argument** that is +provided elsewhere you can use the ``call_next_provider`` +method to obtain the "next" value: + +.. sourcecode:: python + + def pytest_funcarg__myfile(self, request): + myfile = request.call_next_provider() + # do something extra + return myfile + +This will raise a ``request.Error`` exception if there +is no next provider left. See the `decorator example`_ +for a use of this method. + +.. _`lookup order`: + +Order of funcarg provider 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 + + +Funcarg Examples +===================== + +Example: basic application specific setup +----------------------------------------------------- + +Here is a basic useful example for handling application +specific setup. The goal is to have one place where +we have the glue code for bootstrapping and configuring +application objects and allow test modules and +test functions to stay ignorant of involved details. +Let's start with the using side and consider a simple +test function living in a test file ``test_sample.py``: + +.. sourcecode:: python + + def test_answer(mysetup): + app = mysetup.myapp() + answer = app.question() + assert answer == 42 + +To run this test py.test looks up and calls a provider to obtain the +required "mysetup" function argument. The test function simply +interacts with the provided application specific setup. + +To provide the ``mysetup`` function argument we write down +a provider method in a `local plugin`_ by putting this +into a local ``conftest.py``: + +.. sourcecode:: python + + from myapp import MyApp class ConftestPlugin: - def pytest_configure(self, config): - config.register_funcargs(mysetup=MySetup) + def pytest_funcarg__mysetup(self, request): + return MySetup() + + class MySetup: + def myapp(self): + return MyApp() + +The ``pytest_funcarg__mysetup`` method is called to +provide a value for the test function argument. +To complete the example we put a pseudo MyApp object +into ``myapp.py``: + +.. sourcecode:: python + + class MyApp: + def question(self): + return 6 * 9 + +.. _`local plugin`: test-ext.html#local-plugin + +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 also put such a function into a test class like this: + +.. sourcecode:: python + + class TestClass: + def pytest_funcarg__mysetup(self, request): + # ... + # + + +Example: command line option for providing SSH-host +----------------------------------------------------------- If you provide a "funcarg" from a plugin you can easily make methods depend on command line options or environment settings. Here is a complete example that allows to run tests involving -an SSH connection if an ssh host is specified:: +an SSH connection if an ssh host is specified: + +.. sourcecode:: python class ConftestPlugin: def pytest_addoption(self, parser): parser.addoption("--ssh", action="store", default=None, help="specify ssh host to run tests with") - def pytest_configure(self, config): - config.register_funcargs(mysetup=MySetup) + pytest_funcarg__mysetup = MySetupFuncarg - class MySetup: - def __init__(self, pyfuncitem): - self.pyfuncitem = pyfuncitem + class MySetupFuncarg: + def __init__(self, request): + self.request = request def ssh_gateway(self): - host = pyfuncitem.config.option.ssh + host = self.request.config.option.ssh if host is None: py.test.skip("specify ssh host with --ssh to run this test") return py.execnet.SshGateway(host) -Now any test functions can use the "mysetup" object, for example:: +Now any test functions can use the "mysetup.ssh_gateway()" method like this: + +.. sourcecode:: python class TestClass: def test_function(self, mysetup): ssh_gw = mysetup.ssh_gateway() # work with ssh_gw - -Without specifying a command line option the output looks like this:: + +Running this without the command line will yield this run result:: ... -Lookup rules -====================== +.. _`accept example`: -In order to run this test function a value for the -``mysetup`` needs to be found. Here is how py.test -finds a matching provider function: +example: specifying and selecting acceptance tests +-------------------------------------------------------------- -1. see if there is a ``pytest_funcargs`` dictionary - which maps ``mysetup`` to a provider function. - if so, call the provider function. +.. sourcecode:: python -XXX - - - -example -===================== - -You can run a test file ``test_some.py`` with this content: - - pytest_funcargs = {'myarg': (lambda pyfuncitem: 42)} - - def test_something(myarg): - assert myarg == 42 - -You can also put this on a class: - - class TestClass: - pytest_funcargs = {'myarg': (lambda pyfuncitem: 42)} - - def test_something(self, myarg): - assert myarg == 42 - -To separate funcarg setup you can also put a funcarg -definition into a conftest.py:: - - pytest_funcargs = {'myarg': decorate_myarg} - def decorate_myarg(pyfuncitem): - result = pyfuncitem.call_next_provider() - return result + 1 - -for registering funcargs from a plugin, talk to the -test configuration object like this:: - - class MyPlugin: - def pytest_configure(self, config): - config.register_funcargs( - myarg=decorate_myarg - ) - -a local helper funcarg for doing acceptance tests maybe -by running shell commands could look like this:: - - class MyPlugin: + class ConftestPlugin: def pytest_option(self, parser): - group = parser.addgroup("myproject acceptance tests") + group = parser.getgroup("myproject") group.addoption("-A", dest="acceptance", action="store_true", help="run (slow) acceptance tests") - def pytest_configure(self, config): - config.register_funcargs(accept=AcceptFuncarg) + def pytest_funcarg__accept(self, request): + return AcceptFuncarg(request) class AcceptFuncarg: - def __init__(self, pyfuncitem): - if not pyfuncitem.config.option.acceptance: + def __init__(self, request): + if not request.config.option.acceptance: py.test.skip("specify -A to run acceptance tests") - self.tmpdir = pyfuncitem.config.maketempdir(pyfuncitem.name) + self.tmpdir = request.config.maketempdir(request.argname) self._old = self.tmpdir.chdir() - pyfuncitem.addfinalizer(self.finalize) + request.addfinalizer(self.finalize) def run(self): return py.process.cmdexec("echo hello") @@ -144,17 +246,70 @@ by running shell commands could look like this:: def finalize(self): self._old.chdir() # cleanup any other resources + and the actual test function example: +.. sourcecode:: python + def test_some_acceptance_aspect(accept): accept.tmpdir.mkdir("somesub") result = accept.run() assert result - -for registering funcargs from a plugin, talk to the -test configuration object like this:: + +That's it! This test will get automatically skipped with +an appropriate message if you just run ``py.test``:: - XXX + ... OUTPUT of py.test on this example ... +.. _`decorator example`: + +example: decorating/extending a funcarg in a TestClass +-------------------------------------------------------------- + +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: + +.. 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 + + def test_sometest(self, accept): + assert accept.tmpdir.join("special").check() + +According to the `lookup order`_ our class-specific provider will +be invoked first. Here, we just ask our request object to +call the next provider and decoare its result. This simple +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. + +.. _`py.path.local`: path.html#local + +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). + +.. _`Convention over Configuration`: http://en.wikipedia.org/wiki/Convention_over_Configuration + From fe59ed04d5acca363a91fbf70bae4fadf218327e Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 13 Apr 2009 15:09:12 +0200 Subject: [PATCH 02/18] small updates --HG-- branch : trunk --- .hgignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgignore b/.hgignore index 22216bfc3..ef2f17e0a 100644 --- a/.hgignore +++ b/.hgignore @@ -9,3 +9,4 @@ syntax:glob syntax:glob *.pyc *.pyo +*.html From 4a33940d82ac1cd1d569a62ccecdeb6b9cffd3de Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 13 Apr 2009 16:29:06 +0200 Subject: [PATCH 03/18] section on xUnit style setups, better linking, some streamlining --HG-- branch : trunk --- doc/test/features.txt | 14 +++++++- doc/test/funcargs.txt | 4 +-- doc/test/statemanage.txt | 65 ----------------------------------- doc/test/xunit_setup.txt | 73 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 68 deletions(-) delete mode 100644 doc/test/statemanage.txt create mode 100644 doc/test/xunit_setup.txt diff --git a/doc/test/features.txt b/doc/test/features.txt index 68e0b2a71..1a535d162 100644 --- a/doc/test/features.txt +++ b/doc/test/features.txt @@ -36,8 +36,20 @@ or class with a leading ``Test`` name is collected. Rapidly write integration, functional, unit tests =================================================== -py.test provides +XXX +Unique test state setup/fixture methods +=================================================== + +py.test provides the `unique funcargs mechanism`_ +for constructing test fixtures and handling +complex test scenarios. You can also +use `traditional xUnit style setup`_ for +existing code bases or if you prefer +it for your unit tests. + +.. _`unique funcargs mechanism`: funcargs.html +.. _`traditional xUnit style setup`: xunit_setup.html load-balance tests to multiple CPUs =================================== diff --git a/doc/test/funcargs.txt b/doc/test/funcargs.txt index a24b97e9c..07b9ada03 100644 --- a/doc/test/funcargs.txt +++ b/doc/test/funcargs.txt @@ -153,7 +153,7 @@ into ``myapp.py``: def question(self): return 6 * 9 -.. _`local plugin`: test-ext.html#local-plugin +.. _`local plugin`: ext.html#local-plugin Example: specifying funcargs in test modules or classes --------------------------------------------------------- @@ -294,7 +294,7 @@ function argument is provided. Note that we make use here of `py.path.local`_ objects that provide uniform access to the local filesystem. -.. _`py.path.local`: path.html#local +.. _`py.path.local`: ../path.html#local Questions and Answers ================================== diff --git a/doc/test/statemanage.txt b/doc/test/statemanage.txt deleted file mode 100644 index a33383e81..000000000 --- a/doc/test/statemanage.txt +++ /dev/null @@ -1,65 +0,0 @@ -================= -Managing state -================= - -funcargs: provding arguments for test functions -=========================================================== - -XXX write docs - -Managing test state across test modules, classes and methods -============================================================ - -Often you want to create some files, database connections or other -state in order to run tests in a certain environment. With -``py.test`` there are three scopes for which you can provide hooks to -manage such state. Again, ``py.test`` will detect these hooks in -modules on a name basis. The following module-level hooks will -automatically be called by the session:: - - def setup_module(module): - """ setup up any state specific to the execution - of the given module. - """ - - def teardown_module(module): - """ teardown any state that was previously setup - with a setup_module method. - """ - -The following hooks are available for test classes:: - - def setup_class(cls): - """ setup up any state specific to the execution - of the given class (which usually contains tests). - """ - - def teardown_class(cls): - """ teardown any state that was previously setup - with a call to setup_class. - """ - - def setup_method(self, method): - """ setup up any state tied to the execution of the given - method in a class. setup_method is invoked for every - test method of a class. - """ - - def teardown_method(self, method): - """ teardown any state that was previously setup - with a setup_method call. - """ - -The last two hooks, ``setup_method`` and ``teardown_method``, are -equivalent to ``setUp`` and ``tearDown`` in the Python standard -library's ``unitest`` module. - -All setup/teardown methods are optional. You could have a -``setup_module`` but no ``teardown_module`` and the other way round. - -Note that while the test session guarantees that for every ``setup`` a -corresponding ``teardown`` will be invoked (if it exists) it does -*not* guarantee that any ``setup`` is called only happens once. For -example, the session might decide to call the ``setup_module`` / -``teardown_module`` pair more than once during the execution of a test -module. diff --git a/doc/test/xunit_setup.txt b/doc/test/xunit_setup.txt new file mode 100644 index 000000000..d8d8756df --- /dev/null +++ b/doc/test/xunit_setup.txt @@ -0,0 +1,73 @@ +==================================== +xUnit style setup +==================================== + +.. _`funcargs`: funcargs.html +.. _`xUnit`: http://en.wikipedia.org/wiki/XUnit + +** Note that since version 1.0 py.test offers `funcargs`_ +for simple and complex test setup needs. Especially +if you want to do functional and integration testing, +but also fo runittesting, it is highly recommended +that you consider using `funcargs`_ for keeping +a clean and maintenable test suite. ** + +Python, Java and other languages have a tradition +of using xUnit_ style testing. This typically +involves the call of a ``setup`` method before +a test function is run and ``teardown`` after +it finishes. With ``py.test`` there are three +scopes for which you can provide setup/teardown +hooks to provide test fixtures: per-module, per-class +and per-method/function. ``py.test`` will +discover and call according methods automatically. +All setup/teardown methods are optional. + +The following methods are called at module level if they exist: + +.. sourcecode:: python + + def setup_module(module): + """ setup up any state specific to the execution + of the given module. + """ + + def teardown_module(module): + """ teardown any state that was previously setup + with a setup_module method. + """ + +The following hooks are available for test classes: + +.. sourcecode:: python + + def setup_class(cls): + """ setup up any state specific to the execution + of the given class (which usually contains tests). + """ + + def teardown_class(cls): + """ teardown any state that was previously setup + with a call to setup_class. + """ + + def setup_method(self, method): + """ setup up any state tied to the execution of the given + method in a class. setup_method is invoked for every + test method of a class. + """ + + def teardown_method(self, method): + """ teardown any state that was previously setup + with a setup_method call. + """ + +The last two hooks, ``setup_method`` and ``teardown_method``, are +equivalent to ``setUp`` and ``tearDown`` in the Python standard +library's `unittest.py module`_. + +Note that it possible that setup/teardown pairs are invoked multiple +times per testing process. + +.. _`unittest.py module`: http://docs.python.org/library/unittest.html + From 2a72f75d58c0b801fb3e443e0daccd12be0e1347 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 13 Apr 2009 16:35:23 +0200 Subject: [PATCH 04/18] refining formulations --HG-- branch : trunk --- doc/test/features.txt | 15 +++++++-------- doc/test/funcargs.txt | 3 +++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/doc/test/features.txt b/doc/test/features.txt index 1a535d162..fafd333ed 100644 --- a/doc/test/features.txt +++ b/doc/test/features.txt @@ -38,17 +38,16 @@ Rapidly write integration, functional, unit tests XXX -Unique test state setup/fixture methods +funcargs and xUnit style setups =================================================== -py.test provides the `unique funcargs mechanism`_ -for constructing test fixtures and handling -complex test scenarios. You can also -use `traditional xUnit style setup`_ for -existing code bases or if you prefer -it for your unit tests. +py.test provides powerful means for managing test +state and fixtures. Apart from the `traditional +xUnit style setup`_ for unittests it features the +simple and powerful `funcargs mechanism`_ for handling +both complex and simple test scenarious. -.. _`unique funcargs mechanism`: funcargs.html +.. _`funcargs mechanism`: funcargs.html .. _`traditional xUnit style setup`: xunit_setup.html load-balance tests to multiple CPUs diff --git a/doc/test/funcargs.txt b/doc/test/funcargs.txt index 07b9ada03..70d1c9aeb 100644 --- a/doc/test/funcargs.txt +++ b/doc/test/funcargs.txt @@ -11,6 +11,9 @@ Using funcargs you can: * cleanly encapsulate glue code between your app and your tests * do test scenario setup dependent on command line opts or environment +Using the funcargs mechanism will keep your test suite clean +and allow for easier refactoring of your applications. + The basic funcarg request/provide mechanism ============================================= From 46ee6beaa9bfc4987176ee371be02685865f927c Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 13 Apr 2009 16:37:22 +0200 Subject: [PATCH 05/18] streamline --HG-- branch : trunk --- doc/test/funcargs.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/test/funcargs.txt b/doc/test/funcargs.txt index 70d1c9aeb..f73960aa1 100644 --- a/doc/test/funcargs.txt +++ b/doc/test/funcargs.txt @@ -11,8 +11,9 @@ Using funcargs you can: * cleanly encapsulate glue code between your app and your tests * do test scenario setup dependent on command line opts or environment -Using the funcargs mechanism will keep your test suite clean -and allow for easier refactoring of your applications. +Using the funcargs mechanism will increase readability +and allow for easier refactoring of your application +and its test suites. The basic funcarg request/provide mechanism ============================================= From 1dc9ac62ca251f9ae2de3f1a1b4c41466a52bcb0 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 13 Apr 2009 19:36:58 +0200 Subject: [PATCH 06/18] refinements --HG-- branch : trunk --- doc/test/funcargs.txt | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/doc/test/funcargs.txt b/doc/test/funcargs.txt index f73960aa1..64ee2eb60 100644 --- a/doc/test/funcargs.txt +++ b/doc/test/funcargs.txt @@ -5,11 +5,11 @@ 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: +Using funcargs you can easily: * write self-contained, simple to read and debug test functions * cleanly encapsulate glue code between your app and your tests -* do test scenario setup dependent on command line opts or environment +* setup test state depending on command line options or environment Using the funcargs mechanism will increase readability and allow for easier refactoring of your application @@ -18,15 +18,15 @@ and its test suites. The basic funcarg request/provide mechanism ============================================= -All you need to do from a test function or test method -is to specify an argument for your test function: +To use funcargs you only need to specify +a named argument for your test function: .. sourcecode:: python def test_function(myarg): # use myarg -For each test function that requests the ``myarg`` +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: @@ -40,11 +40,10 @@ 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. Because it -has access to the "request" object a provider -method is a uniquely powerful place for -containing setup up of test scenarios and -test configuration. +name which follows this prefix. The passed in +"request" object allows to interact +with test configuration, test collection +and test running aspects. .. _`request object`: @@ -55,11 +54,13 @@ Request objects give access to command line options, the underlying python function and the test running process. Each funcarg provider method receives a ``request`` object that allows interaction with the test method and test -running process. Basic attributes:: +running process. Basic attributes of request objects: - argname: requested argument name - function: python function object requesting the argument - config: access to command line opts and general config +``request.argname``: name of the request argument + +``request.function``: python function object requesting the argument + +``request.config``: access to command line opts and general config Request objects have a ``addfinalizer`` method that allows to **register a finalizer method** which is @@ -105,6 +106,15 @@ lookup order for provider methods: 4. 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. + + Funcarg Examples ===================== From ee78661775f7e460380b554a17bcc801b5306abd Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 13 Apr 2009 19:51:42 +0200 Subject: [PATCH 07/18] refining docs further --HG-- branch : trunk --- doc/test/funcargs.txt | 20 ++++++++++++-------- example/funcarg/mysetup/conftest.py | 10 ++++++++++ example/funcarg/mysetup/myapp.py | 5 +++++ example/funcarg/mysetup/test_sample.py | 5 +++++ 4 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 example/funcarg/mysetup/conftest.py create mode 100644 example/funcarg/mysetup/myapp.py create mode 100644 example/funcarg/mysetup/test_sample.py diff --git a/doc/test/funcargs.txt b/doc/test/funcargs.txt index 64ee2eb60..d9cca7a4f 100644 --- a/doc/test/funcargs.txt +++ b/doc/test/funcargs.txt @@ -56,14 +56,13 @@ process. Each funcarg provider method receives a ``request`` object that allows interaction with the test method and test running process. Basic attributes of request objects: -``request.argname``: name of the request argument +``request.argname``: name of the requested function argument ``request.function``: python function object requesting the argument ``request.config``: access to command line opts and general config -Request objects have a ``addfinalizer`` method that -allows to **register a finalizer method** which is +Request objects allow to **register a finalizer method** which is called after a test function has finished running. This is useful for tearing down or cleaning up test state. Here is a basic example for providing @@ -78,8 +77,8 @@ function finish: return myfile If you want to **decorate a function argument** that is -provided elsewhere you can use the ``call_next_provider`` -method to obtain the "next" value: +provided elsewhere you can ask the request object +to provide the "next" value: .. sourcecode:: python @@ -141,8 +140,8 @@ required "mysetup" function argument. The test function simply interacts with the provided application specific setup. To provide the ``mysetup`` function argument we write down -a provider method in a `local plugin`_ by putting this -into a local ``conftest.py``: +a provider method in a `local plugin`_ by putting the +following code into a local ``conftest.py``: .. sourcecode:: python @@ -157,7 +156,7 @@ into a local ``conftest.py``: return MyApp() The ``pytest_funcarg__mysetup`` method is called to -provide a value for the test function argument. +provide a value for the requested ``mysetup`` test function argument. To complete the example we put a pseudo MyApp object into ``myapp.py``: @@ -167,8 +166,13 @@ into ``myapp.py``: def question(self): return 6 * 9 +You can now run the test with ``py.test test_sample.py``: + +.. sourcecode:: python + .. _`local plugin`: ext.html#local-plugin + Example: specifying funcargs in test modules or classes --------------------------------------------------------- diff --git a/example/funcarg/mysetup/conftest.py b/example/funcarg/mysetup/conftest.py new file mode 100644 index 000000000..c62df898d --- /dev/null +++ b/example/funcarg/mysetup/conftest.py @@ -0,0 +1,10 @@ + +from myapp import MyApp + +class ConftestPlugin: + def pytest_funcarg__mysetup(self, request): + return MySetup() + +class MySetup: + def myapp(self): + return MyApp() diff --git a/example/funcarg/mysetup/myapp.py b/example/funcarg/mysetup/myapp.py new file mode 100644 index 000000000..2ecd83d11 --- /dev/null +++ b/example/funcarg/mysetup/myapp.py @@ -0,0 +1,5 @@ + +class MyApp: + def question(self): + return 6 * 9 + diff --git a/example/funcarg/mysetup/test_sample.py b/example/funcarg/mysetup/test_sample.py new file mode 100644 index 000000000..167d16a47 --- /dev/null +++ b/example/funcarg/mysetup/test_sample.py @@ -0,0 +1,5 @@ + +def test_answer(mysetup): + app = mysetup.myapp() + answer = app.question() + assert answer == 42 From 5424d3ed285be33a964497960c169626911ae26f Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 13 Apr 2009 19:56:11 +0200 Subject: [PATCH 08/18] fixing ReST typo --HG-- branch : trunk --- doc/test/funcargs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/test/funcargs.txt b/doc/test/funcargs.txt index d9cca7a4f..b73765b0b 100644 --- a/doc/test/funcargs.txt +++ b/doc/test/funcargs.txt @@ -326,7 +326,7 @@ 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 +"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 From 2456c77e1e73f2363bdacf49a1f13290cd305d44 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 13 Apr 2009 20:11:14 +0200 Subject: [PATCH 09/18] refining docs --HG-- branch : trunk --- doc/test/funcargs.txt | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/doc/test/funcargs.txt b/doc/test/funcargs.txt index b73765b0b..491937f23 100644 --- a/doc/test/funcargs.txt +++ b/doc/test/funcargs.txt @@ -2,6 +2,7 @@ **funcargs**: powerful and simple test setup ====================================================== + 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. @@ -15,6 +16,9 @@ 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 ============================================= @@ -41,20 +45,20 @@ 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 +``request`` object allows to interact with test configuration, test collection -and test running aspects. +and test running aspects. .. _`request object`: -request objects +funcarg request objects ------------------------ -Request objects give access to command line options, -the underlying python function and the test running -process. Each funcarg provider method receives a ``request`` object -that allows interaction with the test method and test -running process. Basic attributes of request objects: +Request objects encapsulate a request for a function argument from a +specific test function. Request objects provide access to command line +options, the underlying python function and allow interaction +with other providers and the test running process. +Basic attributes of request objects: ``request.argname``: name of the requested function argument @@ -136,7 +140,7 @@ test function living in a test file ``test_sample.py``: assert answer == 42 To run this test py.test looks up and calls a provider to obtain the -required "mysetup" function argument. The test function simply +required ``mysetup`` function argument. The test function simply interacts with the provided application specific setup. To provide the ``mysetup`` function argument we write down @@ -305,7 +309,7 @@ by putting this in our test class: According to the `lookup order`_ our class-specific provider will be invoked first. Here, we just ask our request object to -call the next provider and decoare its result. This simple +call the next provider and decorate its result. This simple mechanism allows us to stay ignorant of how/where the function argument is provided. @@ -317,6 +321,8 @@ that provide uniform access to the local filesystem. Questions and Answers ================================== +.. _`why pytest_pyfuncarg__ methods?`: + Why ``pytest_funcarg__*`` methods? ------------------------------------ From 04aec935bd132a4c75e610145d4b35f066c7cb68 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 14 Apr 2009 00:05:23 +0200 Subject: [PATCH 10/18] fix note on funcargs --HG-- branch : trunk --- doc/test/xunit_setup.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/test/xunit_setup.txt b/doc/test/xunit_setup.txt index d8d8756df..aec020cb0 100644 --- a/doc/test/xunit_setup.txt +++ b/doc/test/xunit_setup.txt @@ -5,12 +5,12 @@ xUnit style setup .. _`funcargs`: funcargs.html .. _`xUnit`: http://en.wikipedia.org/wiki/XUnit -** Note that since version 1.0 py.test offers `funcargs`_ -for simple and complex test setup needs. Especially -if you want to do functional and integration testing, -but also fo runittesting, it is highly recommended -that you consider using `funcargs`_ for keeping -a clean and maintenable test suite. ** +Note: + + Since version 1.0 py.test offers funcargs_ for both + simple and complex test setup needs. Especially + for functional and integration, but also for unit testing, it is + highly recommended that you use this new method. Python, Java and other languages have a tradition of using xUnit_ style testing. This typically From 70840f605e9c230d5a100e998509e4d8e957b996 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 14 Apr 2009 02:23:42 +0200 Subject: [PATCH 11/18] move tests to test_funcargs.py and start with new request object tests --HG-- branch : trunk --- py/test/pluginmanager.py | 4 +- py/test/pycollect.py | 18 +++++- py/test/testing/test_funcargs.py | 103 ++++++++++++++++++++++++++++++ py/test/testing/test_pycollect.py | 82 ------------------------ 4 files changed, 121 insertions(+), 86 deletions(-) create mode 100644 py/test/testing/test_funcargs.py diff --git a/py/test/pluginmanager.py b/py/test/pluginmanager.py index b2aa238d6..08a480ea8 100644 --- a/py/test/pluginmanager.py +++ b/py/test/pluginmanager.py @@ -79,8 +79,8 @@ class PluginManager(object): for x in self.comregistry.listattr(attrname): return x - def listattr(self, attrname, plugins=None): - return self.comregistry.listattr(attrname, plugins=plugins) + def listattr(self, attrname, plugins=None, extra=()): + return self.comregistry.listattr(attrname, plugins=plugins, extra=extra) def call_firstresult(self, *args, **kwargs): return self.comregistry.call_firstresult(*args, **kwargs) diff --git a/py/test/pycollect.py b/py/test/pycollect.py index eac49c3b3..0d50ae04e 100644 --- a/py/test/pycollect.py +++ b/py/test/pycollect.py @@ -410,6 +410,20 @@ class Function(FunctionMixin, py.test.collect.Item): def __ne__(self, other): return not self == other + def getrequest(self, argname): + return FuncargRequest(pyfuncitem=self, argname=argname) + -# DEPRECATED -#from py.__.test.plugin.pytest_doctest import DoctestFile +class FuncargRequest: + def __init__(self, pyfuncitem, argname): + self.pyfuncitem = pyfuncitem + self.argname = argname + funcargname = "pytest_funcarg__" + str(argname) + pm = self.pyfuncitem.config.pluginmanager + extra = [] + current = pyfuncitem + while not isinstance(current, Module): + current = current.parent + if isinstance(current, (Instance, Module)): + extra.insert(0, current.obj) + self._methods = pm.listattr(funcargname, extra=extra) diff --git a/py/test/testing/test_funcargs.py b/py/test/testing/test_funcargs.py new file mode 100644 index 000000000..582520f4f --- /dev/null +++ b/py/test/testing/test_funcargs.py @@ -0,0 +1,103 @@ +import py + +class TestFuncargs: + def test_funcarg_lookupfails(self, testdir): + testdir.makeconftest(""" + class ConftestPlugin: + def pytest_funcarg__something(self, pyfuncitem): + return 42 + """) + item = testdir.getitem("def test_func(some): pass") + exc = py.test.raises(LookupError, "item.setupargs()") + s = str(exc.value) + assert s.find("something") != -1 + + def test_funcarg_lookup_default(self, testdir): + item = testdir.getitem("def test_func(some, other=42): pass") + class Provider: + def pytest_funcarg__some(self, pyfuncitem): + return pyfuncitem.name + item.config.pluginmanager.register(Provider()) + item.setupargs() + 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, pyfuncitem): + return pyfuncitem.name + item.config.pluginmanager.register(Provider()) + item.setupargs() + 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: + def pytest_funcarg__some(self, pyfuncitem): + return pyfuncitem.name + def pytest_funcarg__other(self, pyfuncitem): + return 42 + item.config.pluginmanager.register(Provider()) + item.setupargs() + assert len(item.funcargs) == 2 + assert item.funcargs['some'] == "test_func" + assert item.funcargs['other'] == 42 + + def test_funcarg_addfinalizer(self, testdir): + item = testdir.getitem("def test_func(some): pass") + l = [] + class Provider: + def pytest_funcarg__some(self, pyfuncitem): + pyfuncitem.addfinalizer(lambda: l.append(42)) + return 3 + item.config.pluginmanager.register(Provider()) + item.setupargs() + assert len(item.funcargs) == 1 + assert item.funcargs['some'] == 3 + assert len(l) == 0 + item.teardown() + assert len(l) == 1 + assert l[0] == 42 + + def test_funcarg_lookup_modulelevel(self, testdir): + modcol = testdir.getmodulecol(""" + def pytest_funcarg__something(pyfuncitem): + return pyfuncitem.name + + class TestClass: + def test_method(self, something): + pass + def test_func(something): + pass + """) + item1, item2 = testdir.genitems([modcol]) + modcol.setup() + assert modcol.config.pluginmanager.isregistered(modcol.obj) + item1.setupargs() + assert item1.funcargs['something'] == "test_method" + item2.setupargs() + assert item2.funcargs['something'] == "test_func" + modcol.teardown() + assert not modcol.config.pluginmanager.isregistered(modcol.obj) + +class TestRequest: + def test_request_contains_funcargs_methods(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 diff --git a/py/test/testing/test_pycollect.py b/py/test/testing/test_pycollect.py index b2fb2f8fa..cd680eaed 100644 --- a/py/test/testing/test_pycollect.py +++ b/py/test/testing/test_pycollect.py @@ -243,88 +243,6 @@ class TestFunction: assert f1 == f1_b assert not f1 != f1_b - def test_funcarg_lookupfails(self, testdir): - testdir.makeconftest(""" - class ConftestPlugin: - def pytest_funcarg__something(self, pyfuncitem): - return 42 - """) - item = testdir.getitem("def test_func(some): pass") - exc = py.test.raises(LookupError, "item.setupargs()") - s = str(exc.value) - assert s.find("something") != -1 - - def test_funcarg_lookup_default(self, testdir): - item = testdir.getitem("def test_func(some, other=42): pass") - class Provider: - def pytest_funcarg__some(self, pyfuncitem): - return pyfuncitem.name - item.config.pluginmanager.register(Provider()) - item.setupargs() - 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, pyfuncitem): - return pyfuncitem.name - item.config.pluginmanager.register(Provider()) - item.setupargs() - 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: - def pytest_funcarg__some(self, pyfuncitem): - return pyfuncitem.name - def pytest_funcarg__other(self, pyfuncitem): - return 42 - item.config.pluginmanager.register(Provider()) - item.setupargs() - assert len(item.funcargs) == 2 - assert item.funcargs['some'] == "test_func" - assert item.funcargs['other'] == 42 - - def test_funcarg_addfinalizer(self, testdir): - item = testdir.getitem("def test_func(some): pass") - l = [] - class Provider: - def pytest_funcarg__some(self, pyfuncitem): - pyfuncitem.addfinalizer(lambda: l.append(42)) - return 3 - item.config.pluginmanager.register(Provider()) - item.setupargs() - assert len(item.funcargs) == 1 - assert item.funcargs['some'] == 3 - assert len(l) == 0 - item.teardown() - assert len(l) == 1 - assert l[0] == 42 - - def test_funcarg_lookup_modulelevel(self, testdir): - modcol = testdir.getmodulecol(""" - def pytest_funcarg__something(pyfuncitem): - return pyfuncitem.name - - class TestClass: - def test_method(self, something): - pass - def test_func(something): - pass - """) - item1, item2 = testdir.genitems([modcol]) - modcol.setup() - assert modcol.config.pluginmanager.isregistered(modcol.obj) - item1.setupargs() - assert item1.funcargs['something'] == "test_method" - item2.setupargs() - assert item2.funcargs['something'] == "test_func" - modcol.teardown() - assert not modcol.config.pluginmanager.isregistered(modcol.obj) - class TestSorting: def test_check_equality_and_cmp_basic(self, testdir): modcol = testdir.getmodulecol(""" From de4c2dc98d28e16a880a7f5913917e47afffaa40 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 14 Apr 2009 02:59:50 +0200 Subject: [PATCH 12/18] implement request object as per docs --HG-- branch : trunk --- py/test/pycollect.py | 21 ++++++++++++++++++--- py/test/testing/test_funcargs.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/py/test/pycollect.py b/py/test/pycollect.py index 0d50ae04e..5c45e5e71 100644 --- a/py/test/pycollect.py +++ b/py/test/pycollect.py @@ -415,15 +415,30 @@ class Function(FunctionMixin, py.test.collect.Item): class FuncargRequest: + class Error(LookupError): + """ error on performing funcarg request. """ + def __init__(self, pyfuncitem, argname): self.pyfuncitem = pyfuncitem self.argname = argname - funcargname = "pytest_funcarg__" + str(argname) - pm = self.pyfuncitem.config.pluginmanager + self.function = pyfuncitem.obj + self.config = pyfuncitem.config extra = [] current = pyfuncitem while not isinstance(current, Module): current = current.parent if isinstance(current, (Instance, Module)): extra.insert(0, current.obj) - self._methods = pm.listattr(funcargname, extra=extra) + self._methods = self.pyfuncitem.config.pluginmanager.listattr( + "pytest_funcarg__" + str(argname), + extra=extra, + ) + + 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) diff --git a/py/test/testing/test_funcargs.py b/py/test/testing/test_funcargs.py index 582520f4f..56ba76b22 100644 --- a/py/test/testing/test_funcargs.py +++ b/py/test/testing/test_funcargs.py @@ -84,6 +84,16 @@ class TestFuncargs: assert not modcol.config.pluginmanager.isregistered(modcol.obj) class TestRequest: + def test_request_attributes(self, testdir): + item = testdir.getitem(""" + def pytest_funcarg__something(request): pass + def test_func(something): pass + """) + req = item.getrequest("other") + assert req.argname == "other" + assert req.function == item.obj + assert req.config == item.config + def test_request_contains_funcargs_methods(self, testdir): modcol = testdir.getmodulecol(""" def pytest_funcarg__something(request): @@ -101,3 +111,23 @@ class TestRequest: method1, method2 = methods assert not hasattr(method1, 'im_self') assert method2.im_self is not None + + 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") + val = req.call_next_provider() + assert val is None + py.test.raises(req.Error, "req.call_next_provider()") + + def test_request_addfinalizer(self, testdir): + item = testdir.getitem(""" + def pytest_funcarg__something(request): pass + def test_func(something): pass + """) + req = item.getrequest("something") + l = [1] + req.addfinalizer(l.pop) + item.teardown() From f0a277008ade084eb8759cabdb2a47f61335d432 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 14 Apr 2009 17:25:59 +0200 Subject: [PATCH 13/18] fix a bug --HG-- branch : trunk --- py/_com.py | 4 +--- py/misc/testing/test_com.py | 6 ++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/py/_com.py b/py/_com.py index 138451752..3666cae8b 100644 --- a/py/_com.py +++ b/py/_com.py @@ -90,9 +90,7 @@ class Registry: l = [] if plugins is None: plugins = self._plugins - if extra: - plugins += list(extra) - for plugin in plugins: + for plugin in list(plugins) + list(extra): try: l.append(getattr(plugin, attrname)) except AttributeError: diff --git a/py/misc/testing/test_com.py b/py/misc/testing/test_com.py index 4b2bc9cdc..a289b5277 100644 --- a/py/misc/testing/test_com.py +++ b/py/misc/testing/test_com.py @@ -135,6 +135,12 @@ class TestRegistry: l = list(plugins.listattr('x', reverse=True)) assert l == [43, 42, 41] + class api4: + x = 44 + l = list(plugins.listattr('x', extra=(api4,))) + assert l == range(41, 45) + assert len(list(plugins)) == 3 # otherwise extra added + def test_api_and_defaults(): assert isinstance(py._com.comregistry, Registry) From 763d0d72a5a33935e08ffce38486e73caa4aa5f2 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 14 Apr 2009 18:30:26 +0200 Subject: [PATCH 14/18] funcargs work mostly according to the documentation --HG-- branch : trunk --- py/conftest.py | 8 ++--- py/test/dist/testing/test_nodemanage.py | 40 +++++++++++---------- py/test/dist/testing/test_txnode.py | 14 +++----- py/test/plugin/pytest__pytest.py | 12 +++---- py/test/plugin/pytest_default.py | 1 - py/test/plugin/pytest_iocapture.py | 8 ++--- py/test/plugin/pytest_monkeypatch.py | 4 +-- py/test/plugin/pytest_plugintester.py | 38 +++++++------------- py/test/plugin/pytest_pytester.py | 37 +++++++++++-------- py/test/plugin/pytest_restdoc.py | 9 +++-- py/test/plugin/pytest_resultdb.py | 5 ++- py/test/plugin/pytest_resultlog.py | 3 +- py/test/plugin/pytest_tmpdir.py | 8 ++--- py/test/pluginmanager.py | 1 + py/test/pycollect.py | 26 +++++++------- py/test/testing/conftest.py | 1 + py/test/testing/test_funcargs.py | 48 ++++++++++--------------- py/test/testing/test_pickling.py | 24 ++++++------- py/test/testing/test_pycollect.py | 7 ---- py/test/testing/test_traceback.py | 2 +- 20 files changed, 136 insertions(+), 160 deletions(-) diff --git a/py/conftest.py b/py/conftest.py index 8dc4f1272..d93859aba 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -5,10 +5,10 @@ rsyncignore = ['c-extension/greenlet/build'] import py class PylibTestconfigPlugin: - def pytest_funcarg__specssh(self, pyfuncitem): - return getspecssh(pyfuncitem.config) - def pytest_funcarg__specsocket(self, pyfuncitem): - return getsocketspec(pyfuncitem.config) + def pytest_funcarg__specssh(self, request): + return getspecssh(request.config) + def pytest_funcarg__specsocket(self, request): + return getsocketspec(request.config) def pytest_addoption(self, parser): group = parser.addgroup("pylib", "py lib testing options") diff --git a/py/test/dist/testing/test_nodemanage.py b/py/test/dist/testing/test_nodemanage.py index 8c30ff9e4..b03bec989 100644 --- a/py/test/dist/testing/test_nodemanage.py +++ b/py/test/dist/testing/test_nodemanage.py @@ -1,28 +1,25 @@ - -""" RSync filter test -""" - import py from py.__.test.dist.nodemanage import NodeManager -def pytest_funcarg__source(pyfuncitem): - return py.test.ensuretemp(pyfuncitem.getmodpath()).mkdir("source") -def pytest_funcarg__dest(pyfuncitem): - dest = py.test.ensuretemp(pyfuncitem.getmodpath()).mkdir("dest") - return dest +class pytest_funcarg__mysetup: + def __init__(self, request): + basetemp = request.config.ensuretemp(request.getfspath().purebasename) + basetemp = basetemp.mkdir(request.funcname) + self.source = basetemp.mkdir("source") + self.dest = basetemp.mkdir("dest") class TestNodeManager: @py.test.mark.xfail("consider / forbid implicit rsyncdirs?") - def test_rsync_roots_no_roots(self, source, dest): - source.ensure("dir1", "file1").write("hello") + def test_rsync_roots_no_roots(self, mysetup): + mysetup.source.ensure("dir1", "file1").write("hello") config = py.test.config._reparse([source]) - nodemanager = NodeManager(config, ["popen//chdir=%s" % dest]) + nodemanager = NodeManager(config, ["popen//chdir=%s" % mysetup.dest]) assert nodemanager.config.topdir == source == config.topdir nodemanager.rsync_roots() p, = nodemanager.gwmanager.multi_exec("import os ; channel.send(os.getcwd())").receive_each() p = py.path.local(p) print "remote curdir", p - assert p == dest.join(config.topdir.basename) + assert p == mysetup.dest.join(config.topdir.basename) assert p.join("dir1").check() assert p.join("dir1", "file1").check() @@ -33,8 +30,9 @@ class TestNodeManager: nodemanager.setup_nodes([].append) nodemanager.wait_nodesready(timeout=2.0) - def test_popen_rsync_subdir(self, testdir, source, dest): - dir1 = source.mkdir("dir1") + def test_popen_rsync_subdir(self, testdir, mysetup): + source, dest = mysetup.source, mysetup.dest + dir1 = mysetup.source.mkdir("dir1") dir2 = dir1.mkdir("dir2") dir2.ensure("hello") for rsyncroot in (dir1, source): @@ -53,7 +51,8 @@ class TestNodeManager: assert dest.join("dir1", "dir2", 'hello').check() nodemanager.gwmanager.exit() - def test_init_rsync_roots(self, source, dest): + def test_init_rsync_roots(self, mysetup): + source, dest = mysetup.source, mysetup.dest dir2 = source.ensure("dir1", "dir2", dir=1) source.ensure("dir1", "somefile", dir=1) dir2.ensure("hello") @@ -68,7 +67,8 @@ class TestNodeManager: assert not dest.join("dir1").check() assert not dest.join("bogus").check() - def test_rsyncignore(self, source, dest): + def test_rsyncignore(self, mysetup): + source, dest = mysetup.source, mysetup.dest dir2 = source.ensure("dir1", "dir2", dir=1) dir5 = source.ensure("dir5", "dir6", "bogus") dirf = source.ensure("dir5", "file") @@ -86,7 +86,8 @@ class TestNodeManager: assert dest.join("dir5","file").check() assert not dest.join("dir6").check() - def test_optimise_popen(self, source, dest): + def test_optimise_popen(self, mysetup): + source, dest = mysetup.source, mysetup.dest specs = ["popen"] * 3 source.join("conftest.py").write("rsyncdirs = ['a']") source.ensure('a', dir=1) @@ -97,7 +98,8 @@ class TestNodeManager: assert gwspec._samefilesystem() assert not gwspec.chdir - def test_setup_DEBUG(self, source, testdir): + def test_setup_DEBUG(self, mysetup, testdir): + source = mysetup.source specs = ["popen"] * 2 source.join("conftest.py").write("rsyncdirs = ['a']") source.ensure('a', dir=1) diff --git a/py/test/dist/testing/test_txnode.py b/py/test/dist/testing/test_txnode.py index 63540c52d..c96d972ba 100644 --- a/py/test/dist/testing/test_txnode.py +++ b/py/test/dist/testing/test_txnode.py @@ -31,8 +31,8 @@ class EventQueue: print str(kwargs["excrepr"]) class MySetup: - def __init__(self, pyfuncitem): - self.pyfuncitem = pyfuncitem + def __init__(self, request): + self.request = request def geteventargs(self, eventname, timeout=2.0): eq = EventQueue(self.config.pluginmanager, self.queue) @@ -55,17 +55,11 @@ class MySetup: print "exiting:", gw gw.exit() -def pytest_funcarg__mysetup(pyfuncitem): - mysetup = MySetup(pyfuncitem) +def pytest_funcarg__mysetup(request): + mysetup = MySetup(request) #pyfuncitem.addfinalizer(mysetup.finalize) return mysetup -def pytest_funcarg__testdir(__call__, pyfuncitem): - # decorate to make us always change to testdir - testdir = __call__.execute(firstresult=True) - testdir.chdir() - return testdir - def test_node_hash_equality(mysetup): node = mysetup.makenode() node2 = mysetup.makenode() diff --git a/py/test/plugin/pytest__pytest.py b/py/test/plugin/pytest__pytest.py index 7724b4dde..8206e5751 100644 --- a/py/test/plugin/pytest__pytest.py +++ b/py/test/plugin/pytest__pytest.py @@ -1,19 +1,19 @@ import py class _pytestPlugin: - def pytest_funcarg___pytest(self, pyfuncitem): - return PytestArg(pyfuncitem) + def pytest_funcarg___pytest(self, request): + return PytestArg(request) class PytestArg: - def __init__(self, pyfuncitem): - self.pyfuncitem = pyfuncitem + def __init__(self, request): + self.request = request def getcallrecorder(self, apiclass, comregistry=None): if comregistry is None: - comregistry = self.pyfuncitem.config.pluginmanager.comregistry + comregistry = self.request.config.pluginmanager.comregistry callrecorder = CallRecorder(comregistry) callrecorder.start_recording(apiclass) - self.pyfuncitem.addfinalizer(callrecorder.finalize) + self.request.addfinalizer(callrecorder.finalize) return callrecorder diff --git a/py/test/plugin/pytest_default.py b/py/test/plugin/pytest_default.py index 36a7d97ad..4e396cfd1 100644 --- a/py/test/plugin/pytest_default.py +++ b/py/test/plugin/pytest_default.py @@ -22,7 +22,6 @@ class DefaultPlugin: rep = runner.ItemTestReport(item, excinfo, "execute", outerr) item.config.api.pytest_itemtestreport(rep=rep) - # XXX make this access pyfuncitem.args or funcargs def pytest_pyfunc_call(self, pyfuncitem, args, kwargs): pyfuncitem.obj(*args, **kwargs) diff --git a/py/test/plugin/pytest_iocapture.py b/py/test/plugin/pytest_iocapture.py index e7df7190a..d5949ed05 100644 --- a/py/test/plugin/pytest_iocapture.py +++ b/py/test/plugin/pytest_iocapture.py @@ -2,14 +2,14 @@ import py class IocapturePlugin: """ capture sys.stdout/sys.stderr / fd1/fd2. """ - def pytest_funcarg__stdcapture(self, pyfuncitem): + def pytest_funcarg__stdcapture(self, request): capture = Capture(py.io.StdCapture) - pyfuncitem.addfinalizer(capture.finalize) + request.addfinalizer(capture.finalize) return capture - def pytest_funcarg__stdcapturefd(self, pyfuncitem): + def pytest_funcarg__stdcapturefd(self, request): capture = Capture(py.io.StdCaptureFD) - pyfuncitem.addfinalizer(capture.finalize) + request.addfinalizer(capture.finalize) return capture class Capture: diff --git a/py/test/plugin/pytest_monkeypatch.py b/py/test/plugin/pytest_monkeypatch.py index 52cbdabf0..b81b53ac5 100644 --- a/py/test/plugin/pytest_monkeypatch.py +++ b/py/test/plugin/pytest_monkeypatch.py @@ -2,9 +2,9 @@ import os class MonkeypatchPlugin: """ setattr-monkeypatching with automatical reversal after test. """ - def pytest_funcarg__monkeypatch(self, pyfuncitem): + def pytest_funcarg__monkeypatch(self, request): monkeypatch = MonkeyPatch() - pyfuncitem.addfinalizer(monkeypatch.finalize) + request.addfinalizer(monkeypatch.finalize) return monkeypatch notset = object() diff --git a/py/test/plugin/pytest_plugintester.py b/py/test/plugin/pytest_plugintester.py index 30022accc..74e19ac82 100644 --- a/py/test/plugin/pytest_plugintester.py +++ b/py/test/plugin/pytest_plugintester.py @@ -6,37 +6,23 @@ from py.__.test.plugin import api class PlugintesterPlugin: """ test support code for testing pytest plugins. """ - def pytest_funcarg__plugintester(self, pyfuncitem): - pt = PluginTester(pyfuncitem) - pyfuncitem.addfinalizer(pt.finalize) - return pt + def pytest_funcarg__plugintester(self, request): + return PluginTester(request) -class Support(object): - def __init__(self, pyfuncitem): - """ instantiated per function that requests it. """ - self.pyfuncitem = pyfuncitem +class PluginTester: + def __init__(self, request): + self.request = request - def getmoditem(self): - for colitem in self.pyfuncitem.listchain(): - if isinstance(colitem, colitem.Module): - return colitem - - def finalize(self): - """ called after test function finished execution""" - -class PluginTester(Support): def testdir(self): - # XXX import differently, eg. - # FSTester = self.pyfuncitem.config.pluginmanager.getpluginattr("pytester", "FSTester") from pytest_pytester import TmpTestdir - crunner = TmpTestdir(self.pyfuncitem) - self.pyfuncitem.addfinalizer(crunner.finalize) + crunner = TmpTestdir(self.request) + self.request.addfinalizer(crunner.finalize) # - for colitem in self.pyfuncitem.listchain(): - if isinstance(colitem, py.test.collect.Module) and \ - colitem.name.startswith("pytest_"): - crunner.plugins.append(colitem.fspath.purebasename) - break + #for colitem in self.request.listchain(): + # if isinstance(colitem, py.test.collect.Module) and \ + # colitem.name.startswith("pytest_"): + # crunner.plugins.append(colitem.fspath.purebasename) + # break return crunner def apicheck(self, pluginclass): diff --git a/py/test/plugin/pytest_pytester.py b/py/test/plugin/pytest_pytester.py index 0a1d33bad..0a51707b5 100644 --- a/py/test/plugin/pytest_pytester.py +++ b/py/test/plugin/pytest_pytester.py @@ -11,19 +11,19 @@ import api class PytesterPlugin: - def pytest_funcarg__linecomp(self, pyfuncitem): + def pytest_funcarg__linecomp(self, request): return LineComp() - def pytest_funcarg__LineMatcher(self, pyfuncitem): + def pytest_funcarg__LineMatcher(self, request): return LineMatcher - def pytest_funcarg__testdir(self, pyfuncitem): - tmptestdir = TmpTestdir(pyfuncitem) + def pytest_funcarg__testdir(self, request): + tmptestdir = TmpTestdir(request) return tmptestdir - def pytest_funcarg__eventrecorder(self, pyfuncitem): + def pytest_funcarg__eventrecorder(self, request): evrec = EventRecorder(py._com.comregistry) - pyfuncitem.addfinalizer(lambda: evrec.comregistry.unregister(evrec)) + request.addfinalizer(lambda: evrec.comregistry.unregister(evrec)) return evrec def test_generic(plugintester): @@ -38,11 +38,11 @@ class RunResult: self.stderr = LineMatcher(errlines) class TmpTestdir: - def __init__(self, pyfuncitem): - self.pyfuncitem = pyfuncitem + def __init__(self, request): + self.request = request # XXX remove duplication with tmpdir plugin - basetmp = pyfuncitem.config.ensuretemp("testdir") - name = pyfuncitem.name + basetmp = request.config.ensuretemp("testdir") + name = request.funcname for i in range(100): try: tmpdir = basetmp.mkdir(name + str(i)) @@ -57,7 +57,7 @@ class TmpTestdir: self._syspathremove = [] self.chdir() # always chdir assert hasattr(self, '_olddir') - self.pyfuncitem.addfinalizer(self.finalize) + self.request.addfinalizer(self.finalize) def __repr__(self): return "" % (self.tmpdir,) @@ -78,7 +78,7 @@ class TmpTestdir: sorter.callrecorder = CallRecorder(registry) sorter.callrecorder.start_recording(api.PluginHooks) sorter.api = sorter.callrecorder.api - self.pyfuncitem.addfinalizer(sorter.callrecorder.finalize) + self.request.addfinalizer(sorter.callrecorder.finalize) return sorter def chdir(self): @@ -90,7 +90,7 @@ class TmpTestdir: items = kwargs.items() if args: source = "\n".join(map(str, args)) - basename = self.pyfuncitem.name + basename = self.request.funcname items.insert(0, (basename, source)) ret = None for name, value in items: @@ -139,7 +139,7 @@ class TmpTestdir: # used from runner functional tests item = self.getitem(source) # the test class where we are called from wants to provide the runner - testclassinstance = self.pyfuncitem.obj.im_self + testclassinstance = self.request.function.im_self runner = testclassinstance.getrunner() return runner(item, **runnerargs) @@ -200,7 +200,7 @@ class TmpTestdir: return self.config.getfsnode(path) def getmodulecol(self, source, configargs=(), withinit=False): - kw = {self.pyfuncitem.name: py.code.Source(source).strip()} + kw = {self.request.funcname: py.code.Source(source).strip()} path = self.makepyfile(**kw) if withinit: self.makepyfile(__init__ = "#") @@ -455,3 +455,10 @@ class LineMatcher: return extralines + + +def test_parseconfig(testdir): + config1 = testdir.parseconfig() + config2 = testdir.parseconfig() + assert config2 != config1 + assert config1 != py.test.config diff --git a/py/test/plugin/pytest_restdoc.py b/py/test/plugin/pytest_restdoc.py index 3a3b644b5..7952abbbf 100644 --- a/py/test/plugin/pytest_restdoc.py +++ b/py/test/plugin/pytest_restdoc.py @@ -382,10 +382,15 @@ class TestApigenLinkRole: "resolve_linkrole('source', 'py/foo/bar.py')") -def pytest_funcarg__testdir(__call__, pyfuncitem): - testdir = __call__.execute(firstresult=True) +def pytest_funcarg__testdir(request): + testdir = request.call_next_provider() testdir.makepyfile(confrest="from py.__.misc.rest import Project") testdir.plugins.append(RestdocPlugin()) + count = 0 + for p in testdir.plugins: + if isinstance(p, RestdocPlugin): + count += 1 + assert count < 2 return testdir class TestDoctest: diff --git a/py/test/plugin/pytest_resultdb.py b/py/test/plugin/pytest_resultdb.py index 84f8b4ee0..da2718ba6 100644 --- a/py/test/plugin/pytest_resultdb.py +++ b/py/test/plugin/pytest_resultdb.py @@ -1,4 +1,3 @@ -import uuid import py from pytest_resultlog import ResultLog @@ -65,7 +64,7 @@ class JSONResultArchive(object): self._flush() def append_data(self, data): - runid = uuid.uuid4() + runid = py.std.uuid.uuid4() for item in data: item = item.copy() item['runid'] = str(runid) @@ -100,7 +99,7 @@ class SQLiteResultArchive(object): def append_data(self, data): flat_data = [] - runid = uuid.uuid4() + runid = py.std.uuid.uuid4() for item in data: item = item.copy() item['runid'] = str(runid) diff --git a/py/test/plugin/pytest_resultlog.py b/py/test/plugin/pytest_resultlog.py index 09e3cd74d..0bc442b1b 100644 --- a/py/test/plugin/pytest_resultlog.py +++ b/py/test/plugin/pytest_resultlog.py @@ -157,6 +157,7 @@ class TestWithFunctionIntegration: # ignorant regarding formatting details. def getresultlog(self, testdir, arg): resultlog = testdir.tmpdir.join("resultlog") + testdir.plugins.append("resultlog") args = ["--resultlog=%s" % resultlog] + [arg] testdir.runpytest(*args) return filter(None, resultlog.readlines(cr=0)) @@ -166,7 +167,6 @@ class TestWithFunctionIntegration: ok = testdir.makepyfile(test_collection_ok="") skip = testdir.makepyfile(test_collection_skip="import py ; py.test.skip('hello')") fail = testdir.makepyfile(test_collection_fail="XXX") - lines = self.getresultlog(testdir, ok) assert not lines @@ -226,6 +226,7 @@ class TestWithFunctionIntegration: def test_generic(plugintester, LineMatcher): plugintester.apicheck(ResultlogPlugin) testdir = plugintester.testdir() + testdir.plugins.append("resultlog") testdir.makepyfile(""" import py def test_pass(): diff --git a/py/test/plugin/pytest_tmpdir.py b/py/test/plugin/pytest_tmpdir.py index fd2d162a4..307ab470e 100644 --- a/py/test/plugin/pytest_tmpdir.py +++ b/py/test/plugin/pytest_tmpdir.py @@ -13,9 +13,9 @@ class TmpdirPlugin: """ provide temporary directories to test functions and methods. """ - def pytest_funcarg__tmpdir(self, pyfuncitem): - name = pyfuncitem.name - return pyfuncitem.config.mktemp(name, numbered=True) + def pytest_funcarg__tmpdir(self, request): + name = request.funcname + return request.config.mktemp(name, numbered=True) # =============================================================================== # @@ -29,7 +29,7 @@ def test_generic(plugintester): def test_funcarg(testdir): item = testdir.getitem("def test_func(tmpdir): pass") plugin = TmpdirPlugin() - p = plugin.pytest_funcarg__tmpdir(item) + p = plugin.pytest_funcarg__tmpdir(item.getrequest("tmpdir")) assert p.check() bn = p.basename.strip("0123456789-") assert bn.endswith("test_func") diff --git a/py/test/pluginmanager.py b/py/test/pluginmanager.py index 08a480ea8..efdb9b225 100644 --- a/py/test/pluginmanager.py +++ b/py/test/pluginmanager.py @@ -18,6 +18,7 @@ class PluginManager(object): def register(self, plugin): self.api.pytest_plugin_registered(plugin=plugin) + import types self.comregistry.register(plugin) def unregister(self, plugin): diff --git a/py/test/pycollect.py b/py/test/pycollect.py index 5c45e5e71..d0650696b 100644 --- a/py/test/pycollect.py +++ b/py/test/pycollect.py @@ -177,7 +177,7 @@ class Module(py.test.collect.File, PyCollectorMixin): #print "*" * 20, "INVOKE assertion", self py.magic.invoke(assertion=1) mod = self.obj - self.config.pluginmanager.register(mod) + #self.config.pluginmanager.register(mod) if hasattr(mod, 'setup_module'): self.obj.setup_module(mod) @@ -187,7 +187,7 @@ class Module(py.test.collect.File, PyCollectorMixin): if not self.config.option.nomagic: #print "*" * 20, "revoke assertion", self py.magic.revoke(assertion=1) - self.config.pluginmanager.unregister(self.obj) + #self.config.pluginmanager.unregister(self.obj) class Class(PyCollectorMixin, py.test.collect.Collector): @@ -366,7 +366,8 @@ class Function(FunctionMixin, py.test.collect.Item): if i < startindex: continue try: - self.funcargs[argname] = self.lookup_onearg(argname) + argvalue = self.getrequest(argname).call_next_provider() + self.funcargs[argname] = argvalue except LookupError, e: numdefaults = len(funcobj.func_defaults or ()) if i + numdefaults >= len(argnames): @@ -374,15 +375,6 @@ class Function(FunctionMixin, py.test.collect.Item): else: raise - def lookup_onearg(self, argname): - prefix = "pytest_funcarg__" - #makerlist = self.config.pluginmanager.listattr(prefix + argname) - value = self.config.pluginmanager.call_firstresult(prefix + argname, pyfuncitem=self) - if value is not None: - return value - else: - self._raisefuncargerror(argname, prefix) - def _raisefuncargerror(self, argname, prefix="pytest_funcarg__"): metainfo = self.repr_metainfo() available = [] @@ -419,9 +411,11 @@ class FuncargRequest: """ 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.funcname = pyfuncitem.name self.config = pyfuncitem.config extra = [] current = pyfuncitem @@ -434,11 +428,19 @@ class FuncargRequest: extra=extra, ) + def __repr__(self): + return "" %(self.argname, self.pyfuncitem) + def call_next_provider(self): if not self._methods: raise self.Error("no provider methods left") nextmethod = self._methods.pop() + print "calling", nextmethod return nextmethod(request=self) def addfinalizer(self, finalizer): self.pyfuncitem.addfinalizer(finalizer) + + def getfspath(self): + return self.pyfuncitem.fspath + diff --git a/py/test/testing/conftest.py b/py/test/testing/conftest.py index 213bfa969..0e27ad3a6 100644 --- a/py/test/testing/conftest.py +++ b/py/test/testing/conftest.py @@ -1,2 +1,3 @@ pytest_plugins = "pytest_xfail", "pytest_pytester", "pytest_tmpdir" + diff --git a/py/test/testing/test_funcargs.py b/py/test/testing/test_funcargs.py index 56ba76b22..7dcf05dd0 100644 --- a/py/test/testing/test_funcargs.py +++ b/py/test/testing/test_funcargs.py @@ -4,7 +4,7 @@ class TestFuncargs: def test_funcarg_lookupfails(self, testdir): testdir.makeconftest(""" class ConftestPlugin: - def pytest_funcarg__something(self, pyfuncitem): + def pytest_funcarg__xyz(self, request): return 42 """) item = testdir.getitem("def test_func(some): pass") @@ -15,8 +15,8 @@ class TestFuncargs: def test_funcarg_lookup_default(self, testdir): item = testdir.getitem("def test_func(some, other=42): pass") class Provider: - def pytest_funcarg__some(self, pyfuncitem): - return pyfuncitem.name + def pytest_funcarg__some(self, request): + return request.funcname item.config.pluginmanager.register(Provider()) item.setupargs() assert len(item.funcargs) == 1 @@ -24,8 +24,8 @@ class TestFuncargs: 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, pyfuncitem): - return pyfuncitem.name + def pytest_funcarg__other(self, request): + return request.funcname item.config.pluginmanager.register(Provider()) item.setupargs() assert len(item.funcargs) == 1 @@ -36,9 +36,9 @@ class TestFuncargs: def test_funcarg_basic(self, testdir): item = testdir.getitem("def test_func(some, other): pass") class Provider: - def pytest_funcarg__some(self, pyfuncitem): - return pyfuncitem.name - def pytest_funcarg__other(self, pyfuncitem): + def pytest_funcarg__some(self, request): + return request.funcname + def pytest_funcarg__other(self, request): return 42 item.config.pluginmanager.register(Provider()) item.setupargs() @@ -46,26 +46,10 @@ class TestFuncargs: assert item.funcargs['some'] == "test_func" assert item.funcargs['other'] == 42 - def test_funcarg_addfinalizer(self, testdir): - item = testdir.getitem("def test_func(some): pass") - l = [] - class Provider: - def pytest_funcarg__some(self, pyfuncitem): - pyfuncitem.addfinalizer(lambda: l.append(42)) - return 3 - item.config.pluginmanager.register(Provider()) - item.setupargs() - assert len(item.funcargs) == 1 - assert item.funcargs['some'] == 3 - assert len(l) == 0 - item.teardown() - assert len(l) == 1 - assert l[0] == 42 - def test_funcarg_lookup_modulelevel(self, testdir): modcol = testdir.getmodulecol(""" - def pytest_funcarg__something(pyfuncitem): - return pyfuncitem.name + def pytest_funcarg__something(request): + return request.funcname class TestClass: def test_method(self, something): @@ -74,14 +58,10 @@ class TestFuncargs: pass """) item1, item2 = testdir.genitems([modcol]) - modcol.setup() - assert modcol.config.pluginmanager.isregistered(modcol.obj) item1.setupargs() assert item1.funcargs['something'] == "test_method" item2.setupargs() assert item2.funcargs['something'] == "test_func" - modcol.teardown() - assert not modcol.config.pluginmanager.isregistered(modcol.obj) class TestRequest: def test_request_attributes(self, testdir): @@ -92,7 +72,9 @@ class TestRequest: req = item.getrequest("other") assert req.argname == "other" assert req.function == item.obj + assert req.funcname == "test_func" assert req.config == item.config + assert repr(req).find(req.funcname) != -1 def test_request_contains_funcargs_methods(self, testdir): modcol = testdir.getmodulecol(""" @@ -131,3 +113,9 @@ class TestRequest: l = [1] req.addfinalizer(l.pop) item.teardown() + + def test_request_getmodulepath(self, testdir): + modcol = testdir.getmodulecol("def test_somefunc(): pass") + item, = testdir.genitems([modcol]) + req = item.getrequest("hello") + assert req.getfspath() == modcol.fspath diff --git a/py/test/testing/test_pickling.py b/py/test/testing/test_pickling.py index fed4aa8cb..97f265983 100644 --- a/py/test/testing/test_pickling.py +++ b/py/test/testing/test_pickling.py @@ -1,31 +1,27 @@ import py -def pytest_funcarg__pickletransport(pyfuncitem): - return ImmutablePickleTransport() - -def pytest_pyfunc_call(__call__, pyfuncitem, args, kwargs): - # for each function call we patch py._com.comregistry - # so that the unpickling of config objects - # (which bind to this mechanism) doesn't do harm - # usually config objects are no meant to be unpickled in - # the same system +def setglobals(request): oldconfig = py.test.config oldcom = py._com.comregistry print "setting py.test.config to None" py.test.config = None py._com.comregistry = py._com.Registry() - try: - return __call__.execute(firstresult=True) - finally: + def resetglobals(): print "setting py.test.config to", oldconfig py.test.config = oldconfig py._com.comregistry = oldcom + request.addfinalizer(resetglobals) + +def pytest_funcarg__testdir(request): + setglobals(request) + return request.call_next_provider() class ImmutablePickleTransport: - def __init__(self): + def __init__(self, request): from py.__.test.dist.mypickle import ImmutablePickler self.p1 = ImmutablePickler(uneven=0) self.p2 = ImmutablePickler(uneven=1) + setglobals(request) def p1_to_p2(self, obj): return self.p2.loads(self.p1.dumps(obj)) @@ -39,6 +35,8 @@ class ImmutablePickleTransport: return p2config class TestImmutablePickling: + pytest_funcarg__pickletransport = ImmutablePickleTransport + def test_pickle_config(self, testdir, pickletransport): config1 = testdir.parseconfig() assert config1.topdir == testdir.tmpdir diff --git a/py/test/testing/test_pycollect.py b/py/test/testing/test_pycollect.py index cd680eaed..e7fb0d195 100644 --- a/py/test/testing/test_pycollect.py +++ b/py/test/testing/test_pycollect.py @@ -34,13 +34,6 @@ class TestModule: x = l.pop() assert x is None - def test_module_participates_as_plugin(self, testdir): - modcol = testdir.getmodulecol("") - modcol.setup() - assert modcol.config.pluginmanager.isregistered(modcol.obj) - modcol.teardown() - assert not modcol.config.pluginmanager.isregistered(modcol.obj) - def test_module_considers_pluginmanager_at_import(self, testdir): modcol = testdir.getmodulecol("pytest_plugins='xasdlkj',") py.test.raises(ImportError, "modcol.obj") diff --git a/py/test/testing/test_traceback.py b/py/test/testing/test_traceback.py index 25f0fa112..3dfa79fb4 100644 --- a/py/test/testing/test_traceback.py +++ b/py/test/testing/test_traceback.py @@ -10,7 +10,7 @@ class TestTracebackCutting: def test_traceback_argsetup(self, testdir): testdir.makeconftest(""" class ConftestPlugin: - def pytest_funcarg__hello(self, pyfuncitem): + def pytest_funcarg__hello(self, request): raise ValueError("xyz") """) p = testdir.makepyfile("def test(hello): pass") From 9c6a790992f3b519cc2ddcc687c57bf53aeb7b8d Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 14 Apr 2009 18:42:36 +0200 Subject: [PATCH 15/18] remove print, shift code --HG-- branch : trunk --- py/test/pycollect.py | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/py/test/pycollect.py b/py/test/pycollect.py index d0650696b..e3097a189 100644 --- a/py/test/pycollect.py +++ b/py/test/pycollect.py @@ -365,30 +365,15 @@ class Function(FunctionMixin, py.test.collect.Item): for i, argname in py.builtin.enumerate(argnames): if i < startindex: continue + request = self.getrequest(argname) try: - argvalue = self.getrequest(argname).call_next_provider() - self.funcargs[argname] = argvalue - except LookupError, e: + self.funcargs[argname] = request.call_next_provider() + except request.Error: numdefaults = len(funcobj.func_defaults or ()) if i + numdefaults >= len(argnames): - continue # continue # seems that our args have defaults + continue # our args have defaults XXX issue warning? else: - raise - - def _raisefuncargerror(self, argname, prefix="pytest_funcarg__"): - metainfo = self.repr_metainfo() - available = [] - plugins = list(self.config.pluginmanager.comregistry) - #plugins.extend(self.config.pluginmanager.registry.plugins) - for plugin in plugins: - for name in vars(plugin.__class__): - if name.startswith(prefix): - name = name[len(prefix):] - if name not in available: - available.append(name) - msg = "funcargument %r not found for: %s" %(argname,metainfo.verboseline()) - msg += "\n available funcargs: %s" %(", ".join(available),) - raise LookupError(msg) + raise # request.raiselookupfailed() def __eq__(self, other): try: @@ -435,7 +420,6 @@ class FuncargRequest: if not self._methods: raise self.Error("no provider methods left") nextmethod = self._methods.pop() - print "calling", nextmethod return nextmethod(request=self) def addfinalizer(self, finalizer): @@ -443,4 +427,20 @@ class FuncargRequest: def getfspath(self): return self.pyfuncitem.fspath + + def _raisefuncargerror(self): + metainfo = self.repr_metainfo() + available = [] + plugins = list(self.config.pluginmanager.comregistry) + #plugins.extend(self.config.pluginmanager.registry.plugins) + for plugin in plugins: + for name in vars(plugin.__class__): + if name.startswith(prefix): + name = name[len(prefix):] + if name not in available: + available.append(name) + msg = "funcargument %r not found for: %s" %(argname,metainfo.verboseline()) + msg += "\n available funcargs: %s" %(", ".join(available),) + raise LookupError(msg) + From 792dce025cdc59441cef6b6e9cd35202aa64a077 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 14 Apr 2009 19:24:45 +0200 Subject: [PATCH 16/18] fix reporting failure --HG-- branch : trunk --- py/test/pycollect.py | 41 ++++++++++++++++++-------------- py/test/testing/test_funcargs.py | 4 ++-- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/py/test/pycollect.py b/py/test/pycollect.py index e3097a189..521006abd 100644 --- a/py/test/pycollect.py +++ b/py/test/pycollect.py @@ -373,7 +373,7 @@ class Function(FunctionMixin, py.test.collect.Item): if i + numdefaults >= len(argnames): continue # our args have defaults XXX issue warning? else: - raise # request.raiselookupfailed() + request._raiselookupfailed() def __eq__(self, other): try: @@ -392,6 +392,8 @@ class Function(FunctionMixin, py.test.collect.Item): class FuncargRequest: + _argprefix = "pytest_funcarg__" + class Error(LookupError): """ error on performing funcarg request. """ @@ -402,20 +404,25 @@ class FuncargRequest: self.function = pyfuncitem.obj self.funcname = pyfuncitem.name self.config = pyfuncitem.config - extra = [] - current = pyfuncitem - while not isinstance(current, Module): - current = current.parent - if isinstance(current, (Instance, Module)): - extra.insert(0, current.obj) - self._methods = self.pyfuncitem.config.pluginmanager.listattr( - "pytest_funcarg__" + str(argname), - extra=extra, + self._plugins = self._getplugins() + self._methods = self.config.pluginmanager.listattr( + plugins=self._plugins, + attrname=self._argprefix + str(argname) ) def __repr__(self): return "" %(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") @@ -428,18 +435,16 @@ class FuncargRequest: def getfspath(self): return self.pyfuncitem.fspath - def _raisefuncargerror(self): - metainfo = self.repr_metainfo() + def _raiselookupfailed(self): available = [] - plugins = list(self.config.pluginmanager.comregistry) - #plugins.extend(self.config.pluginmanager.registry.plugins) - for plugin in plugins: + for plugin in self._plugins: for name in vars(plugin.__class__): - if name.startswith(prefix): - name = name[len(prefix):] + if name.startswith(self._argprefix): + name = name[len(self._argprefix):] if name not in available: available.append(name) - msg = "funcargument %r not found for: %s" %(argname,metainfo.verboseline()) + metainfo = self.pyfuncitem.repr_metainfo() + msg = "funcargument %r not found for: %s" %(self.argname,metainfo.verboseline()) msg += "\n available funcargs: %s" %(", ".join(available),) raise LookupError(msg) diff --git a/py/test/testing/test_funcargs.py b/py/test/testing/test_funcargs.py index 7dcf05dd0..ca9b4bcca 100644 --- a/py/test/testing/test_funcargs.py +++ b/py/test/testing/test_funcargs.py @@ -4,13 +4,13 @@ class TestFuncargs: def test_funcarg_lookupfails(self, testdir): testdir.makeconftest(""" class ConftestPlugin: - def pytest_funcarg__xyz(self, request): + def pytest_funcarg__xyzsomething(self, request): return 42 """) item = testdir.getitem("def test_func(some): pass") exc = py.test.raises(LookupError, "item.setupargs()") s = str(exc.value) - assert s.find("something") != -1 + assert s.find("xyzsomething") != -1 def test_funcarg_lookup_default(self, testdir): item = testdir.getitem("def test_func(some, other=42): pass") From 5e03bdad846e00a648cfd4e2c40ea0a6037be624 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 14 Apr 2009 19:57:00 +0200 Subject: [PATCH 17/18] bringing docs and funcargs in sync --HG-- branch : trunk --- doc/test/funcargs.txt | 17 ++++++++++++++--- py/test/dist/testing/test_nodemanage.py | 2 +- py/test/plugin/pytest_pytester.py | 6 +++--- py/test/plugin/pytest_tmpdir.py | 2 +- py/test/pycollect.py | 13 ++++++------- py/test/testing/test_funcargs.py | 8 ++++---- 6 files changed, 29 insertions(+), 19 deletions(-) diff --git a/doc/test/funcargs.txt b/doc/test/funcargs.txt index 491937f23..db2c0f3b9 100644 --- a/doc/test/funcargs.txt +++ b/doc/test/funcargs.txt @@ -2,7 +2,6 @@ **funcargs**: powerful and simple test setup ====================================================== - 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. @@ -170,9 +169,21 @@ into ``myapp.py``: def question(self): return 6 * 9 -You can now run the test with ``py.test test_sample.py``: +You can now run the test with ``py.test test_sample.py`` which will +show this failure: -.. sourcecode:: python +.. sourcecode:: python + + def test_answer(mysetup): + app = mysetup.myapp() + answer = app.question() + > assert answer == 42 + E assert 54 == 42 + +If you are confused as to that the question or answer is +here, please visit here_. + +.. _here: http://uncyclopedia.wikia.com/wiki/The_Hitchhiker's_Guide_to_the_Galaxy .. _`local plugin`: ext.html#local-plugin diff --git a/py/test/dist/testing/test_nodemanage.py b/py/test/dist/testing/test_nodemanage.py index b03bec989..a5097a926 100644 --- a/py/test/dist/testing/test_nodemanage.py +++ b/py/test/dist/testing/test_nodemanage.py @@ -4,7 +4,7 @@ from py.__.test.dist.nodemanage import NodeManager class pytest_funcarg__mysetup: def __init__(self, request): basetemp = request.config.ensuretemp(request.getfspath().purebasename) - basetemp = basetemp.mkdir(request.funcname) + basetemp = basetemp.mkdir(request.function.__name__) self.source = basetemp.mkdir("source") self.dest = basetemp.mkdir("dest") diff --git a/py/test/plugin/pytest_pytester.py b/py/test/plugin/pytest_pytester.py index 0a51707b5..080bbd063 100644 --- a/py/test/plugin/pytest_pytester.py +++ b/py/test/plugin/pytest_pytester.py @@ -42,7 +42,7 @@ class TmpTestdir: self.request = request # XXX remove duplication with tmpdir plugin basetmp = request.config.ensuretemp("testdir") - name = request.funcname + name = request.function.__name__ for i in range(100): try: tmpdir = basetmp.mkdir(name + str(i)) @@ -90,7 +90,7 @@ class TmpTestdir: items = kwargs.items() if args: source = "\n".join(map(str, args)) - basename = self.request.funcname + basename = self.request.function.__name__ items.insert(0, (basename, source)) ret = None for name, value in items: @@ -200,7 +200,7 @@ class TmpTestdir: return self.config.getfsnode(path) def getmodulecol(self, source, configargs=(), withinit=False): - kw = {self.request.funcname: py.code.Source(source).strip()} + kw = {self.request.function.__name__: py.code.Source(source).strip()} path = self.makepyfile(**kw) if withinit: self.makepyfile(__init__ = "#") diff --git a/py/test/plugin/pytest_tmpdir.py b/py/test/plugin/pytest_tmpdir.py index 307ab470e..fa3ec98d9 100644 --- a/py/test/plugin/pytest_tmpdir.py +++ b/py/test/plugin/pytest_tmpdir.py @@ -14,7 +14,7 @@ class TmpdirPlugin: """ def pytest_funcarg__tmpdir(self, request): - name = request.funcname + name = request.function.__name__ return request.config.mktemp(name, numbered=True) # =============================================================================== diff --git a/py/test/pycollect.py b/py/test/pycollect.py index 521006abd..893643672 100644 --- a/py/test/pycollect.py +++ b/py/test/pycollect.py @@ -399,10 +399,9 @@ class FuncargRequest: def __init__(self, pyfuncitem, argname): # XXX make pyfuncitem _pyfuncitem - self.pyfuncitem = pyfuncitem + self._pyfuncitem = pyfuncitem self.argname = argname self.function = pyfuncitem.obj - self.funcname = pyfuncitem.name self.config = pyfuncitem.config self._plugins = self._getplugins() self._methods = self.config.pluginmanager.listattr( @@ -411,12 +410,12 @@ class FuncargRequest: ) def __repr__(self): - return "" %(self.argname, self.pyfuncitem) + return "" %(self.argname, self._pyfuncitem) def _getplugins(self): plugins = [] - current = self.pyfuncitem + current = self._pyfuncitem while not isinstance(current, Module): current = current.parent if isinstance(current, (Instance, Module)): @@ -430,10 +429,10 @@ class FuncargRequest: return nextmethod(request=self) def addfinalizer(self, finalizer): - self.pyfuncitem.addfinalizer(finalizer) + self._pyfuncitem.addfinalizer(finalizer) def getfspath(self): - return self.pyfuncitem.fspath + return self._pyfuncitem.fspath def _raiselookupfailed(self): available = [] @@ -443,7 +442,7 @@ class FuncargRequest: name = name[len(self._argprefix):] if name not in available: available.append(name) - metainfo = self.pyfuncitem.repr_metainfo() + metainfo = self._pyfuncitem.repr_metainfo() msg = "funcargument %r not found for: %s" %(self.argname,metainfo.verboseline()) msg += "\n available funcargs: %s" %(", ".join(available),) raise LookupError(msg) diff --git a/py/test/testing/test_funcargs.py b/py/test/testing/test_funcargs.py index ca9b4bcca..cee4681b2 100644 --- a/py/test/testing/test_funcargs.py +++ b/py/test/testing/test_funcargs.py @@ -16,7 +16,7 @@ class TestFuncargs: item = testdir.getitem("def test_func(some, other=42): pass") class Provider: def pytest_funcarg__some(self, request): - return request.funcname + return request.function.__name__ item.config.pluginmanager.register(Provider()) item.setupargs() assert len(item.funcargs) == 1 @@ -25,7 +25,7 @@ class TestFuncargs: item = testdir.getitem("def test_func(some=42, other=13): pass") class Provider: def pytest_funcarg__other(self, request): - return request.funcname + return request.function.__name__ item.config.pluginmanager.register(Provider()) item.setupargs() assert len(item.funcargs) == 1 @@ -37,7 +37,7 @@ class TestFuncargs: item = testdir.getitem("def test_func(some, other): pass") class Provider: def pytest_funcarg__some(self, request): - return request.funcname + return request.function.__name__ def pytest_funcarg__other(self, request): return 42 item.config.pluginmanager.register(Provider()) @@ -49,7 +49,7 @@ class TestFuncargs: def test_funcarg_lookup_modulelevel(self, testdir): modcol = testdir.getmodulecol(""" def pytest_funcarg__something(request): - return request.funcname + return request.function.__name__ class TestClass: def test_method(self, something): From e976fb36fdca47f9ace83a45b483d8e4a5ff21b0 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 14 Apr 2009 21:36:57 +0200 Subject: [PATCH 18/18] finalized docs and funcarg attributes, i think --HG-- branch : trunk --- doc/test/config.txt | 23 +++++++++++++++ doc/test/funcargs.txt | 39 ++++++++++++++++++++++++- py/test/dist/testing/test_nodemanage.py | 2 +- py/test/pycollect.py | 10 +++++-- py/test/testing/test_funcargs.py | 15 ++++++++-- 5 files changed, 82 insertions(+), 7 deletions(-) diff --git a/doc/test/config.txt b/doc/test/config.txt index 0632cfc6c..2541c3a93 100644 --- a/doc/test/config.txt +++ b/doc/test/config.txt @@ -21,3 +21,26 @@ behind the ``--`` double-dash. IOW, you can set default values for options per project, per home-directoray, per shell session or per test-run. + +.. _`basetemp`: + +per-testrun temporary directories +------------------------------------------- + +``py.test`` runs provide means to create per-test session +temporary (sub) directories. You can create such directories +like this: + +.. sourcecode: python + + import py + basetemp = py.test.config.ensuretemp() + basetemp_subdir = py.test.config.ensuretemp("subdir") + +By default, ``py.test`` creates a ``pytest-NUMBER`` directory +and will keep around the directories of the last three +test runs. You can also set the base temporary directory +with the `--basetemp`` option. When distributing +tests on the same machine, ``py.test`` takes care to +pass around the basetemp directory such that all temporary +files land below the same basetemp directory. diff --git a/doc/test/funcargs.txt b/doc/test/funcargs.txt index db2c0f3b9..593eadeb8 100644 --- a/doc/test/funcargs.txt +++ b/doc/test/funcargs.txt @@ -57,14 +57,21 @@ Request objects encapsulate a request for a function argument from a specific test function. Request objects provide access to command line options, the underlying python function and allow interaction with other providers and the test running process. -Basic attributes of request objects: + +Attributes of request objects +++++++++++++++++++++++++++++++++++++++++ ``request.argname``: name of the requested function argument ``request.function``: python function object requesting the argument +``request.fspath``: filesystem path of containing module + ``request.config``: access to command line opts and general config +finalizing after test function executed +++++++++++++++++++++++++++++++++++++++++ + Request objects allow to **register a finalizer method** which is called after a test function has finished running. This is useful for tearing down or cleaning up @@ -79,6 +86,35 @@ 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 +++++++++++++++++++++++++++++++++++++++++ + If you want to **decorate a function argument** that is provided elsewhere you can ask the request object to provide the "next" value: @@ -94,6 +130,7 @@ This will raise a ``request.Error`` exception if there is no next provider left. See the `decorator example`_ for a use of this method. + .. _`lookup order`: Order of funcarg provider lookup diff --git a/py/test/dist/testing/test_nodemanage.py b/py/test/dist/testing/test_nodemanage.py index a5097a926..556b9037b 100644 --- a/py/test/dist/testing/test_nodemanage.py +++ b/py/test/dist/testing/test_nodemanage.py @@ -3,7 +3,7 @@ from py.__.test.dist.nodemanage import NodeManager class pytest_funcarg__mysetup: def __init__(self, request): - basetemp = request.config.ensuretemp(request.getfspath().purebasename) + basetemp = request.maketempdir() basetemp = basetemp.mkdir(request.function.__name__) self.source = basetemp.mkdir("source") self.dest = basetemp.mkdir("dest") diff --git a/py/test/pycollect.py b/py/test/pycollect.py index 893643672..d53302c52 100644 --- a/py/test/pycollect.py +++ b/py/test/pycollect.py @@ -403,6 +403,7 @@ class FuncargRequest: 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, @@ -431,8 +432,12 @@ class FuncargRequest: def addfinalizer(self, finalizer): self._pyfuncitem.addfinalizer(finalizer) - def getfspath(self): - return self._pyfuncitem.fspath + 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 = [] @@ -447,4 +452,5 @@ class FuncargRequest: msg += "\n available funcargs: %s" %(", ".join(available),) raise LookupError(msg) + diff --git a/py/test/testing/test_funcargs.py b/py/test/testing/test_funcargs.py index cee4681b2..adbc47d4a 100644 --- a/py/test/testing/test_funcargs.py +++ b/py/test/testing/test_funcargs.py @@ -72,9 +72,9 @@ class TestRequest: req = item.getrequest("other") assert req.argname == "other" assert req.function == item.obj - assert req.funcname == "test_func" + assert req.function.__name__ == "test_func" assert req.config == item.config - assert repr(req).find(req.funcname) != -1 + assert repr(req).find(req.function.__name__) != -1 def test_request_contains_funcargs_methods(self, testdir): modcol = testdir.getmodulecol(""" @@ -114,8 +114,17 @@ class TestRequest: 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") - assert req.getfspath() == modcol.fspath + assert req.fspath == modcol.fspath