diff --git a/.gitignore b/.gitignore index 79e1c6765..2b7c267b0 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ include/ *.pyc *.pyo *.swp -*.html *.class *.orig *~ diff --git a/.hgtags b/.hgtags index ba85d24b0..8fc60f55b 100644 --- a/.hgtags +++ b/.hgtags @@ -60,3 +60,8 @@ b93ac0cdae02effaa3c136a681cc45bba757fe46 1.4.14 0000000000000000000000000000000000000000 1.4.14 0000000000000000000000000000000000000000 1.4.14 0000000000000000000000000000000000000000 1.4.14 +af860de70cc3f157ac34ca1d4bf557a057bff775 2.4.0 +8828c924acae0b4cad2e2cb92943d51da7cb744a 2.4.1 +8d051f89184bfa3033f5e59819dff9f32a612941 2.4.2 +a064ad64d167508a8e9e73766b1a4e6bd10c85db 2.5.0 +039d543d1ca02a716c0b0de9a7131beb8021e8a2 2.5.1 diff --git a/.travis.yml b/.travis.yml index 8a1dc1ab2..1c865fb9e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,9 @@ language: python # command to install dependencies -install: "pip install 'virtualenv<1.10' -e . detox" +install: "pip install -U detox" # # command to run tests -script: detox --recreate +script: detox --recreate -i ALL=https://devpi.net/hpk/dev/ + notifications: irc: - "chat.freenode.net#pytest-dev" diff --git a/AUTHORS b/AUTHORS index aa716a34f..bce4c6bc5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,7 +1,7 @@ Holger Krekel, holger at merlinux eu merlinux GmbH, Germany, office at merlinux eu -Contributors include:: +Contributors include:: Ronny Pfannschmidt Benjamin Peterson @@ -9,21 +9,21 @@ Floris Bruynooghe Jason R. Coombs Wouter van Ackooy Samuele Pedroni -Anatoly Bubenkoff +Anatoly Bubenkoff Brianna Laugher Carl Friedrich Bolz Armin Rigo Maho -Jaap Broekhuizen +Jaap Broekhuizen Maciek Fijalkowski Guido Wesdorp Brian Dorsey Ross Lawley Ralf Schmitt -Chris Lamb +Chris Lamb Harald Armin Massa Martijn Faassen -Ian Bicking +Ian Bicking Jan Balster Grig Gheorghiu Bob Ippolito @@ -35,3 +35,6 @@ Brian Okken Katarzyna Jachim Christian Theunert Anthon van der Neut +Mark Abramowitz +Piotr Banaszkiewicz +Jurko Gospodnetić diff --git a/CHANGELOG b/CHANGELOG index cd46a64ea..a6b1c1f56 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,41 +1,344 @@ -Changes between 2.3.5 and 2.4.DEV +UNRELEASED ----------------------------------- -- make "import pdb ; pdb.set_trace()" work natively wrt capturing (no "-s" needed - anymore), making ``pytest.set_trace()`` a mere shortcut. +- fix issue409 -- better interoperate with cx_freeze by not + trying to import from collections.abc which causes problems for py27/cx_freeze. + Thanks Wolfgang L. for reporting and tracking it down. -- fix issue181: --pdb now also works on collect errors (and - on internal errors) . This was implemented by a slight internal - refactoring and the introduction of a new hook - ``pytest_exception_interact`` hook (see below). +- fixed docs and code to use "pytest" instead of "py.test" almost everywhere. + Thanks Jurko Gospodnetic for the complete PR. -- fix issue341: introduce new experimental hook for IDEs/terminals to +- fix issue425: mention at end of "py.test -h" that --markers + and --fixtures work according to specified test path (or current dir) + +- fix issue413: exceptions with unicode attributes are now printed + correctly also on python2 and with pytest-xdist runs. (the fix + requires py-1.4.20) + +- copy, cleanup and integrate py.io capture + from pylib 1.4.20.dev2 (rev 13d9af95547e) + +- address issue416: clarify docs as to conftest.py loading semantics + + +- make capfd/capsys.capture private, its unused and shouldnt be exposed + + +2.5.1 +----------------------------------- + +- merge new documentation styling PR from Tobias Bieniek. + +- fix issue403: allow parametrize of multiple same-name functions within + a collection node. Thanks Andreas Kloeckner and Alex Gaynor for reporting + and analysis. + +- Allow parameterized fixtures to specify the ID of the parameters by + adding an ids argument to pytest.fixture() and pytest.yield_fixture(). + Thanks Floris Bruynooghe. + +- fix issue404 by always using the binary xml escape in the junitxml + plugin. Thanks Ronny Pfannschmidt. + +- fix issue407: fix addoption docstring to point to argparse instead of + optparse. Thanks Daniel D. Wright. + + + +2.5.0 +----------------------------------- + +- dropped python2.5 from automated release testing of pytest itself + which means it's probably going to break soon (but still works + with this release we believe). + +- simplified and fixed implementation for calling finalizers when + parametrized fixtures or function arguments are involved. finalization + is now performed lazily at setup time instead of in the "teardown phase". + While this might sound odd at first, it helps to ensure that we are + correctly handling setup/teardown even in complex code. User-level code + should not be affected unless it's implementing the pytest_runtest_teardown + hook and expecting certain fixture instances are torn down within (very + unlikely and would have been unreliable anyway). + +- PR90: add --color=yes|no|auto option to force terminal coloring + mode ("auto" is default). Thanks Marc Abramowitz. + +- fix issue319 - correctly show unicode in assertion errors. Many + thanks to Floris Bruynooghe for the complete PR. Also means + we depend on py>=1.4.19 now. + +- fix issue396 - correctly sort and finalize class-scoped parametrized + tests independently from number of methods on the class. + +- refix issue323 in a better way -- parametrization should now never + cause Runtime Recursion errors because the underlying algorithm + for re-ordering tests per-scope/per-fixture is not recursive + anymore (it was tail-call recursive before which could lead + to problems for more than >966 non-function scoped parameters). + +- fix issue290 - there is preliminary support now for parametrizing + with repeated same values (sometimes useful to to test if calling + a second time works as with the first time). + +- close issue240 - document precisely how pytest module importing + works, discuss the two common test directory layouts, and how it + interacts with PEP420-namespace packages. + +- fix issue246 fix finalizer order to be LIFO on independent fixtures + depending on a parametrized higher-than-function scoped fixture. + (was quite some effort so please bear with the complexity of this sentence :) + Thanks Ralph Schmitt for the precise failure example. + +- fix issue244 by implementing special index for parameters to only use + indices for paramentrized test ids + +- fix issue287 by running all finalizers but saving the exception + from the first failing finalizer and re-raising it so teardown will + still have failed. We reraise the first failing exception because + it might be the cause for other finalizers to fail. + +- fix ordering when mock.patch or other standard decorator-wrappings + are used with test methods. This fixues issue346 and should + help with random "xdist" collection failures. Thanks to + Ronny Pfannschmidt and Donald Stufft for helping to isolate it. + +- fix issue357 - special case "-k" expressions to allow for + filtering with simple strings that are not valid python expressions. + Examples: "-k 1.3" matches all tests parametrized with 1.3. + "-k None" filters all tests that have "None" in their name + and conversely "-k 'not None'". + Previously these examples would raise syntax errors. + +- fix issue384 by removing the trial support code + since the unittest compat enhancements allow + trial to handle it on its own + +- don't hide an ImportError when importing a plugin produces one. + fixes issue375. + +- fix issue275 - allow usefixtures and autouse fixtures + for running doctest text files. + +- fix issue380 by making --resultlog only rely on longrepr instead + of the "reprcrash" attribute which only exists sometimes. + +- address issue122: allow @pytest.fixture(params=iterator) by exploding + into a list early on. + +- fix pexpect-3.0 compatibility for pytest's own tests. + (fixes issue386) + +- allow nested parametrize-value markers, thanks James Lan for the PR. + +- fix unicode handling with new monkeypatch.setattr(import_path, value) + API. Thanks Rob Dennis. Fixes issue371. + +- fix unicode handling with junitxml, fixes issue368. + +- In assertion rewriting mode on Python 2, fix the detection of coding + cookies. See issue #330. + +- make "--runxfail" turn imperative pytest.xfail calls into no ops + (it already did neutralize pytest.mark.xfail markers) + +- refine pytest / pkg_resources interactions: The AssertionRewritingHook + PEP302 compliant loader now registers itself with setuptools/pkg_resources + properly so that the pkg_resources.resource_stream method works properly. + Fixes issue366. Thanks for the investigations and full PR to Jason R. Coombs. + +- pytestconfig fixture is now session-scoped as it is the same object during the + whole test run. Fixes issue370. + +- avoid one surprising case of marker malfunction/confusion:: + + @pytest.mark.some(lambda arg: ...) + def test_function(): + + would not work correctly because pytest assumes @pytest.mark.some + gets a function to be decorated already. We now at least detect if this + arg is an lambda and thus the example will work. Thanks Alex Gaynor + for bringing it up. + +- xfail a test on pypy that checks wrong encoding/ascii (pypy does + not error out). fixes issue385. + +- internally make varnames() deal with classes's __init__, + although it's not needed by pytest itself atm. Also + fix caching. Fixes issue376. + +- fix issue221 - handle importing of namespace-package with no + __init__.py properly. + +- refactor internal FixtureRequest handling to avoid monkeypatching. + One of the positive user-facing effects is that the "request" object + can now be used in closures. + +- fixed version comparison in pytest.importskip(modname, minverstring) + +- fix issue377 by clarifying in the nose-compat docs that pytest + does not duplicate the unittest-API into the "plain" namespace. + +- fix verbose reporting for @mock'd test functions + +v2.4.2 +----------------------------------- + +- on Windows require colorama and a newer py lib so that py.io.TerminalWriter() + now uses colorama instead of its own ctypes hacks. (fixes issue365) + thanks Paul Moore for bringing it up. + +- fix "-k" matching of tests where "repr" and "attr" and other names would + cause wrong matches because of an internal implementation quirk + (don't ask) which is now properly implemented. fixes issue345. + +- avoid tmpdir fixture to create too long filenames especially + when parametrization is used (issue354) + +- fix pytest-pep8 and pytest-flakes / pytest interactions + (collection names in mark plugin was assuming an item always + has a function which is not true for those plugins etc.) + Thanks Andi Zeidler. + +- introduce node.get_marker/node.add_marker API for plugins + like pytest-pep8 and pytest-flakes to avoid the messy + details of the node.keywords pseudo-dicts. Adapated + docs. + +- remove attempt to "dup" stdout at startup as it's icky. + the normal capturing should catch enough possibilities + of tests messing up standard FDs. + +- add pluginmanager.do_configure(config) as a link to + config.do_configure() for plugin-compatibility + +v2.4.1 +----------------------------------- + +- When using parser.addoption() unicode arguments to the + "type" keyword should also be converted to the respective types. + thanks Floris Bruynooghe, @dnozay. (fixes issue360 and issue362) + +- fix dotted filename completion when using argcomplete + thanks Anthon van der Neuth. (fixes issue361) + +- fix regression when a 1-tuple ("arg",) is used for specifying + parametrization (the values of the parametrization were passed + nested in a tuple). Thanks Donald Stufft. + +- merge doc typo fixes, thanks Andy Dirnberger + +v2.4 +----------------------------------- + +known incompatibilities: + +- if calling --genscript from python2.7 or above, you only get a + standalone script which works on python2.7 or above. Use Python2.6 + to also get a python2.5 compatible version. + +- all xunit-style teardown methods (nose-style, pytest-style, + unittest-style) will not be called if the corresponding setup method failed, + see issue322 below. + +- the pytest_plugin_unregister hook wasn't ever properly called + and there is no known implementation of the hook - so it got removed. + +- pytest.fixture-decorated functions cannot be generators (i.e. use + yield) anymore. This change might be reversed in 2.4.1 if it causes + unforeseen real-life issues. However, you can always write and return + an inner function/generator and change the fixture consumer to iterate + over the returned generator. This change was done in lieu of the new + ``pytest.yield_fixture`` decorator, see below. + +new features: + +- experimentally introduce a new ``pytest.yield_fixture`` decorator + which accepts exactly the same parameters as pytest.fixture but + mandates a ``yield`` statement instead of a ``return statement`` from + fixture functions. This allows direct integration with "with-style" + context managers in fixture functions and generally avoids registering + of finalization callbacks in favour of treating the "after-yield" as + teardown code. Thanks Andreas Pelme, Vladimir Keleshev, Floris + Bruynooghe, Ronny Pfannschmidt and many others for discussions. + +- allow boolean expression directly with skipif/xfail + if a "reason" is also specified. Rework skipping documentation + to recommend "condition as booleans" because it prevents surprises + when importing markers between modules. Specifying conditions + as strings will remain fully supported. + +- reporting: color the last line red or green depending if + failures/errors occured or everything passed. thanks Christian + Theunert. + +- make "import pdb ; pdb.set_trace()" work natively wrt capturing (no + "-s" needed anymore), making ``pytest.set_trace()`` a mere shortcut. + +- fix issue181: --pdb now also works on collect errors (and + on internal errors) . This was implemented by a slight internal + refactoring and the introduction of a new hook + ``pytest_exception_interact`` hook (see next item). + +- fix issue341: introduce new experimental hook for IDEs/terminals to intercept debugging: ``pytest_exception_interact(node, call, report)``. +- new monkeypatch.setattr() variant to provide a shorter + invocation for patching out classes/functions from modules: + + monkeypatch.setattr("requests.get", myfunc) + + will replace the "get" function of the "requests" module with ``myfunc``. + +- fix issue322: tearDownClass is not run if setUpClass failed. Thanks + Mathieu Agopian for the initial fix. Also make all of pytest/nose + finalizer mimick the same generic behaviour: if a setupX exists and + fails, don't run teardownX. This internally introduces a new method + "node.addfinalizer()" helper which can only be called during the setup + phase of a node. + +- simplify pytest.mark.parametrize() signature: allow to pass a + CSV-separated string to specify argnames. For example: + ``pytest.mark.parametrize("input,expected", [(1,2), (2,3)])`` + works as well as the previous: + ``pytest.mark.parametrize(("input", "expected"), ...)``. + +- add support for setUpModule/tearDownModule detection, thanks Brian Okken. + +- integrate tab-completion on options through use of "argcomplete". + Thanks Anthon van der Neut for the PR. + +- change option names to be hyphen-separated long options but keep the + old spelling backward compatible. py.test -h will only show the + hyphenated version, for example "--collect-only" but "--collectonly" + will remain valid as well (for backward-compat reasons). Many thanks to + Anthon van der Neut for the implementation and to Hynek Schlawack for + pushing us. + +- fix issue 308 - allow to mark/xfail/skip individual parameter sets + when parametrizing. Thanks Brianna Laugher. + +- call new experimental pytest_load_initial_conftests hook to allow + 3rd party plugins to do something before a conftest is loaded. + +Bug fixes: + +- fix issue358 - capturing options are now parsed more properly + by using a new parser.parse_known_args method. + +- pytest now uses argparse instead of optparse (thanks Anthon) which + means that "argparse" is added as a dependency if installing into python2.6 + environments or below. + +- fix issue333: fix a case of bad unittest/pytest hook interaction. + - PR27: correctly handle nose.SkipTest during collection. Thanks Antonio Cuni, Ronny Pfannschmidt. -- new monkeypatch.replace() to avoid imports and provide a shorter - invocation for patching out classes/functions from modules: - - monkeypatch.replace("requests.get", myfunc - - will replace the "get" function of the "requests" module with ``myfunc``. - -- fix issue322: tearDownClass is not run if setUpClass failed. Thanks - Mathieu Agopian for the initial fix. Also make all of pytest/nose finalizer - mimick the same generic behaviour: if a setupX exists and fails, - don't run teardownX. This also introduces a new method "node.addfinalizer()" - helper which can only be called during the setup phase of a node. +- fix issue355: junitxml puts name="pytest" attribute to testsuite tag. - fix issue336: autouse fixture in plugins should work again. -- change to use hyphen-separated long options but keep the old spelling - backward compatible. py.test -h will only show the hyphenated version, - for example "--collect-only" but "--collectonly" will remain valid as well - (for backward-compat reasons). Many thanks to Anthon van der Neut for - the implementation and to Hynek Schlawack for pushing us. - - fix issue279: improve object comparisons on assertion failure for standard datatypes and recognise collections.abc. Thanks to Brianna Laugher and Mathieu Agopian. @@ -49,19 +352,9 @@ Changes between 2.3.5 and 2.4.DEV - fix issue305: ignore any problems when writing pyc files. -- integrate tab-completion on options through use of "argcomplete". - Thanks Anthon van der Neut for the PR. - -- pytest now uses argparse instead of optparse (thanks Anthon) which - means that "argparse" is added as a dependency if installing into python2.6 - environments or below. - - SO-17664702: call fixture finalizers even if the fixture function partially failed (finalizers would not always be called before) -- color the last line red or green depending if failures/errors occured - or everything passed. thanks Christian Theunert. - - fix issue320 - fix class scope for fixtures when mixed with module-level functions. Thanks Anatloy Bubenkoff. @@ -73,35 +366,16 @@ Changes between 2.3.5 and 2.4.DEV - fix issue323 - sorting of many module-scoped arg parametrizations -- add support for setUpModule/tearDownModule detection, thanks Brian Okken. - - make sessionfinish hooks execute with the same cwd-context as at - session start (helps fix plugin behaviour which write output files + session start (helps fix plugin behaviour which write output files with relative path such as pytest-cov) - fix issue316 - properly reference collection hooks in docs -- fix issue 308 - allow to mark/xfail/skip individual parameter sets - when parametrizing. Thanks Brianna Laugher. - -- simplify parametrize() signature: allow to pass a CSV-separated string - to specify argnames. For example: ``pytest.mark.parametrize("input,expected", [(1,2), (2,3)])`` is possible now in addition to the prior - ``pytest.mark.parametrize(("input", "expected"), ...)``. - - fix issue 306 - cleanup of -k/-m options to only match markers/test names/keywords respectively. Thanks Wouter van Ackooy. -- (experimental) allow fixture functions to be - implemented as context managers. Thanks Andreas Pelme, - Vladimir Keleshev. - -- (experimental) allow boolean expression directly with skipif/xfail - if a "reason" is also specified. Rework skipping documentation - to recommend "condition as booleans" because it prevents surprises - when importing markers between modules. Specifying conditions - as strings will remain fully supported. - -- improved doctest counting for doctests in python modules -- +- improved doctest counting for doctests in python modules -- files without any doctest items will not show up anymore and doctest examples are counted as separate test items. thanks Danilo Bellini. @@ -111,7 +385,7 @@ Changes between 2.3.5 and 2.4.DEV mode. Thanks Jason R. Coombs. - fix junitxml generation when test output contains control characters, - addressing issue267, thanks Jaap Broekhuizen + addressing issue267, thanks Jaap Broekhuizen - fix issue338: honor --tb style for setup/teardown errors as well. Thanks Maho. @@ -119,15 +393,11 @@ Changes between 2.3.5 and 2.4.DEV - better parametrize error messages, thanks Brianna Laugher +- pytest_terminal_summary(terminalreporter) hooks can now use + ".section(title)" and ".line(msg)" methods to print extra + information at the end of a test run. -known incompatibilities: - -- if calling --genscript from python2.7 or above, you only get a - standalone script which works on python2.7 or above. Use Python2.6 - to also get a python2.5 compatible version. - - -Changes between 2.3.4 and 2.3.5 +v2.3.5 ----------------------------------- - fix issue169: respect --tb=style with setup/teardown errors as well. @@ -178,7 +448,7 @@ Changes between 2.3.4 and 2.3.5 - fix bug where using capsys with pytest.set_trace() in a test function would break when looking at capsys.readouterr() -- allow to specify prefixes starting with "_" when +- allow to specify prefixes starting with "_" when customizing python_functions test discovery. (thanks Graham Horler) - improve PYTEST_DEBUG tracing output by puting @@ -192,10 +462,10 @@ Changes between 2.3.4 and 2.3.5 - fix issue266 - accept unicode in MarkEvaluator expressions -Changes between 2.3.3 and 2.3.4 +v2.3.4 ----------------------------------- -- yielded test functions will now have autouse-fixtures active but +- yielded test functions will now have autouse-fixtures active but cannot accept fixtures as funcargs - it's anyway recommended to rather use the post-2.0 parametrize features instead of yield, see: http://pytest.org/latest/example/parametrize.html @@ -210,9 +480,9 @@ Changes between 2.3.3 and 2.3.4 can write: -k "name1 or name2" etc. This is a slight incompatibility if you used special syntax like "TestClass.test_method" which you now need to write as -k "TestClass and test_method" to match a certain - method in a certain test class. + method in a certain test class. -Changes between 2.3.2 and 2.3.3 +v2.3.3 ----------------------------------- - fix issue214 - parse modules that contain special objects like e. g. @@ -244,10 +514,10 @@ Changes between 2.3.2 and 2.3.3 - fix issue127 - improve documentation for pytest_addoption() and add a ``config.getoption(name)`` helper function for consistency. -Changes between 2.3.1 and 2.3.2 +v2.3.2 ----------------------------------- -- fix issue208 and fix issue29 use new py version to avoid long pauses +- fix issue208 and fix issue29 use new py version to avoid long pauses when printing tracebacks in long modules - fix issue205 - conftests in subdirs customizing @@ -277,7 +547,7 @@ Changes between 2.3.1 and 2.3.2 - add tox.ini to pytest distribution so that ignore-dirs and others config bits are properly distributed for maintainers who run pytest-own tests -Changes between 2.3.0 and 2.3.1 +v2.3.1 ----------------------------------- - fix issue202 - fix regression: using "self" from fixture functions now @@ -290,7 +560,7 @@ Changes between 2.3.0 and 2.3.1 - link to web pages from --markers output which provides help for pytest.mark.* usage. -Changes between 2.2.4 and 2.3.0 +v2.3.0 ----------------------------------- - fix issue202 - better automatic names for parametrized test functions @@ -331,7 +601,7 @@ Changes between 2.2.4 and 2.3.0 - pluginmanager.register(...) now raises ValueError if the plugin has been already registered or the name is taken -- fix issue159: improve http://pytest.org/latest/faq.html +- fix issue159: improve http://pytest.org/latest/faq.html especially with respect to the "magic" history, also mention pytest-django, trial and unittest integration. @@ -362,14 +632,14 @@ Changes between 2.2.4 and 2.3.0 you can use startdir.bestrelpath(yourpath) to show nice relative path - - allow plugins to implement both pytest_report_header and + - allow plugins to implement both pytest_report_header and pytest_sessionstart (sessionstart is invoked first). - don't show deselected reason line if there is none - py.test -vv will show all of assert comparisations instead of truncating -Changes between 2.2.3 and 2.2.4 +v2.2.4 ----------------------------------- - fix error message for rewritten assertions involving the % operator @@ -386,17 +656,17 @@ Changes between 2.2.3 and 2.2.4 - fix issue #144: better mangle test ids to junitxml classnames - upgrade distribute_setup.py to 0.6.27 -Changes between 2.2.2 and 2.2.3 +v2.2.3 ---------------------------------------- - fix uploaded package to only include neccesary files -Changes between 2.2.1 and 2.2.2 +v2.2.2 ---------------------------------------- - fix issue101: wrong args to unittest.TestCase test function now produce better output -- fix issue102: report more useful errors and hints for when a +- fix issue102: report more useful errors and hints for when a test directory was renamed and some pyc/__pycache__ remain - fix issue106: allow parametrize to be applied multiple times e.g. from module, class and at function level. @@ -411,11 +681,11 @@ Changes between 2.2.1 and 2.2.2 - allow adding of attributes to test reports such that it also works with distributed testing (no upgrade of pytest-xdist needed) -Changes between 2.2.0 and 2.2.1 +v2.2.1 ---------------------------------------- - fix issue99 (in pytest and py) internallerrors with resultlog now - produce better output - fixed by normalizing pytest_internalerror + produce better output - fixed by normalizing pytest_internalerror input arguments. - fix issue97 / traceback issues (in pytest and py) improve traceback output in conjunction with jinja2 and cython which hack tracebacks @@ -423,25 +693,25 @@ Changes between 2.2.0 and 2.2.1 the final test in a test node will now run its teardown directly instead of waiting for the end of the session. Thanks Dave Hunt for the good reporting and feedback. The pytest_runtest_protocol as well - as the pytest_runtest_teardown hooks now have "nextitem" available + as the pytest_runtest_teardown hooks now have "nextitem" available which will be None indicating the end of the test run. - fix collection crash due to unknown-source collected items, thanks to Ralf Schmitt (fixed by depending on a more recent pylib) -Changes between 2.1.3 and 2.2.0 +v2.2.0 ---------------------------------------- - fix issue90: introduce eager tearing down of test items so that teardown function are called earlier. -- add an all-powerful metafunc.parametrize function which allows to +- add an all-powerful metafunc.parametrize function which allows to parametrize test function arguments in multiple steps and therefore - from indepdenent plugins and palces. + from indepdenent plugins and palces. - add a @pytest.mark.parametrize helper which allows to easily call a test function with different argument values -- Add examples to the "parametrize" example page, including a quick port +- Add examples to the "parametrize" example page, including a quick port of Test scenarios and the new parametrize function and decorator. - introduce registration for "pytest.mark.*" helpers via ini-files - or through plugin hooks. Also introduce a "--strict" option which + or through plugin hooks. Also introduce a "--strict" option which will treat unregistered markers as errors allowing to avoid typos and maintain a well described set of markers for your test suite. See exaples at http://pytest.org/latest/mark.html @@ -450,12 +720,12 @@ Changes between 2.1.3 and 2.2.0 (this is a stricter and more predictable version of '-k' in that "-m" only matches complete markers and has more obvious rules for and/or semantics. -- new feature to help optimizing the speed of your tests: - --durations=N option for displaying N slowest test calls +- new feature to help optimizing the speed of your tests: + --durations=N option for displaying N slowest test calls and setup/teardown methods. - fix issue87: --pastebin now works with python3 - fix issue89: --pdb with unexpected exceptions in doctest work more sensibly -- fix and cleanup pytest's own test suite to not leak FDs +- fix and cleanup pytest's own test suite to not leak FDs - fix issue83: link to generated funcarg list - fix issue74: pyarg module names are now checked against imp.find_module false positives - fix compatibility with twisted/trial-11.1.0 use cases @@ -463,7 +733,7 @@ Changes between 2.1.3 and 2.2.0 - simplify junitxml output code by relying on py.xml - add support for skip properties on unittest classes and functions -Changes between 2.1.2 and 2.1.3 +v2.1.3 ---------------------------------------- - fix issue79: assertion rewriting failed on some comparisons in boolops @@ -472,7 +742,7 @@ Changes between 2.1.2 and 2.1.3 - fix issue75 / skipping test failure on jython - fix issue77 / Allow assertrepr_compare hook to apply to a subset of tests -Changes between 2.1.1 and 2.1.2 +v2.1.2 ---------------------------------------- - fix assertion rewriting on files with windows newlines on some Python versions @@ -482,7 +752,7 @@ Changes between 2.1.1 and 2.1.2 - fix issue66: use different assertion rewriting caches when the -O option is passed - don't try assertion rewriting on Jython, use reinterp -Changes between 2.1.0 and 2.1.1 +v2.1.1 ---------------------------------------------- - fix issue64 / pytest.set_trace now works within pytest_generate_tests hooks @@ -495,7 +765,7 @@ Changes between 2.1.0 and 2.1.1 - fix issue61: assertion rewriting on boolean operations with 3 or more operands - you can now build a man page with "cd doc ; make man" -Changes between 2.0.3 and 2.1.0.DEV +v2.1.0 ---------------------------------------------- - fix issue53 call nosestyle setup functions with correct ordering @@ -515,7 +785,7 @@ Changes between 2.0.3 and 2.1.0.DEV - report KeyboardInterrupt even if interrupted during session startup - fix issue 35 - provide PDF doc version and download link from index page -Changes between 2.0.2 and 2.0.3 +v2.0.3 ---------------------------------------------- - fix issue38: nicer tracebacks on calls to hooks, particularly early @@ -535,7 +805,7 @@ Changes between 2.0.2 and 2.0.3 - fix issue37: avoid invalid characters in junitxml's output -Changes between 2.0.1 and 2.0.2 +v2.0.2 ---------------------------------------------- - tackle issue32 - speed up test runs of very quick test functions @@ -547,17 +817,17 @@ Changes between 2.0.1 and 2.0.2 Also you can now access module globals from xfail/skipif expressions so that this for example works now:: - + import pytest import mymodule @pytest.mark.skipif("mymodule.__version__[0] == "1") def test_function(): pass - This will not run the test function if the module's version string + This will not run the test function if the module's version string does not start with a "1". Note that specifying a string instead - of a boolean expressions allows py.test to report meaningful information - when summarizing a test run as to what conditions lead to skipping + of a boolean expressions allows py.test to report meaningful information + when summarizing a test run as to what conditions lead to skipping (or xfail-ing) tests. - fix issue28 - setup_method and pytest_generate_tests work together @@ -580,14 +850,14 @@ Changes between 2.0.1 and 2.0.2 - fixed typos in the docs (thanks Victor Garcia, Brianna Laugher) and particular thanks to Laura Creighton who also revieved parts of the documentation. -- fix slighly wrong output of verbose progress reporting for classes +- fix slighly wrong output of verbose progress reporting for classes (thanks Amaury) - more precise (avoiding of) deprecation warnings for node.Class|Function accesses - avoid std unittest assertion helper code in tracebacks (thanks Ronny) -Changes between 2.0.0 and 2.0.1 +v2.0.1 ---------------------------------------------- - refine and unify initial capturing so that it works nicely @@ -596,7 +866,7 @@ Changes between 2.0.0 and 2.0.1 - allow to omit "()" in test ids to allow for uniform test ids as produced by Alfredo's nice pytest.vim plugin. - fix issue12 - show plugin versions with "--version" and - "--traceconfig" and also document how to add extra information + "--traceconfig" and also document how to add extra information to reporting test header - fix issue17 (import-* reporting issue on python3) by requiring py>1.4.0 (1.4.1 is going to include it) @@ -626,17 +896,17 @@ Changes between 2.0.0 and 2.0.1 - fix issue14: no logging errors at process exit - refinements to "collecting" output on non-ttys - refine internal plugin registration and --traceconfig output -- introduce a mechanism to prevent/unregister plugins from the +- introduce a mechanism to prevent/unregister plugins from the command line, see http://pytest.org/plugins.html#cmdunregister - activate resultlog plugin by default - fix regression wrt yielded tests which due to the - collection-before-running semantics were not + collection-before-running semantics were not setup as with pytest 1.3.4. Note, however, that - the recommended and much cleaner way to do test + the recommended and much cleaner way to do test parametraization remains the "pytest_generate_tests" mechanism, see the docs. -Changes between 1.3.4 and 2.0.0 +v2.0.0 ---------------------------------------------- - pytest-2.0 is now its own package and depends on pylib-2.0 @@ -681,7 +951,7 @@ Changes between 1.3.4 and 2.0.0 - add ability to use "class" level for cached_setup helper - fix strangeness: mark.* objects are now immutable, create new instances -Changes between 1.3.3 and 1.3.4 +v1.3.4 ---------------------------------------------- - fix issue111: improve install documentation for windows @@ -690,7 +960,7 @@ Changes between 1.3.3 and 1.3.4 - fix issue115: unify internal exception passthrough/catching/GeneratorExit - fix issue118: new --tb=native for presenting cpython-standard exceptions -Changes between 1.3.2 and 1.3.3 +v1.3.3 ---------------------------------------------- - fix issue113: assertion representation problem with triple-quoted strings @@ -705,7 +975,7 @@ Changes between 1.3.2 and 1.3.3 (thanks Armin Ronacher for reporting) - remove trailing whitespace in all py/text distribution files -Changes between 1.3.1 and 1.3.2 +v1.3.2 ---------------------------------------------- New features @@ -780,7 +1050,7 @@ Bug fixes / Maintenance - fix homedir detection on Windows - ship distribute_setup.py version 0.6.13 -Changes between 1.3.0 and 1.3.1 +v1.3.1 --------------------------------------------- New features @@ -852,7 +1122,7 @@ Fixes / Maintenance (and internally be more careful when presenting unexpected byte sequences) -Changes between 1.2.1 and 1.3.0 +v1.3.0 --------------------------------------------- - deprecate --report option in favour of a new shorter and easier to @@ -917,7 +1187,7 @@ Changes between 1.2.1 and 1.3.0 - added links to the new capturelog and coverage plugins -Changes between 1.2.1 and 1.2.0 +v1.2.0 --------------------------------------------- - refined usage and options for "py.cleanup":: @@ -956,7 +1226,7 @@ Changes between 1.2.1 and 1.2.0 - fix plugin links -Changes between 1.2 and 1.1.1 +v1.1.1 --------------------------------------------- - moved dist/looponfailing from py.test core into a new @@ -1040,7 +1310,7 @@ Changes between 1.2 and 1.1.1 - fix docs, fix internal bin/ script generation -Changes between 1.1.1 and 1.1.0 +v1.1.0 --------------------------------------------- - introduce automatic plugin registration via 'pytest11' @@ -1059,7 +1329,7 @@ Changes between 1.1.1 and 1.1.0 - try harder to have deprecation warnings for py.compat.* accesses report a correct location -Changes between 1.1.0 and 1.0.2 +v1.0.2 --------------------------------------------- * adjust and improve docs @@ -1144,7 +1414,7 @@ Changes between 1.1.0 and 1.0.2 * simplified internal localpath implementation -Changes between 1.0.1 and 1.0.2 +v1.0.2 ------------------------------------------- * fixing packaging issues, triggered by fedora redhat packaging, @@ -1152,7 +1422,7 @@ Changes between 1.0.1 and 1.0.2 * added a documentation link to the new django plugin. -Changes between 1.0.0 and 1.0.1 +v1.0.1 ------------------------------------------- * added a 'pytest_nose' plugin which handles nose.SkipTest, @@ -1186,13 +1456,13 @@ Changes between 1.0.0 and 1.0.1 * simplified multicall mechanism and plugin architecture, renamed some internal methods and argnames -Changes between 1.0.0b9 and 1.0.0 +v1.0.0 ------------------------------------------- * more terse reporting try to show filesystem path relatively to current dir * improve xfail output a bit -Changes between 1.0.0b8 and 1.0.0b9 +v1.0.0b9 ------------------------------------------- * cleanly handle and report final teardown of test setup @@ -1226,7 +1496,7 @@ Changes between 1.0.0b8 and 1.0.0b9 * item.repr_failure(excinfo) instead of item.repr_failure(excinfo, outerr) -Changes between 1.0.0b7 and 1.0.0b8 +v1.0.0b8 ------------------------------------------- * pytest_unittest-plugin is now enabled by default @@ -1255,7 +1525,7 @@ Changes between 1.0.0b7 and 1.0.0b8 * tweaked doctest output for docstrings in py modules, thanks Radomir. -Changes between 1.0.0b3 and 1.0.0b7 +v1.0.0b7 ------------------------------------------- * renamed py.test.xfail back to py.test.mark.xfail to avoid @@ -1280,7 +1550,7 @@ Changes between 1.0.0b3 and 1.0.0b7 * make __name__ == "__channelexec__" for remote_exec code -Changes between 1.0.0b1 and 1.0.0b3 +v1.0.0b3 ------------------------------------------- * plugin classes are removed: one now defines @@ -1297,7 +1567,7 @@ Changes between 1.0.0b1 and 1.0.0b3 well with function arguments. -Changes between 0.9.2 and 1.0.0b1 +v1.0.0b1 ------------------------------------------- * introduced new "funcarg" setup method, @@ -1321,7 +1591,7 @@ Changes between 0.9.2 and 1.0.0b1 XXX lots of things missing here XXX -Changes between 0.9.1 and 0.9.2 +v0.9.2 ------------------------------------------- * refined installation and metadata, created new setup.py, @@ -1354,10 +1624,10 @@ Changes between 0.9.1 and 0.9.2 * there now is a py.__version__ attribute -Changes between 0.9.0 and 0.9.1 +v0.9.1 ------------------------------------------- -This is a fairly complete list of changes between 0.9 and 0.9.1, which can +This is a fairly complete list of v0.9.1, which can serve as a reference for developers. * allowing + signs in py.path.svn urls [39106] diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 000000000..b65731cb7 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,171 @@ +============ +Contributing +============ + +Contributions are highly welcomed and appreciated. Every little help counts, +so do not hesitate! + + +Types of contributions +====================== + +Report bugs +----------- + +Report bugs at https://bitbucket.org/hpk42/pytest/issues. + +If you are reporting a bug, please include: + +* Your operating system name and version. +* Any details about your local setup that might be helpful in troubleshooting, + specifically Python interpreter version, + installed libraries and pytest version. +* Detailed steps to reproduce the bug. + +Submit feedback for developers +------------------------------ + +Do you like pytest? Share some love on Twitter or in your blog posts! + +We'd also like to hear about your propositions and suggestions. Feel free to +`submit them as issues `__ and: + +* Set the "kind" to "enhancement" or "proposal" so that we can quickly find + about them. +* Explain in detail how they should work. +* Keep the scope as narrow as possible. This will make it easier to implement. +* If you have required skills and/or knowledge, we are very happy for + :ref:`pull requests `. + + +Fix bugs +-------- + +Look through the BitBucket issues for bugs. Here is sample filter you can use: +https://bitbucket.org/hpk42/pytest/issues?status=new&status=open&kind=bug + +:ref:`Talk ` to developers to find out how you can fix specific bugs. + +Implement features +------------------ + +Look through the BitBucket issues for enhancements. Here is sample filter you +can use: +https://bitbucket.org/hpk42/pytest/issues?status=new&status=open&kind=enhancement + +:ref:`Talk ` to developers to find out how you can implement specific +features. + +Write documentation +------------------- + +pytest could always use more documentation. What exactly is needed? + +* More complementary documentation. Have you perhaps found something unclear? +* Documentation translations. We currently have English and Japanese versions. +* Docstrings. There's never too much of them. +* Blog posts, articles and such -- they're all very appreciated. + +.. _pull-requests: + +Preparing Pull Requests on Bitbucket +===================================== + +.. note:: + What is a "pull request"? It informs project's core developers about the + changes you want to review and merge. Pull requests are stored on + `BitBucket servers `__. + Once you send pull request, we can discuss it's potential modifications and + even add more commits to it later on. + +The primary development platform for pytest is BitBucket. You can find all +the issues there and submit your pull requests. + +1. Fork the + `pytest BitBucket repository `__. It's + fine to use ``pytest`` as your fork repository name because it will live + under your user. + +.. _virtualenvactivate: + +2. Create and activate a fork-specific virtualenv + (http://www.virtualenv.org/en/latest/):: + + $ virtualenv pytest-venv + $ source pytest-venv/bin/activate + +.. _checkout: + +3. Clone your fork locally using `Mercurial `_ + (``hg``) and create a branch:: + + $ hg clone ssh://hg@bitbucket.org/YOUR_BITBUCKET_USERNAME/pytest + $ cd pytest + $ hg branch your-branch-name + + If you need some help with Mercurial, follow this quick start + guide: http://mercurial.selenic.com/wiki/QuickStart + +.. _testing-pytest: + +4. You can now edit your local working copy. To test you need to + install the "tox" tool into your virtualenv:: + + $ pip install tox + + You need to have Python 2.7 and 3.3 available in your system. Now + running tests is as simple as issuing this command:: + + $ python runtox.py -e py27,py33,flakes + + This command will run tests via the "tox" tool against Python 2.7 and 3.3 + and also perform "flakes" coding-style checks. ``runtox.py`` is + a thin wrapper around ``tox`` which installs from a development package + index where newer (not yet released to pypi) versions of dependencies + (especially ``py``) might be present. + + To run tests on py27 and pass options (e.g. enter pdb on failure) + to pytest you can do:: + + $ python runtox.py -e py27 -- --pdb + + or to only run tests in a particular test module on py33:: + + $ python runtox.py -e py33 -- testing/test_config.py + +5. Commit and push once your tests pass and you are happy with your change(s):: + + $ hg commit -m"" + $ hg push -b . + +6. Finally, submit a pull request through the BitBucket website: + + .. image:: img/pullrequest.png + :width: 700px + :align: center + + :: + + source: YOUR_BITBUCKET_USERNAME/pytest + branch: your-branch-name + + target: hpk42/pytest + branch: default + +.. _contribution-using-git: + +What about git (and so GitHub)? +------------------------------- + +There used to be the pytest GitHub mirror. It was removed in favor of the +Mercurial one, to remove confusion of people not knowing where it's better to +put their issues and pull requests. Also it wasn't easily possible to automate +the mirroring process. + +However, it's still possible to use git to contribute to pytest using tools +like `gitifyhg `_ which allows you to +clone and work with Mercurial repo still using git. + +.. warning:: + Remember that git is **not** a default version control system for pytest and + you need to be careful using it. diff --git a/ISSUES.txt b/ISSUES.txt index b02a7a683..15c341a7b 100644 --- a/ISSUES.txt +++ b/ISSUES.txt @@ -122,8 +122,8 @@ customize test function collection ------------------------------------------------------- tags: feature -- introduce py.test.mark.nocollect for not considering a function for - test collection at all. maybe also introduce a py.test.mark.test to +- introduce pytest.mark.nocollect for not considering a function for + test collection at all. maybe also introduce a pytest.mark.test to explicitely mark a function to become a tested one. Lookup JUnit ways of tagging tests. @@ -135,18 +135,18 @@ in addition to the imperative pytest.importorskip also introduce a pytest.mark.importorskip so that the test count is more correct. -introduce py.test.mark.platform +introduce pytest.mark.platform ------------------------------------------------------- tags: feature Introduce nice-to-spell platform-skipping, examples: - @py.test.mark.platform("python3") - @py.test.mark.platform("not python3") - @py.test.mark.platform("win32 and not python3") - @py.test.mark.platform("darwin") - @py.test.mark.platform("not (jython and win32)") - @py.test.mark.platform("not (jython and win32)", xfail=True) + @pytest.mark.platform("python3") + @pytest.mark.platform("not python3") + @pytest.mark.platform("win32 and not python3") + @pytest.mark.platform("darwin") + @pytest.mark.platform("not (jython and win32)") + @pytest.mark.platform("not (jython and win32)", xfail=True) etc. Idea is to allow Python expressions which can operate on common spellings for operating systems and python @@ -181,8 +181,8 @@ tags: feature allow to name conftest.py files (in sub directories) that should be imported early, as to include command line options. -improve central py.test ini file ----------------------------------- +improve central pytest ini file +------------------------------- tags: feature introduce more declarative configuration options: @@ -196,7 +196,7 @@ new documentation ---------------------------------- tags: feature -- logo py.test +- logo pytest - examples for unittest or functional testing - resource management for functional testing - patterns: page object @@ -205,17 +205,17 @@ have imported module mismatch honour relative paths -------------------------------------------------------- tags: bug -With 1.1.1 py.test fails at least on windows if an import +With 1.1.1 pytest fails at least on windows if an import is relative and compared against an absolute conftest.py path. Normalize. -consider globals: py.test.ensuretemp and config +consider globals: pytest.ensuretemp and config -------------------------------------------------------------- tags: experimental-wish -consider deprecating py.test.ensuretemp and py.test.config -to further reduce py.test globality. Also consider -having py.test.config and ensuretemp coming from +consider deprecating pytest.ensuretemp and pytest.config +to further reduce pytest globality. Also consider +having pytest.config and ensuretemp coming from a plugin rather than being there from the start. @@ -223,7 +223,7 @@ consider pytest_addsyspath hook ----------------------------------------- tags: wish -py.test could call a new pytest_addsyspath() in order to systematically +pytest could call a new pytest_addsyspath() in order to systematically allow manipulation of sys.path and to inhibit it via --no-addsyspath in order to more easily run against installed packages. @@ -232,11 +232,11 @@ and pytest_configure. -deprecate global py.test.config usage +deprecate global pytest.config usage ---------------------------------------------------------------- tags: feature -py.test.ensuretemp and py.test.config are probably the last +pytest.ensuretemp and pytest.config are probably the last objects containing global state. Often using them is not neccessary. This is about trying to get rid of them, i.e. deprecating them and checking with PyPy's usages as well diff --git a/README.rst b/README.rst index c48afb380..81fb2b6c2 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,11 @@ -The ``py.test`` testing tool makes it easy to write small tests, yet + +Documentation: http://pytest.org/latest/ + +Changelog: http://pytest.org/latest/changelog.html + +Issues: https://bitbucket.org/hpk42/pytest/issues?status=open + +The ``pytest`` testing tool makes it easy to write small tests, yet scales to support complex functional testing. It provides - `auto-discovery @@ -7,17 +14,14 @@ scales to support complex functional testing. It provides - detailed info on failing `assert statements `_ (no need to remember ``self.assert*`` names) - `modular fixtures `_ for managing small or parametrized long-lived test resources. -- multi-paradigm support: you can use ``py.test`` to run test suites based +- multi-paradigm support: you can use ``pytest`` to run test suites based on `unittest `_ (or trial), `nose `_ -- single-source compatibility to Python2.4 all the way up to Python3.3, +- single-source compatibility to Python2.5 all the way up to Python3.3, PyPy-1.9 and Jython-2.5.1. - many `external plugins `_. -.. image:: https://secure.travis-ci.org/hpk42/pytest.png - :target: http://travis-ci.org/hpk42/pytest - A simple example for a test:: # content of test_module.py diff --git a/_pytest/__init__.py b/_pytest/__init__.py index 7d731d0c1..2ec33245d 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.4.0.dev12' +__version__ = '2.5.2.dev1' diff --git a/_pytest/_argcomplete.py b/_pytest/_argcomplete.py index 8b4807c95..4f4eaf925 100644 --- a/_pytest/_argcomplete.py +++ b/_pytest/_argcomplete.py @@ -74,9 +74,13 @@ class FastFilesCompleter: else: prefix_dir = 0 completion = [] + globbed = [] if '*' not in prefix and '?' not in prefix: + if prefix[-1] == os.path.sep: # we are on unix, otherwise no bash + globbed.extend(glob(prefix + '.*')) prefix += '*' - for x in sorted(glob(prefix)): + globbed.extend(glob(prefix)) + for x in sorted(globbed): if os.path.isdir(x): x += '/' # append stripping the prefix (like bash, not like compgen) diff --git a/_pytest/assertion/__init__.py b/_pytest/assertion/__init__.py index ec841e7c0..8b4bae45c 100644 --- a/_pytest/assertion/__init__.py +++ b/_pytest/assertion/__init__.py @@ -3,7 +3,6 @@ support for presenting detailed information in failing assertions. """ import py import sys -import pytest from _pytest.monkeypatch import monkeypatch from _pytest.assertion import util @@ -19,7 +18,7 @@ def pytest_addoption(parser): to provide assert expression information. """) group.addoption('--no-assert', action="store_true", default=False, dest="noassert", help="DEPRECATED equivalent to --assert=plain") - group.addoption('--nomagic', '--no-magic', action="store_true", + group.addoption('--nomagic', '--no-magic', action="store_true", default=False, help="DEPRECATED equivalent to --assert=plain") class AssertionState: @@ -35,7 +34,7 @@ def pytest_configure(config): mode = "plain" if mode == "rewrite": try: - import ast + import ast # noqa except ImportError: mode = "reinterp" else: @@ -49,10 +48,10 @@ def pytest_configure(config): m = monkeypatch() config._cleanup.append(m.undo) m.setattr(py.builtin.builtins, 'AssertionError', - reinterpret.AssertionError) + reinterpret.AssertionError) # noqa hook = None if mode == "rewrite": - hook = rewrite.AssertionRewritingHook() + hook = rewrite.AssertionRewritingHook() # noqa sys.meta_path.insert(0, hook) warn_about_missing_assertion(mode) config._assertstate = AssertionState(config, mode) @@ -79,10 +78,13 @@ def pytest_runtest_setup(item): for new_expl in hook_result: if new_expl: - # Don't include pageloads of data unless we are very verbose (-vv) - if len(''.join(new_expl[1:])) > 80*8 and item.config.option.verbose < 2: - new_expl[1:] = ['Detailed information truncated, use "-vv" to see'] - res = '\n~'.join(new_expl) + # Don't include pageloads of data unless we are very + # verbose (-vv) + if (len(py.builtin._totext('').join(new_expl[1:])) > 80*8 + and item.config.option.verbose < 2): + new_expl[1:] = [py.builtin._totext( + 'Detailed information truncated, use "-vv" to see')] + res = py.builtin._totext('\n~').join(new_expl) if item.config.getvalue("assertmode") == "rewrite": # The result will be fed back a python % formatting # operation, which will fail if there are extraneous @@ -102,9 +104,9 @@ def pytest_sessionfinish(session): def _load_modules(mode): """Lazily import assertion related code.""" global rewrite, reinterpret - from _pytest.assertion import reinterpret + from _pytest.assertion import reinterpret # noqa if mode == "rewrite": - from _pytest.assertion import rewrite + from _pytest.assertion import rewrite # noqa def warn_about_missing_assertion(mode): try: diff --git a/_pytest/assertion/reinterpret.py b/_pytest/assertion/reinterpret.py index f7a0b2d56..fe1f87d2d 100644 --- a/_pytest/assertion/reinterpret.py +++ b/_pytest/assertion/reinterpret.py @@ -1,18 +1,26 @@ import sys import py from _pytest.assertion.util import BuiltinAssertionError +u = py.builtin._totext + class AssertionError(BuiltinAssertionError): def __init__(self, *args): BuiltinAssertionError.__init__(self, *args) if args: + # on Python2.6 we get len(args)==2 for: assert 0, (x,y) + # on Python2.7 and above we always get len(args) == 1 + # with args[0] being the (x,y) tuple. + if len(args) > 1: + toprint = args + else: + toprint = args[0] try: - self.msg = str(args[0]) - except py.builtin._sysex: - raise - except: - self.msg = "<[broken __repr__] %s at %0xd>" %( - args[0].__class__, id(args[0])) + self.msg = u(toprint) + except Exception: + self.msg = u( + "<[broken __repr__] %s at %0xd>" + % (toprint.__class__, id(toprint))) else: f = py.code.Frame(sys._getframe(1)) try: diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index 96b1a33d3..95bf4117d 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -15,7 +15,7 @@ import py from _pytest.assertion import util -# py.test caches rewritten pycs in __pycache__. +# pytest caches rewritten pycs in __pycache__. if hasattr(imp, "get_tag"): PYTEST_TAG = imp.get_tag() + "-PYTEST" else: @@ -41,6 +41,7 @@ class AssertionRewritingHook(object): def __init__(self): self.session = None self.modules = {} + self._register_with_pkg_resources() def set_session(self, session): self.fnpats = session.config.getini("python_files") @@ -55,8 +56,12 @@ class AssertionRewritingHook(object): names = name.rsplit(".", 1) lastname = names[-1] pth = None - if path is not None and len(path) == 1: - pth = path[0] + if path is not None: + # Starting with Python 3.3, path is a _NamespacePath(), which + # causes problems if not converted to list. + path = list(path) + if len(path) == 1: + pth = path[0] if pth is None: try: fd, fn, desc = imp.find_module(lastname, path) @@ -97,7 +102,7 @@ class AssertionRewritingHook(object): # the most magical part of the process: load the source, rewrite the # asserts, and load the rewritten source. We also cache the rewritten # module code in a special pyc. We must be aware of the possibility of - # concurrent py.test processes rewriting and loading pycs. To avoid + # concurrent pytest processes rewriting and loading pycs. To avoid # tricky race conditions, we maintain the following invariant: The # cached pyc is always a complete, valid pyc. Operations on it must be # atomic. POSIX's atomic rename comes in handy. @@ -169,6 +174,24 @@ class AssertionRewritingHook(object): tp = desc[2] return tp == imp.PKG_DIRECTORY + @classmethod + def _register_with_pkg_resources(cls): + """ + Ensure package resources can be loaded from this loader. May be called + multiple times, as the operation is idempotent. + """ + try: + import pkg_resources + # access an attribute in case a deferred importer is present + pkg_resources.__name__ + except ImportError: + return + + # Since pytest tests are always located in the file system, the + # DefaultProvider is appropriate. + pkg_resources.register_loader_type(cls, pkg_resources.DefaultProvider) + + def _write_pyc(state, co, source_path, pyc): # Technically, we don't have to have the same pyc format as # (C)Python, since these "pycs" should never be seen by builtin @@ -196,7 +219,7 @@ def _write_pyc(state, co, source_path, pyc): RN = "\r\n".encode("utf-8") N = "\n".encode("utf-8") -cookie_re = re.compile("coding[:=]\s*[-\w.]+") +cookie_re = re.compile(r"^[ \t\f]*#.*coding[:=][ \t]*[-\w.]+") BOM_UTF8 = '\xef\xbb\xbf' def _rewrite_test(state, fn): @@ -220,8 +243,8 @@ def _rewrite_test(state, fn): end1 = source.find("\n") end2 = source.find("\n", end1 + 1) if (not source.startswith(BOM_UTF8) and - (not cookie_re.match(source[0:end1]) or - not cookie_re.match(source[end1:end2]))): + cookie_re.match(source[0:end1]) is None and + cookie_re.match(source[end1 + 1:end2]) is None): if hasattr(state, "_indecode"): return None # encodings imported us again, we don't rewrite state._indecode = True @@ -267,7 +290,7 @@ def _make_rewritten_pyc(state, fn, pyc, co): os.rename(proc_pyc, pyc) def _read_pyc(source, pyc): - """Possibly read a py.test pyc containing rewritten code. + """Possibly read a pytest pyc containing rewritten code. Return rewritten code if successful or None if not. """ @@ -300,7 +323,7 @@ def rewrite_asserts(mod): _saferepr = py.io.saferepr -from _pytest.assertion.util import format_explanation as _format_explanation +from _pytest.assertion.util import format_explanation as _format_explanation # noqa def _should_repr_global_name(obj): return not hasattr(obj, "__name__") and not py.builtin.callable(obj) @@ -538,7 +561,8 @@ class AssertionRewriter(ast.NodeVisitor): for i, v in enumerate(boolop.values): if i: fail_inner = [] - self.on_failure.append(ast.If(cond, fail_inner, [])) + # cond is set in a prior loop iteration below + self.on_failure.append(ast.If(cond, fail_inner, [])) # noqa self.on_failure = fail_inner self.push_format_context() res, expl = self.visit(v) @@ -631,7 +655,7 @@ class AssertionRewriter(ast.NodeVisitor): res_expr = ast.Compare(left_res, [op], [next_res]) self.statements.append(ast.Assign([store_names[i]], res_expr)) left_res, left_expl = next_res, next_expl - # Use py.code._reprcompare if that's available. + # Use pytest.assertion.util._reprcompare if that's available. expl_call = self.helper("call_reprcompare", ast.Tuple(syms, ast.Load()), ast.Tuple(load_names, ast.Load()), diff --git a/_pytest/assertion/util.py b/_pytest/assertion/util.py index ad8ed070f..258fee8a8 100644 --- a/_pytest/assertion/util.py +++ b/_pytest/assertion/util.py @@ -2,15 +2,12 @@ import py try: - from collections.abc import Sequence + from collections import Sequence except ImportError: - try: - from collections import Sequence - except ImportError: - Sequence = list - + Sequence = list BuiltinAssertionError = py.builtin.builtins.AssertionError +u = py.builtin._totext # The _reprcompare attribute on the util module is used by the new assertion # interpretation code and assertion rewriter to detect this plugin was @@ -29,7 +26,18 @@ def format_explanation(explanation): for when one explanation needs to span multiple lines, e.g. when displaying diffs. """ - # simplify 'assert False where False = ...' + explanation = _collapse_false(explanation) + lines = _split_explanation(explanation) + result = _format_lines(lines) + return u('\n').join(result) + + +def _collapse_false(explanation): + """Collapse expansions of False + + So this strips out any "assert False\n{where False = ...\n}" + blocks. + """ where = 0 while True: start = where = explanation.find("False\n{False = ", where) @@ -51,28 +59,48 @@ def format_explanation(explanation): explanation = (explanation[:start] + explanation[start+15:end-1] + explanation[end+1:]) where -= 17 - raw_lines = (explanation or '').split('\n') - # escape newlines not followed by {, } and ~ + return explanation + + +def _split_explanation(explanation): + """Return a list of individual lines in the explanation + + This will return a list of lines split on '\n{', '\n}' and '\n~'. + Any other newlines will be escaped and appear in the line as the + literal '\n' characters. + """ + raw_lines = (explanation or u('')).split('\n') lines = [raw_lines[0]] for l in raw_lines[1:]: if l.startswith('{') or l.startswith('}') or l.startswith('~'): lines.append(l) else: lines[-1] += '\\n' + l + return lines + +def _format_lines(lines): + """Format the individual lines + + This will replace the '{', '}' and '~' characters of our mini + formatting language with the proper 'where ...', 'and ...' and ' + + ...' text, taking care of indentation along the way. + + Return a list of formatted lines. + """ result = lines[:1] stack = [0] stackcnt = [0] for line in lines[1:]: if line.startswith('{'): if stackcnt[-1]: - s = 'and ' + s = u('and ') else: - s = 'where ' + s = u('where ') stack.append(len(result)) stackcnt[-1] += 1 stackcnt.append(0) - result.append(' +' + ' '*(len(stack)-1) + s + line[1:]) + result.append(u(' +') + u(' ')*(len(stack)-1) + s + line[1:]) elif line.startswith('}'): assert line.startswith('}') stack.pop() @@ -80,9 +108,9 @@ def format_explanation(explanation): result[stack[-1]] += line[1:] else: assert line.startswith('~') - result.append(' '*len(stack) + line[1:]) + result.append(u(' ')*len(stack) + line[1:]) assert len(stack) == 1 - return '\n'.join(result) + return result # Provide basestring in python3 @@ -97,7 +125,7 @@ def assertrepr_compare(config, op, left, right): width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op left_repr = py.io.saferepr(left, maxsize=int(width/2)) right_repr = py.io.saferepr(right, maxsize=width-len(left_repr)) - summary = '%s %s %s' % (left_repr, op, right_repr) + summary = u('%s %s %s') % (left_repr, op, right_repr) issequence = lambda x: (isinstance(x, (list, tuple, Sequence)) and not isinstance(x, basestring)) @@ -120,13 +148,12 @@ def assertrepr_compare(config, op, left, right): elif op == 'not in': if istext(left) and istext(right): explanation = _notin_text(left, right, verbose) - except py.builtin._sysex: - raise - except: + except Exception: excinfo = py.code.ExceptionInfo() explanation = [ - '(pytest_assertion plugin: representation of details failed. ' - 'Probably an object has a faulty __repr__.)', str(excinfo)] + u('(pytest_assertion plugin: representation of details failed. ' + 'Probably an object has a faulty __repr__.)'), + u(excinfo)] if not explanation: return None @@ -148,8 +175,8 @@ def _diff_text(left, right, verbose=False): break if i > 42: i -= 10 # Provide some context - explanation = ['Skipping %s identical leading ' - 'characters in diff, use -v to show' % i] + explanation = [u('Skipping %s identical leading ' + 'characters in diff, use -v to show') % i] left = left[i:] right = right[i:] if len(left) == len(right): @@ -158,8 +185,8 @@ def _diff_text(left, right, verbose=False): break if i > 42: i -= 10 # Provide some context - explanation += ['Skipping %s identical trailing ' - 'characters in diff, use -v to show' % i] + explanation += [u('Skipping %s identical trailing ' + 'characters in diff, use -v to show') % i] left = left[:-i] right = right[:-i] explanation += [line.strip('\n') @@ -172,16 +199,15 @@ def _compare_eq_sequence(left, right, verbose=False): explanation = [] for i in range(min(len(left), len(right))): if left[i] != right[i]: - explanation += ['At index %s diff: %r != %r' % - (i, left[i], right[i])] + explanation += [u('At index %s diff: %r != %r') + % (i, left[i], right[i])] break if len(left) > len(right): - explanation += [ - 'Left contains more items, first extra item: %s' % - py.io.saferepr(left[len(right)],)] + explanation += [u('Left contains more items, first extra item: %s') + % py.io.saferepr(left[len(right)],)] elif len(left) < len(right): explanation += [ - 'Right contains more items, first extra item: %s' % + u('Right contains more items, first extra item: %s') % py.io.saferepr(right[len(left)],)] return explanation # + _diff_text(py.std.pprint.pformat(left), # py.std.pprint.pformat(right)) @@ -192,11 +218,11 @@ def _compare_eq_set(left, right, verbose=False): diff_left = left - right diff_right = right - left if diff_left: - explanation.append('Extra items in the left set:') + explanation.append(u('Extra items in the left set:')) for item in diff_left: explanation.append(py.io.saferepr(item)) if diff_right: - explanation.append('Extra items in the right set:') + explanation.append(u('Extra items in the right set:')) for item in diff_right: explanation.append(py.io.saferepr(item)) return explanation @@ -207,25 +233,25 @@ def _compare_eq_dict(left, right, verbose=False): common = set(left).intersection(set(right)) same = dict((k, left[k]) for k in common if left[k] == right[k]) if same and not verbose: - explanation += ['Omitting %s identical items, use -v to show' % + explanation += [u('Omitting %s identical items, use -v to show') % len(same)] elif same: - explanation += ['Common items:'] + explanation += [u('Common items:')] explanation += py.std.pprint.pformat(same).splitlines() diff = set(k for k in common if left[k] != right[k]) if diff: - explanation += ['Differing items:'] + explanation += [u('Differing items:')] for k in diff: explanation += [py.io.saferepr({k: left[k]}) + ' != ' + py.io.saferepr({k: right[k]})] extra_left = set(left) - set(right) if extra_left: - explanation.append('Left contains more items:') + explanation.append(u('Left contains more items:')) explanation.extend(py.std.pprint.pformat( dict((k, left[k]) for k in extra_left)).splitlines()) extra_right = set(right) - set(left) if extra_right: - explanation.append('Right contains more items:') + explanation.append(u('Right contains more items:')) explanation.extend(py.std.pprint.pformat( dict((k, right[k]) for k in extra_right)).splitlines()) return explanation @@ -237,14 +263,14 @@ def _notin_text(term, text, verbose=False): tail = text[index+len(term):] correct_text = head + tail diff = _diff_text(correct_text, text, verbose) - newdiff = ['%s is contained here:' % py.io.saferepr(term, maxsize=42)] + newdiff = [u('%s is contained here:') % py.io.saferepr(term, maxsize=42)] for line in diff: - if line.startswith('Skipping'): + if line.startswith(u('Skipping')): continue - if line.startswith('- '): + if line.startswith(u('- ')): continue - if line.startswith('+ '): - newdiff.append(' ' + line[2:]) + if line.startswith(u('+ ')): + newdiff.append(u(' ') + line[2:]) else: newdiff.append(line) return newdiff diff --git a/_pytest/capture.py b/_pytest/capture.py index 3c2874684..797b4970a 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -1,43 +1,114 @@ -""" per-test stdout/stderr capturing mechanisms, ``capsys`` and ``capfd`` function arguments. """ - -import pytest, py +""" + per-test stdout/stderr capturing mechanisms, + ``capsys`` and ``capfd`` function arguments. +""" +# note: py.io capture was where copied from +# pylib 1.4.20.dev2 (rev 13d9af95547e) +import sys import os +import tempfile + +import py +import pytest + +try: + from io import StringIO +except ImportError: + from StringIO import StringIO + +try: + from io import BytesIO +except ImportError: + class BytesIO(StringIO): + def write(self, data): + if isinstance(data, unicode): + raise TypeError("not a byte value: %r" % (data,)) + StringIO.write(self, data) + +if sys.version_info < (3, 0): + class TextIO(StringIO): + def write(self, data): + if not isinstance(data, unicode): + enc = getattr(self, '_encoding', 'UTF-8') + data = unicode(data, enc, 'replace') + StringIO.write(self, data) +else: + TextIO = StringIO + + +patchsysdict = {0: 'stdin', 1: 'stdout', 2: 'stderr'} + def pytest_addoption(parser): group = parser.getgroup("general") - group._addoption('--capture', action="store", default=None, + group._addoption( + '--capture', action="store", default=None, metavar="method", choices=['fd', 'sys', 'no'], help="per-test capturing method: one of fd (default)|sys|no.") - group._addoption('-s', action="store_const", const="no", dest="capture", + group._addoption( + '-s', action="store_const", const="no", dest="capture", help="shortcut for --capture=no.") + @pytest.mark.tryfirst -def pytest_cmdline_parse(pluginmanager, args): - # we want to perform capturing already for plugin/conftest loading - if '-s' in args or "--capture=no" in args: - method = "no" - elif hasattr(os, 'dup') and '--capture=sys' not in args: +def pytest_load_initial_conftests(early_config, parser, args, __multicall__): + ns = parser.parse_known_args(args) + method = ns.capture + if not method: method = "fd" - else: + if method == "fd" and not hasattr(os, "dup"): method = "sys" capman = CaptureManager(method) - pluginmanager.register(capman, "capturemanager") + early_config.pluginmanager.register(capman, "capturemanager") + + # make sure that capturemanager is properly reset at final shutdown + def teardown(): + try: + capman.reset_capturings() + except ValueError: + pass + + early_config.pluginmanager.add_shutdown(teardown) + + # make sure logging does not raise exceptions at the end + def silence_logging_at_shutdown(): + if "logging" in sys.modules: + sys.modules["logging"].raiseExceptions = False + early_config.pluginmanager.add_shutdown(silence_logging_at_shutdown) + + # finally trigger conftest loading but while capturing (issue93) + capman.resumecapture() + try: + try: + return __multicall__.execute() + finally: + out, err = capman.suspendcapture() + except: + sys.stdout.write(out) + sys.stderr.write(err) + raise + def addouterr(rep, outerr): for secname, content in zip(["out", "err"], outerr): if content: rep.sections.append(("Captured std%s" % secname, content)) + class NoCapture: def startall(self): pass + def resume(self): pass + def reset(self): pass + def suspend(self): return "", "" + class CaptureManager: def __init__(self, defaultmethod=None): self._method2capture = {} @@ -45,21 +116,25 @@ class CaptureManager: def _maketempfile(self): f = py.std.tempfile.TemporaryFile() - newf = py.io.dupfile(f, encoding="UTF-8") + newf = dupfile(f, encoding="UTF-8") f.close() return newf def _makestringio(self): - return py.io.TextIO() + return TextIO() def _getcapture(self, method): if method == "fd": - return py.io.StdCaptureFD(now=False, - out=self._maketempfile(), err=self._maketempfile() + return StdCaptureFD( + now=False, + out=self._maketempfile(), + err=self._maketempfile(), ) elif method == "sys": - return py.io.StdCapture(now=False, - out=self._makestringio(), err=self._makestringio() + return StdCapture( + now=False, + out=self._makestringio(), + err=self._makestringio(), ) elif method == "no": return NoCapture() @@ -74,23 +149,24 @@ class CaptureManager: method = config._conftest.rget("option_capture", path=fspath) except KeyError: method = "fd" - if method == "fd" and not hasattr(os, 'dup'): # e.g. jython + if method == "fd" and not hasattr(os, 'dup'): # e.g. jython method = "sys" return method def reset_capturings(self): - for name, cap in self._method2capture.items(): + for cap in self._method2capture.values(): cap.reset() def resumecapture_item(self, item): method = self._getmethod(item.config, item.fspath) if not hasattr(item, 'outerr'): - item.outerr = ('', '') # we accumulate outerr on the item + item.outerr = ('', '') # we accumulate outerr on the item return self.resumecapture(method) def resumecapture(self, method=None): if hasattr(self, '_capturing'): - raise ValueError("cannot resume, already capturing with %r" % + raise ValueError( + "cannot resume, already capturing with %r" % (self._capturing,)) if method is None: method = self._defaultmethod @@ -139,8 +215,9 @@ class CaptureManager: try: self.resumecapture(method) except ValueError: - return # recursive collect, XXX refactor capturing - # to allow for more lightweight recursive capturing + # recursive collect, XXX refactor capturing + # to allow for more lightweight recursive capturing + return try: rep = __multicall__.execute() finally: @@ -181,6 +258,7 @@ class CaptureManager: error_capsysfderror = "cannot use capsys and capfd at the same time" + def pytest_funcarg__capsys(request): """enables capturing of writes to sys.stdout/sys.stderr and makes captured output available via ``capsys.readouterr()`` method calls @@ -188,7 +266,8 @@ def pytest_funcarg__capsys(request): """ if "capfd" in request._funcargs: raise request.raiseerror(error_capsysfderror) - return CaptureFixture(py.io.StdCapture) + return CaptureFixture(StdCapture) + def pytest_funcarg__capfd(request): """enables capturing of writes to file descriptors 1 and 2 and makes @@ -199,26 +278,366 @@ def pytest_funcarg__capfd(request): request.raiseerror(error_capsysfderror) if not hasattr(os, 'dup'): pytest.skip("capfd funcarg needs os.dup") - return CaptureFixture(py.io.StdCaptureFD) + return CaptureFixture(StdCaptureFD) + class CaptureFixture: def __init__(self, captureclass): - self.capture = captureclass(now=False) + self._capture = captureclass(now=False) def _start(self): - self.capture.startall() + self._capture.startall() def _finalize(self): - if hasattr(self, 'capture'): - outerr = self._outerr = self.capture.reset() - del self.capture + if hasattr(self, '_capture'): + outerr = self._outerr = self._capture.reset() + del self._capture return outerr def readouterr(self): try: - return self.capture.readouterr() + return self._capture.readouterr() except AttributeError: return self._outerr def close(self): self._finalize() + + +class FDCapture: + """ Capture IO to/from a given os-level filedescriptor. """ + + def __init__(self, targetfd, tmpfile=None, now=True, patchsys=False): + """ save targetfd descriptor, and open a new + temporary file there. If no tmpfile is + specified a tempfile.Tempfile() will be opened + in text mode. + """ + self.targetfd = targetfd + if tmpfile is None and targetfd != 0: + f = tempfile.TemporaryFile('wb+') + tmpfile = dupfile(f, encoding="UTF-8") + f.close() + self.tmpfile = tmpfile + self._savefd = os.dup(self.targetfd) + if patchsys: + self._oldsys = getattr(sys, patchsysdict[targetfd]) + if now: + self.start() + + def start(self): + try: + os.fstat(self._savefd) + except OSError: + raise ValueError( + "saved filedescriptor not valid, " + "did you call start() twice?") + if self.targetfd == 0 and not self.tmpfile: + fd = os.open(os.devnull, os.O_RDONLY) + os.dup2(fd, 0) + os.close(fd) + if hasattr(self, '_oldsys'): + setattr(sys, patchsysdict[self.targetfd], DontReadFromInput()) + else: + os.dup2(self.tmpfile.fileno(), self.targetfd) + if hasattr(self, '_oldsys'): + setattr(sys, patchsysdict[self.targetfd], self.tmpfile) + + def done(self): + """ unpatch and clean up, returns the self.tmpfile (file object) + """ + os.dup2(self._savefd, self.targetfd) + os.close(self._savefd) + if self.targetfd != 0: + self.tmpfile.seek(0) + if hasattr(self, '_oldsys'): + setattr(sys, patchsysdict[self.targetfd], self._oldsys) + return self.tmpfile + + def writeorg(self, data): + """ write a string to the original file descriptor + """ + tempfp = tempfile.TemporaryFile() + try: + os.dup2(self._savefd, tempfp.fileno()) + tempfp.write(data) + finally: + tempfp.close() + + +def dupfile(f, mode=None, buffering=0, raising=False, encoding=None): + """ return a new open file object that's a duplicate of f + + mode is duplicated if not given, 'buffering' controls + buffer size (defaulting to no buffering) and 'raising' + defines whether an exception is raised when an incompatible + file object is passed in (if raising is False, the file + object itself will be returned) + """ + try: + fd = f.fileno() + mode = mode or f.mode + except AttributeError: + if raising: + raise + return f + newfd = os.dup(fd) + if sys.version_info >= (3, 0): + if encoding is not None: + mode = mode.replace("b", "") + buffering = True + return os.fdopen(newfd, mode, buffering, encoding, closefd=True) + else: + f = os.fdopen(newfd, mode, buffering) + if encoding is not None: + return EncodedFile(f, encoding) + return f + + +class EncodedFile(object): + def __init__(self, _stream, encoding): + self._stream = _stream + self.encoding = encoding + + def write(self, obj): + if isinstance(obj, unicode): + obj = obj.encode(self.encoding) + self._stream.write(obj) + + def writelines(self, linelist): + data = ''.join(linelist) + self.write(data) + + def __getattr__(self, name): + return getattr(self._stream, name) + + +class Capture(object): + def call(cls, func, *args, **kwargs): + """ return a (res, out, err) tuple where + out and err represent the output/error output + during function execution. + call the given function with args/kwargs + and capture output/error during its execution. + """ + so = cls() + try: + res = func(*args, **kwargs) + finally: + out, err = so.reset() + return res, out, err + call = classmethod(call) + + def reset(self): + """ reset sys.stdout/stderr and return captured output as strings. """ + if hasattr(self, '_reset'): + raise ValueError("was already reset") + self._reset = True + outfile, errfile = self.done(save=False) + out, err = "", "" + if outfile and not outfile.closed: + out = outfile.read() + outfile.close() + if errfile and errfile != outfile and not errfile.closed: + err = errfile.read() + errfile.close() + return out, err + + def suspend(self): + """ return current snapshot captures, memorize tempfiles. """ + outerr = self.readouterr() + outfile, errfile = self.done() + return outerr + + +class StdCaptureFD(Capture): + """ This class allows to capture writes to FD1 and FD2 + and may connect a NULL file to FD0 (and prevent + reads from sys.stdin). If any of the 0,1,2 file descriptors + is invalid it will not be captured. + """ + def __init__(self, out=True, err=True, mixed=False, + in_=True, patchsys=True, now=True): + self._options = { + "out": out, + "err": err, + "mixed": mixed, + "in_": in_, + "patchsys": patchsys, + "now": now, + } + self._save() + if now: + self.startall() + + def _save(self): + in_ = self._options['in_'] + out = self._options['out'] + err = self._options['err'] + mixed = self._options['mixed'] + patchsys = self._options['patchsys'] + if in_: + try: + self.in_ = FDCapture( + 0, tmpfile=None, now=False, + patchsys=patchsys) + except OSError: + pass + if out: + tmpfile = None + if hasattr(out, 'write'): + tmpfile = out + try: + self.out = FDCapture( + 1, tmpfile=tmpfile, + now=False, patchsys=patchsys) + self._options['out'] = self.out.tmpfile + except OSError: + pass + if err: + if out and mixed: + tmpfile = self.out.tmpfile + elif hasattr(err, 'write'): + tmpfile = err + else: + tmpfile = None + try: + self.err = FDCapture( + 2, tmpfile=tmpfile, + now=False, patchsys=patchsys) + self._options['err'] = self.err.tmpfile + except OSError: + pass + + def startall(self): + if hasattr(self, 'in_'): + self.in_.start() + if hasattr(self, 'out'): + self.out.start() + if hasattr(self, 'err'): + self.err.start() + + def resume(self): + """ resume capturing with original temp files. """ + self.startall() + + def done(self, save=True): + """ return (outfile, errfile) and stop capturing. """ + outfile = errfile = None + if hasattr(self, 'out') and not self.out.tmpfile.closed: + outfile = self.out.done() + if hasattr(self, 'err') and not self.err.tmpfile.closed: + errfile = self.err.done() + if hasattr(self, 'in_'): + self.in_.done() + if save: + self._save() + return outfile, errfile + + def readouterr(self): + """ return snapshot value of stdout/stderr capturings. """ + out = self._readsnapshot('out') + err = self._readsnapshot('err') + return out, err + + def _readsnapshot(self, name): + if hasattr(self, name): + f = getattr(self, name).tmpfile + else: + return '' + + f.seek(0) + res = f.read() + enc = getattr(f, "encoding", None) + if enc: + res = py.builtin._totext(res, enc, "replace") + f.truncate(0) + f.seek(0) + return res + + +class StdCapture(Capture): + """ This class allows to capture writes to sys.stdout|stderr "in-memory" + and will raise errors on tries to read from sys.stdin. It only + modifies sys.stdout|stderr|stdin attributes and does not + touch underlying File Descriptors (use StdCaptureFD for that). + """ + def __init__(self, out=True, err=True, in_=True, mixed=False, now=True): + self._oldout = sys.stdout + self._olderr = sys.stderr + self._oldin = sys.stdin + if out and not hasattr(out, 'file'): + out = TextIO() + self.out = out + if err: + if mixed: + err = out + elif not hasattr(err, 'write'): + err = TextIO() + self.err = err + self.in_ = in_ + if now: + self.startall() + + def startall(self): + if self.out: + sys.stdout = self.out + if self.err: + sys.stderr = self.err + if self.in_: + sys.stdin = self.in_ = DontReadFromInput() + + def done(self, save=True): + """ return (outfile, errfile) and stop capturing. """ + outfile = errfile = None + if self.out and not self.out.closed: + sys.stdout = self._oldout + outfile = self.out + outfile.seek(0) + if self.err and not self.err.closed: + sys.stderr = self._olderr + errfile = self.err + errfile.seek(0) + if self.in_: + sys.stdin = self._oldin + return outfile, errfile + + def resume(self): + """ resume capturing with original temp files. """ + self.startall() + + def readouterr(self): + """ return snapshot value of stdout/stderr capturings. """ + out = err = "" + if self.out: + out = self.out.getvalue() + self.out.truncate(0) + self.out.seek(0) + if self.err: + err = self.err.getvalue() + self.err.truncate(0) + self.err.seek(0) + return out, err + + +class DontReadFromInput: + """Temporary stub class. Ideally when stdin is accessed, the + capturing should be turned off, with possibly all data captured + so far sent to the screen. This should be configurable, though, + because in automated test runs it is better to crash than + hang indefinitely. + """ + def read(self, *args): + raise IOError("reading from stdin while output is captured") + readline = read + readlines = read + __iter__ = read + + def fileno(self): + raise ValueError("redirected Stdin is pseudofile, has no fileno()") + + def isatty(self): + return False + + def close(self): + pass diff --git a/_pytest/config.py b/_pytest/config.py index ed58fc257..338b40302 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -1,29 +1,88 @@ """ command line options, ini-file and conftest.py processing. """ import py +# DON't import pytest here because it causes import cycle troubles import sys, os +from _pytest import hookspec # the extension point definitions from _pytest.core import PluginManager -import pytest -from _pytest._argcomplete import try_argcomplete, filescompleter -# enable after some grace period for plugin writers -TYPE_WARN = False -if TYPE_WARN: - import warnings +# pytest startup +def main(args=None, plugins=None): + """ return exit code, after performing an in-process test run. -def pytest_cmdline_parse(pluginmanager, args): - config = Config(pluginmanager) - config.parse(args) - return config + :arg args: list of command line arguments. + + :arg plugins: list of plugin objects to be auto-registered during + initialization. + """ + config = _prepareconfig(args, plugins) + return config.hook.pytest_cmdline_main(config=config) + +class cmdline: # compatibility namespace + main = staticmethod(main) + +class UsageError(Exception): + """ error in pytest usage or invocation""" + +_preinit = [] + +default_plugins = ( + "mark main terminal runner python pdb unittest capture skipping " + "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " + "junitxml resultlog doctest").split() + +def _preloadplugins(): + assert not _preinit + _preinit.append(get_plugin_manager()) + +def get_plugin_manager(): + if _preinit: + return _preinit.pop(0) + # subsequent calls to main will create a fresh instance + pluginmanager = PytestPluginManager() + pluginmanager.config = Config(pluginmanager) # XXX attr needed? + for spec in default_plugins: + pluginmanager.import_plugin(spec) + return pluginmanager + +def _prepareconfig(args=None, plugins=None): + if args is None: + args = sys.argv[1:] + elif isinstance(args, py.path.local): + args = [str(args)] + elif not isinstance(args, (tuple, list)): + if not isinstance(args, str): + raise ValueError("not a string or argument list: %r" % (args,)) + args = py.std.shlex.split(args) + pluginmanager = get_plugin_manager() + if plugins: + for plugin in plugins: + pluginmanager.register(plugin) + return pluginmanager.hook.pytest_cmdline_parse( + pluginmanager=pluginmanager, args=args) + +class PytestPluginManager(PluginManager): + def __init__(self, hookspecs=[hookspec]): + super(PytestPluginManager, self).__init__(hookspecs=hookspecs) + self.register(self) + if os.environ.get('PYTEST_DEBUG'): + err = sys.stderr + encoding = getattr(err, 'encoding', 'utf8') + try: + err = py.io.dupfile(err, encoding=encoding) + except Exception: + pass + self.trace.root.setwriter(err.write) + + def pytest_configure(self, config): + config.addinivalue_line("markers", + "tryfirst: mark a hook implementation function such that the " + "plugin machinery will try to call it first/as early as possible.") + config.addinivalue_line("markers", + "trylast: mark a hook implementation function such that the " + "plugin machinery will try to call it last/as late as possible.") -def pytest_unconfigure(config): - while 1: - try: - fin = config._cleanup.pop() - except IndexError: - break - fin() class Parser: """ Parser for command line arguments and ini-file values. """ @@ -70,8 +129,8 @@ class Parser: :opts: option names, can be short or long options. :attrs: same attributes which the ``add_option()`` function of the - `optparse library - `_ + `argparse library + `_ accepts. After command line parsing options are available on the pytest config @@ -82,7 +141,14 @@ class Parser: self._anonymous.addoption(*opts, **attrs) def parse(self, args): - self.optparser = optparser = MyOptionParser(self) + from _pytest._argcomplete import try_argcomplete + self.optparser = self._getparser() + try_argcomplete(self.optparser) + return self.optparser.parse_args([str(x) for x in args]) + + def _getparser(self): + from _pytest._argcomplete import filescompleter + optparser = MyOptionParser(self) groups = self._groups + [self._anonymous] for group in groups: if group.options: @@ -93,16 +159,20 @@ class Parser: a = option.attrs() arggroup.add_argument(*n, **a) # bash like autocompletion for dirs (appending '/') - optparser.add_argument(Config._file_or_dir, nargs='*' + optparser.add_argument(FILE_OR_DIR, nargs='*' ).completer=filescompleter - try_argcomplete(self.optparser) - return self.optparser.parse_args([str(x) for x in args]) + return optparser def parse_setoption(self, args, option): parsedoption = self.parse(args) for name, value in parsedoption.__dict__.items(): setattr(option, name, value) - return getattr(parsedoption, Config._file_or_dir) + return getattr(parsedoption, FILE_OR_DIR) + + def parse_known_args(self, args): + optparser = self._getparser() + args = [str(x) for x in args] + return optparser.parse_known_args(args)[0] def addini(self, name, help, type=None, default=None): """ register an ini-file option. @@ -142,6 +212,8 @@ class Argument: 'int': int, 'string': str, } + # enable after some grace period for plugin writers + TYPE_WARN = False def __init__(self, *names, **attrs): """store parms in private vars for use in add_argument""" @@ -149,12 +221,12 @@ class Argument: self._short_opts = [] self._long_opts = [] self.dest = attrs.get('dest') - if TYPE_WARN: + if self.TYPE_WARN: try: help = attrs['help'] if '%default' in help: - warnings.warn( - 'py.test now uses argparse. "%default" should be' + py.std.warnings.warn( + 'pytest now uses argparse. "%default" should be' ' changed to "%(default)s" ', FutureWarning, stacklevel=3) @@ -166,10 +238,10 @@ class Argument: pass else: # this might raise a keyerror as well, don't want to catch that - if isinstance(typ, str): + if isinstance(typ, py.builtin._basestring): if typ == 'choice': - if TYPE_WARN: - warnings.warn( + if self.TYPE_WARN: + py.std.warnings.warn( 'type argument to addoption() is a string %r.' ' For parsearg this is optional and when supplied ' ' should be a type.' @@ -180,8 +252,8 @@ class Argument: # the type of the first element attrs['type'] = type(attrs['choices'][0]) else: - if TYPE_WARN: - warnings.warn( + if self.TYPE_WARN: + py.std.warnings.warn( 'type argument to addoption() is a string %r.' ' For parsearg this should be a type.' ' (options: %s)' % (typ, names), @@ -323,22 +395,9 @@ class MyOptionParser(py.std.argparse.ArgumentParser): if arg and arg[0] == '-': msg = py.std.argparse._('unrecognized arguments: %s') self.error(msg % ' '.join(argv)) - getattr(args, Config._file_or_dir).extend(argv) + getattr(args, FILE_OR_DIR).extend(argv) return args -# #pylib 2013-07-31 -# (12:05:53) anthon: hynek: can you get me a list of preferred py.test -# long-options with '-' inserted at the right places? -# (12:08:29) hynek: anthon, hpk: generally I'd love the following, decide -# yourself which you agree and which not: -# (12:10:51) hynek: --exit-on-first --full-trace --junit-xml --junit-prefix -# --result-log --collect-only --conf-cut-dir --trace-config -# --no-magic -# (12:18:21) hpk: hynek,anthon: makes sense to me. -# (13:40:30) hpk: hynek: let's not change names, rather only deal with -# hyphens for now -# (13:40:50) hynek: then --exit-first *shrug* - class DropShorterLongHelpFormatter(py.std.argparse.HelpFormatter): """shorten help for long options that differ only in extra hyphens @@ -390,7 +449,7 @@ class DropShorterLongHelpFormatter(py.std.argparse.HelpFormatter): class Conftest(object): """ the single place for accessing values and interacting - towards conftest modules from py.test objects. + towards conftest modules from pytest objects. """ def __init__(self, onimport=None, confcutdir=None): self._path2confmods = {} @@ -504,35 +563,88 @@ class CmdOptions(object): def __repr__(self): return "" %(self.__dict__,) +FILE_OR_DIR = 'file_or_dir' class Config(object): """ access to configuration values, pluginmanager and plugin hooks. """ - _file_or_dir = 'file_or_dir' - def __init__(self, pluginmanager=None): + def __init__(self, pluginmanager): #: access to command line option as attributes. #: (deprecated), use :py:func:`getoption() <_pytest.config.Config.getoption>` instead self.option = CmdOptions() - _a = self._file_or_dir + _a = FILE_OR_DIR self._parser = Parser( usage="%%(prog)s [options] [%s] [%s] [...]" % (_a, _a), processopt=self._processopt, ) #: a pluginmanager instance - self.pluginmanager = pluginmanager or PluginManager(load=True) + self.pluginmanager = pluginmanager self.trace = self.pluginmanager.trace.root.get("config") self._conftest = Conftest(onimport=self._onimportconftest) self.hook = self.pluginmanager.hook self._inicache = {} self._opt2dest = {} self._cleanup = [] + self.pluginmanager.register(self, "pytestconfig") + self.pluginmanager.set_register_callback(self._register_plugin) + self._configured = False + + def _register_plugin(self, plugin, name): + call_plugin = self.pluginmanager.call_plugin + call_plugin(plugin, "pytest_addhooks", + {'pluginmanager': self.pluginmanager}) + self.hook.pytest_plugin_registered(plugin=plugin, + manager=self.pluginmanager) + dic = call_plugin(plugin, "pytest_namespace", {}) or {} + if dic: + import pytest + setns(pytest, dic) + call_plugin(plugin, "pytest_addoption", {'parser': self._parser}) + if self._configured: + call_plugin(plugin, "pytest_configure", {'config': self}) + + def do_configure(self): + assert not self._configured + self._configured = True + self.hook.pytest_configure(config=self) + + def do_unconfigure(self): + assert self._configured + self._configured = False + self.hook.pytest_unconfigure(config=self) + self.pluginmanager.ensure_shutdown() + + def pytest_cmdline_parse(self, pluginmanager, args): + assert self == pluginmanager.config, (self, pluginmanager.config) + self.parse(args) + return self + + def pytest_unconfigure(config): + while config._cleanup: + fin = config._cleanup.pop() + fin() + + def notify_exception(self, excinfo, option=None): + if option and option.fulltrace: + style = "long" + else: + style = "native" + excrepr = excinfo.getrepr(funcargs=True, + showlocals=getattr(option, 'showlocals', False), + style=style, + ) + res = self.hook.pytest_internalerror(excrepr=excrepr, + excinfo=excinfo) + if not py.builtin.any(res): + for line in str(excrepr).split("\n"): + sys.stderr.write("INTERNALERROR> %s\n" %line) + sys.stderr.flush() + @classmethod def fromdictargs(cls, option_dict, args): """ constructor useable for subprocesses. """ - config = cls() - # XXX slightly crude way to initialize capturing - import _pytest.capture - _pytest.capture.pytest_cmdline_parse(config.pluginmanager, args) + pluginmanager = get_plugin_manager() + config = pluginmanager.config config._preparse(args, addopts=False) config.option.__dict__.update(option_dict) for x in config.option.plugins: @@ -558,21 +670,9 @@ class Config(object): plugins += self._conftest.getconftestmodules(fspath) return plugins - def _setinitialconftest(self, args): - # capture output during conftest init (#issue93) - # XXX introduce load_conftest hook to avoid needing to know - # about capturing plugin here - capman = self.pluginmanager.getplugin("capturemanager") - capman.resumecapture() - try: - try: - self._conftest.setinitial(args) - finally: - out, err = capman.suspendcapture() # logging might have got it - except: - sys.stdout.write(out) - sys.stderr.write(err) - raise + def pytest_load_initial_conftests(self, parser, args): + self._conftest.setinitial(args) + pytest_load_initial_conftests.trylast = True def _initini(self, args): self.inicfg = getcfg(args, ["pytest.ini", "tox.ini", "setup.cfg"]) @@ -587,12 +687,11 @@ class Config(object): self.pluginmanager.consider_preparse(args) self.pluginmanager.consider_setuptools_entrypoints() self.pluginmanager.consider_env() - self._setinitialconftest(args) - self.pluginmanager.do_addoption(self._parser) - if addopts: - self.hook.pytest_cmdline_preparse(config=self, args=args) + self.hook.pytest_load_initial_conftests(early_config=self, + args=args, parser=self._parser) def _checkversion(self): + import pytest minver = self.inicfg.get('minversion', None) if minver: ver = minver.split(".") @@ -610,6 +709,8 @@ class Config(object): "can only parse cmdline args at most once per Config object") self._origargs = args self._preparse(args) + # XXX deprecated hook: + self.hook.pytest_cmdline_preparse(config=self, args=args) self._parser.hints.extend(self.pluginmanager._hints) args = self._parser.parse_setoption(args, self.option) if not args: @@ -708,7 +809,7 @@ class Config(object): def getvalueorskip(self, name, path=None): """ (deprecated) return getvalue(name) or call - py.test.skip if no value exists. """ + pytest.skip if no value exists. """ __tracebackhide__ = True try: val = self.getvalue(name, path) @@ -739,3 +840,23 @@ def getcfg(args, inibasenames): return iniconfig['pytest'] return {} + +def setns(obj, dic): + import pytest + for name, value in dic.items(): + if isinstance(value, dict): + mod = getattr(obj, name, None) + if mod is None: + modname = "pytest.%s" % name + mod = py.std.types.ModuleType(modname) + sys.modules[modname] = mod + mod.__all__ = [] + setattr(obj, name, mod) + obj.__all__.append(name) + setns(mod, value) + else: + setattr(obj, name, value) + obj.__all__.append(name) + #if obj != pytest: + # pytest.__all__.append(name) + setattr(pytest, name, value) diff --git a/_pytest/core.py b/_pytest/core.py index ceafe721c..d029e7223 100644 --- a/_pytest/core.py +++ b/_pytest/core.py @@ -1,20 +1,14 @@ """ pytest PluginManager, basic initialization and tracing. -(c) Holger Krekel 2004-2010 """ -import sys, os +import sys import inspect import py -from _pytest import hookspec # the extension point definitions +# don't import pytest to avoid circular imports assert py.__version__.split(".")[:2] >= ['1', '4'], ("installation problem: " "%s is too old, remove or upgrade 'py'" % (py.__version__)) -default_plugins = ( - "config mark main terminal runner python pdb unittest capture skipping " - "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " - "junitxml resultlog doctest").split() - class TagTracer: def __init__(self): self._tag2proc = {} @@ -73,37 +67,36 @@ class TagTracerSub: return self.__class__(self.root, self.tags + (name,)) class PluginManager(object): - def __init__(self, load=False): + def __init__(self, hookspecs=None): self._name2plugin = {} self._listattrcache = {} self._plugins = [] self._hints = [] self.trace = TagTracer().get("pluginmanage") self._plugin_distinfo = [] - if os.environ.get('PYTEST_DEBUG'): - err = sys.stderr - encoding = getattr(err, 'encoding', 'utf8') - try: - err = py.io.dupfile(err, encoding=encoding) - except Exception: - pass - self.trace.root.setwriter(err.write) - self.hook = HookRelay([hookspec], pm=self) - self.register(self) - if load: - for spec in default_plugins: - self.import_plugin(spec) + self._shutdown = [] + self.hook = HookRelay(hookspecs or [], pm=self) + + def do_configure(self, config): + # backward compatibility + config.do_configure() + + def set_register_callback(self, callback): + assert not hasattr(self, "_registercallback") + self._registercallback = callback def register(self, plugin, name=None, prepend=False): if self._name2plugin.get(name, None) == -1: return name = name or getattr(plugin, '__name__', str(id(plugin))) if self.isregistered(plugin, name): - raise ValueError("Plugin already registered: %s=%s" %(name, plugin)) + raise ValueError("Plugin already registered: %s=%s\n%s" %( + name, plugin, self._name2plugin)) #self.trace("registering", name, plugin) self._name2plugin[name] = plugin - self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) - self.hook.pytest_plugin_registered(manager=self, plugin=plugin) + reg = getattr(self, "_registercallback", None) + if reg is not None: + reg(plugin, name) if not prepend: self._plugins.append(plugin) else: @@ -114,11 +107,21 @@ class PluginManager(object): if plugin is None: plugin = self.getplugin(name=name) self._plugins.remove(plugin) - self.hook.pytest_plugin_unregistered(plugin=plugin) for name, value in list(self._name2plugin.items()): if value == plugin: del self._name2plugin[name] + def add_shutdown(self, func): + self._shutdown.append(func) + + def ensure_shutdown(self): + while self._shutdown: + func = self._shutdown.pop() + func() + self._plugins = [] + self._name2plugin.clear() + self._listattrcache.clear() + def isregistered(self, plugin, name=None): if self.getplugin(name) is not None: return True @@ -126,8 +129,8 @@ class PluginManager(object): if plugin == val: return True - def addhooks(self, spec): - self.hook._addhooks(spec, prefix="pytest_") + def addhooks(self, spec, prefix="pytest_"): + self.hook._addhooks(spec, prefix=prefix) def getplugins(self): return list(self._plugins) @@ -209,7 +212,6 @@ class PluginManager(object): if self.getplugin(modname) is not None: return try: - #self.trace("importing", modname) mod = importplugin(modname) except KeyboardInterrupt: raise @@ -228,83 +230,6 @@ class PluginManager(object): self.register(mod, modname) self.consider_module(mod) - def pytest_configure(self, config): - config.addinivalue_line("markers", - "tryfirst: mark a hook implementation function such that the " - "plugin machinery will try to call it first/as early as possible.") - config.addinivalue_line("markers", - "trylast: mark a hook implementation function such that the " - "plugin machinery will try to call it last/as late as possible.") - - def pytest_plugin_registered(self, plugin): - import pytest - dic = self.call_plugin(plugin, "pytest_namespace", {}) or {} - if dic: - self._setns(pytest, dic) - if hasattr(self, '_config'): - self.call_plugin(plugin, "pytest_addoption", - {'parser': self._config._parser}) - self.call_plugin(plugin, "pytest_configure", - {'config': self._config}) - - def _setns(self, obj, dic): - import pytest - for name, value in dic.items(): - if isinstance(value, dict): - mod = getattr(obj, name, None) - if mod is None: - modname = "pytest.%s" % name - mod = py.std.types.ModuleType(modname) - sys.modules[modname] = mod - mod.__all__ = [] - setattr(obj, name, mod) - obj.__all__.append(name) - self._setns(mod, value) - else: - setattr(obj, name, value) - obj.__all__.append(name) - #if obj != pytest: - # pytest.__all__.append(name) - setattr(pytest, name, value) - - def pytest_terminal_summary(self, terminalreporter): - tw = terminalreporter._tw - if terminalreporter.config.option.traceconfig: - for hint in self._hints: - tw.line("hint: %s" % hint) - - def do_addoption(self, parser): - mname = "pytest_addoption" - methods = reversed(self.listattr(mname)) - MultiCall(methods, {'parser': parser}).execute() - - def do_configure(self, config): - assert not hasattr(self, '_config') - self._config = config - config.hook.pytest_configure(config=self._config) - - def do_unconfigure(self, config): - config = self._config - del self._config - config.hook.pytest_unconfigure(config=config) - config.pluginmanager.unregister(self) - - def notify_exception(self, excinfo, option=None): - if option and option.fulltrace: - style = "long" - else: - style = "native" - excrepr = excinfo.getrepr(funcargs=True, - showlocals=getattr(option, 'showlocals', False), - style=style, - ) - res = self.hook.pytest_internalerror(excrepr=excrepr, - excinfo=excinfo) - if not py.builtin.any(res): - for line in str(excrepr).split("\n"): - sys.stderr.write("INTERNALERROR> %s\n" %line) - sys.stderr.flush() - def listattr(self, attrname, plugins=None): if plugins is None: plugins = self._plugins @@ -342,15 +267,8 @@ def importplugin(importspec): __import__(mod) return sys.modules[mod] except ImportError: - #e = py.std.sys.exc_info()[1] - #if str(e).find(name) == -1: - # raise - pass # - try: __import__(importspec) - except ImportError: - raise ImportError(importspec) - return sys.modules[importspec] + return sys.modules[importspec] class MultiCall: """ execute a call into multiple python functions/methods. """ @@ -387,19 +305,36 @@ class MultiCall: return kwargs def varnames(func): + """ return argument name tuple for a function, method, class or callable. + + In case of a class, its "__init__" method is considered. + For methods the "self" parameter is not included unless you are passing + an unbound method with Python3 (which has no supports for unbound methods) + """ + cache = getattr(func, "__dict__", {}) try: - return func._varnames - except AttributeError: + return cache["_varnames"] + except KeyError: pass - if not inspect.isfunction(func) and not inspect.ismethod(func): - func = getattr(func, '__call__', func) - ismethod = inspect.ismethod(func) + if inspect.isclass(func): + try: + func = func.__init__ + except AttributeError: + return () + ismethod = True + else: + if not inspect.isfunction(func) and not inspect.ismethod(func): + func = getattr(func, '__call__', func) + ismethod = inspect.ismethod(func) rawcode = py.code.getrawcode(func) try: x = rawcode.co_varnames[ismethod:rawcode.co_argcount] except AttributeError: x = () - py.builtin._getfuncdict(func)['_varnames'] = x + try: + cache["_varnames"] = x + except TypeError: + pass return x class HookRelay: @@ -457,43 +392,3 @@ class HookCaller: self.trace.root.indent -= 1 return res -_preinit = [] - -def _preloadplugins(): - _preinit.append(PluginManager(load=True)) - -def _prepareconfig(args=None, plugins=None): - if args is None: - args = sys.argv[1:] - elif isinstance(args, py.path.local): - args = [str(args)] - elif not isinstance(args, (tuple, list)): - if not isinstance(args, str): - raise ValueError("not a string or argument list: %r" % (args,)) - args = py.std.shlex.split(args) - if _preinit: - _pluginmanager = _preinit.pop(0) - else: # subsequent calls to main will create a fresh instance - _pluginmanager = PluginManager(load=True) - hook = _pluginmanager.hook - if plugins: - for plugin in plugins: - _pluginmanager.register(plugin) - return hook.pytest_cmdline_parse( - pluginmanager=_pluginmanager, args=args) - -def main(args=None, plugins=None): - """ return exit code, after performing an in-process test run. - - :arg args: list of command line arguments. - - :arg plugins: list of plugin objects to be auto-registered during - initialization. - """ - config = _prepareconfig(args, plugins) - exitstatus = config.hook.pytest_cmdline_main(config=config) - return exitstatus - -class UsageError(Exception): - """ error in py.test usage or invocation""" - diff --git a/_pytest/doctest.py b/_pytest/doctest.py index 29ddf33ad..82bbc4b49 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -91,8 +91,13 @@ class DoctestTextfile(DoctestItem, pytest.File): doctest = py.std.doctest # satisfy `FixtureRequest` constructor... self.funcargs = {} - self._fixtureinfo = FuncFixtureInfo((), [], {}) + fm = self.session._fixturemanager + def func(): + pass + self._fixtureinfo = fm.getfixtureinfo(node=self, func=func, + cls=None, funcargs=False) fixture_request = FixtureRequest(self) + fixture_request._fillfixtures() failed, tot = doctest.testfile( str(self.fspath), module_relative=False, optionflags=doctest.ELLIPSIS, diff --git a/_pytest/genscript.py b/_pytest/genscript.py index 12c0ec505..25e8e6a0f 100755 --- a/_pytest/genscript.py +++ b/_pytest/genscript.py @@ -1,4 +1,4 @@ -""" generate a single-file self-contained version of py.test """ +""" generate a single-file self-contained version of pytest """ import py import sys @@ -55,7 +55,7 @@ def pytest_addoption(parser): group = parser.getgroup("debugconfig") group.addoption("--genscript", action="store", default=None, dest="genscript", metavar="path", - help="create standalone py.test script at given target path.") + help="create standalone pytest script at given target path.") def pytest_cmdline_main(config): genscript = config.getvalue("genscript") @@ -70,7 +70,7 @@ def pytest_cmdline_main(config): "or below due to 'argparse' dependency. Use python2.6 " "to generate a python2.5/6 compatible script", red=True) script = generate_script( - 'import py; raise SystemExit(py.test.cmdline.main())', + 'import pytest; raise SystemExit(pytest.cmdline.main())', deps, ) genscript = py.path.local(genscript) diff --git a/_pytest/helpconfig.py b/_pytest/helpconfig.py index 048863869..1fef7eb3f 100644 --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -12,7 +12,9 @@ def pytest_addoption(parser): help="show help message and configuration info") group._addoption('-p', action="append", dest="plugins", default = [], metavar="name", - help="early-load given plugin (multi-allowed).") + help="early-load given plugin (multi-allowed). " + "To avoid loading of plugins, use the `no:` prefix, e.g. " + "`no:doctest`.") group.addoption('--traceconfig', '--trace-config', action="store_true", default=False, help="trace considerations of conftest.py files."), @@ -46,7 +48,7 @@ def pytest_unconfigure(config): def pytest_cmdline_main(config): if config.option.version: p = py.path.local(pytest.__file__) - sys.stderr.write("This is py.test version %s, imported from %s\n" % + sys.stderr.write("This is pytest version %s, imported from %s\n" % (pytest.__version__, p)) plugininfo = getpluginversioninfo(config) if plugininfo: @@ -54,9 +56,9 @@ def pytest_cmdline_main(config): sys.stderr.write(line + "\n") return 0 elif config.option.help: - config.pluginmanager.do_configure(config) + config.do_configure() showhelp(config) - config.pluginmanager.do_unconfigure(config) + config.do_unconfigure() return 0 def showhelp(config): @@ -82,6 +84,8 @@ def showhelp(config): #tw.sep("=") tw.line("to see available markers type: py.test --markers") tw.line("to see available fixtures type: py.test --fixtures") + tw.line("(shown according to specified file_or_dir or current dir " + "if not specified)") return tw.line("conftest.py options:") @@ -120,7 +124,6 @@ def pytest_report_header(config): if config.option.traceconfig: lines.append("active plugins:") - plugins = [] items = config.pluginmanager._name2plugin.items() for name, plugin in items: if hasattr(plugin, '__file__'): diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py index 64479f03f..244100f5a 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -11,8 +11,8 @@ def pytest_addhooks(pluginmanager): def pytest_namespace(): """return dict of name->object to be made globally available in - the py.test/pytest namespace. This hook is called before command - line options are parsed. + the pytest namespace. This hook is called before command line options + are parsed. """ def pytest_cmdline_parse(pluginmanager, args): @@ -20,7 +20,7 @@ def pytest_cmdline_parse(pluginmanager, args): pytest_cmdline_parse.firstresult = True def pytest_cmdline_preparse(config, args): - """modify command line arguments before option parsing. """ + """(deprecated) modify command line arguments before option parsing. """ def pytest_addoption(parser): """register argparse-style options and ini-style config values. @@ -52,6 +52,10 @@ def pytest_cmdline_main(config): implementation will invoke the configure hooks and runtest_mainloop. """ pytest_cmdline_main.firstresult = True +def pytest_load_initial_conftests(args, early_config, parser): + """ implements loading initial conftests. + """ + def pytest_configure(config): """ called after command line options have been parsed and all plugins and initial conftest files been loaded. @@ -221,7 +225,7 @@ def pytest_report_teststatus(report): pytest_report_teststatus.firstresult = True def pytest_terminal_summary(terminalreporter): - """ add additional section in terminal summary reporting. """ + """ add additional section in terminal summary reporting. """ # ------------------------------------------------------------------------- # doctest hooks @@ -236,10 +240,7 @@ pytest_doctest_prepare_content.firstresult = True # ------------------------------------------------------------------------- def pytest_plugin_registered(plugin, manager): - """ a new py lib plugin got registered. """ - -def pytest_plugin_unregistered(plugin): - """ a py lib plugin got unregistered. """ + """ a new pytest plugin got registered. """ def pytest_internalerror(excrepr, excinfo): """ called for internal errors. """ diff --git a/_pytest/junitxml.py b/_pytest/junitxml.py index 503d8606b..2d330870f 100644 --- a/_pytest/junitxml.py +++ b/_pytest/junitxml.py @@ -9,7 +9,6 @@ import re import sys import time - # Python 2.X and 3.X compatibility try: unichr(65) @@ -62,8 +61,8 @@ def bin_xml_escape(arg): def pytest_addoption(parser): group = parser.getgroup("terminal reporting") - group.addoption('--junitxml', '--junit-xml', action="store", - dest="xmlpath", metavar="path", default=None, + group.addoption('--junitxml', '--junit-xml', action="store", + dest="xmlpath", metavar="path", default=None, help="create junit-xml style report file at given path.") group.addoption('--junitprefix', '--junit-prefix', action="store", metavar="str", default=None, @@ -131,36 +130,36 @@ class LogXML(object): self.skipped += 1 else: fail = Junit.failure(message="test failure") - fail.append(str(report.longrepr)) + fail.append(bin_xml_escape(report.longrepr)) self.append(fail) self.failed += 1 self._write_captured_output(report) def append_collect_failure(self, report): #msg = str(report.longrepr.reprtraceback.extraline) - self.append(Junit.failure(str(report.longrepr), + self.append(Junit.failure(bin_xml_escape(report.longrepr), message="collection failure")) self.errors += 1 def append_collect_skipped(self, report): #msg = str(report.longrepr.reprtraceback.extraline) - self.append(Junit.skipped(str(report.longrepr), + self.append(Junit.skipped(bin_xml_escape(report.longrepr), message="collection skipped")) self.skipped += 1 def append_error(self, report): - self.append(Junit.error(str(report.longrepr), + self.append(Junit.error(bin_xml_escape(report.longrepr), message="test setup failure")) self.errors += 1 def append_skipped(self, report): if hasattr(report, "wasxfail"): - self.append(Junit.skipped(str(report.wasxfail), + self.append(Junit.skipped(bin_xml_escape(report.wasxfail), message="expected test failure")) else: filename, lineno, skipreason = report.longrepr if skipreason.startswith("Skipped: "): - skipreason = skipreason[9:] + skipreason = bin_xml_escape(skipreason[9:]) self.append( Junit.skipped("%s:%s: %s" % report.longrepr, type="pytest.skip", @@ -194,17 +193,17 @@ class LogXML(object): def pytest_internalerror(self, excrepr): self.errors += 1 - data = py.xml.escape(excrepr) + data = bin_xml_escape(excrepr) self.tests.append( Junit.testcase( Junit.error(data, message="internal error"), classname="pytest", name="internal")) - def pytest_sessionstart(self, session): + def pytest_sessionstart(self): self.suite_start_time = time.time() - def pytest_sessionfinish(self, session, exitstatus, __multicall__): + def pytest_sessionfinish(self): if py.std.sys.version_info[0] < 3: logfile = py.std.codecs.open(self.logfile, 'w', encoding='utf-8') else: @@ -217,7 +216,7 @@ class LogXML(object): logfile.write('') logfile.write(Junit.testsuite( self.tests, - name="", + name="pytest", errors=self.errors, failures=self.failed, skips=self.skipped, diff --git a/_pytest/main.py b/_pytest/main.py index 5a9b0488e..3ebfc4dde 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -2,14 +2,12 @@ import py import pytest, _pytest -import inspect import os, sys, imp try: from collections import MutableMapping as MappingMixin except ImportError: from UserDict import DictMixin as MappingMixin -from _pytest.mark import MarkInfo from _pytest.runner import collect_one_node, Skipped tracebackcutdir = py.path.local(_pytest.__file__).dirpath() @@ -65,7 +63,7 @@ def pytest_namespace(): return dict(collect=collect) def pytest_configure(config): - py.test.config = config # compatibiltiy + pytest.config = config # compatibiltiy if config.option.exitfirst: config.option.maxfail = 1 @@ -76,7 +74,7 @@ def wrap_session(config, doit): initstate = 0 try: try: - config.pluginmanager.do_configure(config) + config.do_configure() initstate = 1 config.hook.pytest_sessionstart(session=session) initstate = 2 @@ -92,7 +90,7 @@ def wrap_session(config, doit): session.exitstatus = EXIT_INTERRUPTED except: excinfo = py.code.ExceptionInfo() - config.pluginmanager.notify_exception(excinfo, config.option) + config.notify_exception(excinfo, config.option) session.exitstatus = EXIT_INTERNALERROR if excinfo.errisinstance(SystemExit): sys.stderr.write("mainloop: caught Spurious SystemExit!\n") @@ -106,7 +104,8 @@ def wrap_session(config, doit): session=session, exitstatus=session.exitstatus) if initstate >= 1: - config.pluginmanager.do_unconfigure(config) + config.do_unconfigure() + config.pluginmanager.ensure_shutdown() return session.exitstatus def pytest_cmdline_main(config): @@ -172,30 +171,39 @@ def compatproperty(name): class NodeKeywords(MappingMixin): def __init__(self, node): - parent = node.parent - bases = parent and (parent.keywords._markers,) or () - self._markers = type("dynmarker", bases, {node.name: True}) + self.node = node + self.parent = node.parent + self._markers = {node.name: True} def __getitem__(self, key): try: - return getattr(self._markers, key) - except AttributeError: - raise KeyError(key) + return self._markers[key] + except KeyError: + if self.parent is None: + raise + return self.parent.keywords[key] def __setitem__(self, key, value): - setattr(self._markers, key, value) + self._markers[key] = value def __delitem__(self, key): - delattr(self._markers, key) + raise ValueError("cannot delete key in keywords dict") def __iter__(self): - return iter(self.keys()) + seen = set(self._markers) + if self.parent is not None: + seen.update(self.parent.keywords) + return iter(seen) def __len__(self): - return len(self.keys()) + return len(self.__iter__()) def keys(self): - return dir(self._markers) + return list(self) + + def __repr__(self): + return "" % (self.node, ) + class Node(object): """ base class for Collector and Item the test collection tree. @@ -223,6 +231,8 @@ class Node(object): #: allow adding of extra keywords to use for matching self.extra_keyword_matches = set() + # used for storing artificial fixturedefs for direct parametrization + self._name2pseudofixturedef = {} #self.extrainit() @property @@ -263,21 +273,11 @@ class Node(object): self._nodeid = x = self._makeid() return x - def _makeid(self): return self.parent.nodeid + "::" + self.name - def __eq__(self, other): - if not isinstance(other, Node): - return False - return (self.__class__ == other.__class__ and - self.name == other.name and self.parent == other.parent) - - def __ne__(self, other): - return not self == other - def __hash__(self): - return hash((self.name, self.parent)) + return hash(self.nodeid) def setup(self): pass @@ -314,6 +314,27 @@ class Node(object): chain.reverse() return chain + def add_marker(self, marker): + """ dynamically add a marker object to the node. + + ``marker`` can be a string or pytest.mark.* instance. + """ + from _pytest.mark import MarkDecorator + if isinstance(marker, py.builtin._basestring): + marker = MarkDecorator(marker) + elif not isinstance(marker, MarkDecorator): + raise ValueError("is not a string or pytest.mark.* Marker") + self.keywords[marker.name] = marker + + def get_marker(self, name): + """ get a marker object from this node or None if + the node doesn't have a marker with that name. """ + val = self.keywords.get(name, None) + if val is not None: + from _pytest.mark import MarkInfo, MarkDecorator + if isinstance(val, (MarkDecorator, MarkInfo)): + return val + def listextrakeywords(self): """ Return a set of all extra keywords in self and any parents.""" extra_keywords = set() @@ -337,6 +358,8 @@ class Node(object): self.session._setupstate.addfinalizer(fin, self) def getparent(self, cls): + """ get the next parent node (including ourself) + which is an instance of the given class""" current = self while current and not isinstance(current, cls): current = current.parent @@ -397,7 +420,6 @@ class Collector(Node): def _prunetraceback(self, excinfo): if hasattr(self, 'fspath'): - path = self.fspath traceback = excinfo.traceback ntraceback = traceback.cut(path=self.fspath) if ntraceback == traceback: @@ -672,11 +694,4 @@ class Session(FSCollector): yield x node.ihook.pytest_collectreport(report=rep) -def getfslineno(obj): - # xxx let decorators etc specify a sane ordering - if hasattr(obj, 'place_as'): - obj = obj.place_as - fslineno = py.code.getfslineno(obj) - assert isinstance(fslineno[1], int), obj - return fslineno diff --git a/_pytest/mark.py b/_pytest/mark.py index 9961f9d0e..6b66b1876 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -1,5 +1,5 @@ """ generic mechanism for marking and selecting python functions. """ -import pytest, py +import py def pytest_namespace(): @@ -39,14 +39,14 @@ def pytest_addoption(parser): def pytest_cmdline_main(config): if config.option.markers: - config.pluginmanager.do_configure(config) + config.do_configure() tw = py.io.TerminalWriter() for line in config.getini("markers"): name, rest = line.split(":", 1) tw.write("@pytest.mark.%s:" % name, bold=True) tw.line(rest) tw.line() - config.pluginmanager.do_unconfigure(config) + config.do_unconfigure() return 0 pytest_cmdline_main.tryfirst = True @@ -81,11 +81,8 @@ def pytest_collection_modifyitems(items, config): class MarkMapping: - """Provides a local mapping for markers. - Only the marker names from the given :class:`NodeKeywords` will be mapped, - so the names are taken only from :class:`MarkInfo` or - :class:`MarkDecorator` items. - """ + """Provides a local mapping for markers where item access + resolves to True if the marker is present. """ def __init__(self, keywords): mymarks = set() for key, value in keywords.items(): @@ -93,8 +90,8 @@ class MarkMapping: mymarks.add(key) self._mymarks = mymarks - def __getitem__(self, markname): - return markname in self._mymarks + def __getitem__(self, name): + return name in self._mymarks class KeywordMapping: @@ -129,6 +126,7 @@ def matchkeyword(colitem, keywordexpr): mapped_names = set() # Add the names of the current item and any parent items + import pytest for item in colitem.listchain(): if not isinstance(item, pytest.Instance): mapped_names.add(item.name) @@ -138,23 +136,31 @@ def matchkeyword(colitem, keywordexpr): mapped_names.add(name) # Add the names attached to the current function through direct assignment - for name in colitem.function.__dict__: - mapped_names.add(name) + if hasattr(colitem, 'function'): + for name in colitem.function.__dict__: + mapped_names.add(name) - return eval(keywordexpr, {}, KeywordMapping(mapped_names)) + mapping = KeywordMapping(mapped_names) + if " " not in keywordexpr: + # special case to allow for simple "-k pass" and "-k 1.3" + return mapping[keywordexpr] + elif keywordexpr.startswith("not ") and " " not in keywordexpr[4:]: + return not mapping[keywordexpr[4:]] + return eval(keywordexpr, {}, mapping) def pytest_configure(config): + import pytest if config.option.strict: pytest.mark._config = config class MarkGenerator: """ Factory for :class:`MarkDecorator` objects - exposed as - a ``py.test.mark`` singleton instance. Example:: + a ``pytest.mark`` singleton instance. Example:: import py - @py.test.mark.slowtest + @pytest.mark.slowtest def test_function(): pass @@ -163,7 +169,7 @@ class MarkGenerator: def __getattr__(self, name): if name[0] == "_": - raise AttributeError(name) + raise AttributeError("Marker name must NOT start with underscore") if hasattr(self, '_config'): self._check(name) return MarkDecorator(name) @@ -182,6 +188,9 @@ class MarkGenerator: if name not in self._markers: raise AttributeError("%r not a registered marker" % (name,)) +def istestfunc(func): + return hasattr(func, "__call__") and \ + getattr(func, "__name__", "") != "" class MarkDecorator: """ A decorator for test functions and test classes. When applied @@ -189,32 +198,54 @@ class MarkDecorator: :ref:`retrieved by hooks as item keywords `. MarkDecorator instances are often created like this:: - mark1 = py.test.mark.NAME # simple MarkDecorator - mark2 = py.test.mark.NAME(name1=value) # parametrized MarkDecorator + mark1 = pytest.mark.NAME # simple MarkDecorator + mark2 = pytest.mark.NAME(name1=value) # parametrized MarkDecorator and can then be applied as decorators to test functions:: @mark2 def test_function(): pass + + When a MarkDecorator instance is called it does the following: + 1. If called with a single class as its only positional argument and no + additional keyword arguments, it attaches itself to the class so it + gets applied automatically to all test cases found in that class. + 2. If called with a single function as its only positional argument and + no additional keyword arguments, it attaches a MarkInfo object to the + function, containing all the arguments already stored internally in + the MarkDecorator. + 3. When called in any other case, it performs a 'fake construction' call, + i.e. it returns a new MarkDecorator instance with the original + MarkDecorator's content updated with the arguments passed to this + call. + + Note: The rules above prevent MarkDecorator objects from storing only a + single function or class reference as their positional argument with no + additional keyword or positional arguments. + """ def __init__(self, name, args=None, kwargs=None): - self.markname = name + self.name = name self.args = args or () self.kwargs = kwargs or {} + @property + def markname(self): + return self.name # for backward-compat (2.4.1 had this attr) + def __repr__(self): d = self.__dict__.copy() - name = d.pop('markname') + name = d.pop('name') return "" % (name, d) def __call__(self, *args, **kwargs): """ if passed a single callable argument: decorate it with mark info. otherwise add *args/**kwargs in-place to mark information. """ - if args: + if args and not kwargs: func = args[0] - if len(args) == 1 and hasattr(func, '__call__') or \ - hasattr(func, '__bases__'): + if len(args) == 1 and (istestfunc(func) or + hasattr(func, '__bases__')): if hasattr(func, '__bases__'): if hasattr(func, 'pytestmark'): l = func.pytestmark @@ -225,19 +256,19 @@ class MarkDecorator: else: func.pytestmark = [self] else: - holder = getattr(func, self.markname, None) + holder = getattr(func, self.name, None) if holder is None: holder = MarkInfo( - self.markname, self.args, self.kwargs + self.name, self.args, self.kwargs ) - setattr(func, self.markname, holder) + setattr(func, self.name, holder) else: holder.add(self.args, self.kwargs) return func kw = self.kwargs.copy() kw.update(kwargs) args = self.args + args - return self.__class__(self.markname, args=args, kwargs=kw) + return self.__class__(self.name, args=args, kwargs=kw) class MarkInfo: diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index 9d5f39c56..be131bd93 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -1,7 +1,7 @@ """ monkeypatching and mocking functionality. """ -import os, sys, inspect -import pytest +import os, sys +from py.builtin import _basestring def pytest_funcarg__monkeypatch(request): """The returned ``monkeypatch`` funcarg provides these @@ -25,6 +25,40 @@ def pytest_funcarg__monkeypatch(request): request.addfinalizer(mpatch.undo) return mpatch + + +def derive_importpath(import_path): + import pytest + if not isinstance(import_path, _basestring) or "." not in import_path: + raise TypeError("must be absolute import path string, not %r" % + (import_path,)) + rest = [] + target = import_path + while target: + try: + obj = __import__(target, None, None, "__doc__") + except ImportError: + if "." not in target: + __tracebackhide__ = True + pytest.fail("could not import any sub part: %s" % + import_path) + target, name = target.rsplit(".", 1) + rest.append(name) + else: + assert rest + try: + while len(rest) > 1: + attr = rest.pop() + obj = getattr(obj, attr) + attr = rest[0] + getattr(obj, attr) + except AttributeError: + __tracebackhide__ = True + pytest.fail("object %r has no attribute %r" % (obj, attr)) + return attr, obj + + + notset = object() class monkeypatch: @@ -34,70 +68,67 @@ class monkeypatch: self._setitem = [] self._cwd = None - def replace(self, import_path, value): - """ replace the object specified by a dotted ``import_path`` - with the given ``value``. + def setattr(self, target, name, value=notset, raising=True): + """ set attribute value on target, memorizing the old value. + By default raise AttributeError if the attribute did not exist. - For example ``replace("os.path.abspath", value)`` will - trigger an ``import os.path`` and a subsequent - setattr(os.path, "abspath", value). Or to prevent - the requests library from performing requests you can call - ``replace("requests.sessions.Session.request", None)`` - which will lead to an import of ``requests.sessions`` and a call - to ``setattr(requests.sessions.Session, "request", None)``. + For convenience you can specify a string as ``target`` which + will be interpreted as a dotted import path, with the last part + being the attribute name. Example: + ``monkeypatch.setattr("os.getcwd", lambda x: "/")`` + would set the ``getcwd`` function of the ``os`` module. + + The ``raising`` value determines if the setattr should fail + if the attribute is not already present (defaults to True + which means it will raise). """ - if not isinstance(import_path, str) or "." not in import_path: - raise TypeError("must be absolute import path string, not %r" % - (import_path,)) - rest = [] - target = import_path - while target: - try: - obj = __import__(target, None, None, "__doc__") - except ImportError: - if "." not in target: - __tracebackhide__ = True - pytest.fail("could not import any sub part: %s" % - import_path) - target, name = target.rsplit(".", 1) - rest.append(name) - else: - assert rest - try: - while len(rest) > 1: - attr = rest.pop() - obj = getattr(obj, attr) - attr = rest[0] - getattr(obj, attr) - except AttributeError: - __tracebackhide__ = True - pytest.fail("object %r has no attribute %r" % (obj, attr)) - return self.setattr(obj, attr, value) + __tracebackhide__ = True + import inspect - def setattr(self, obj, name, value, raising=True): - """ set attribute ``name`` on ``obj`` to ``value``, by default - raise AttributeEror if the attribute did not exist. + if value is notset: + if not isinstance(target, _basestring): + raise TypeError("use setattr(target, name, value) or " + "setattr(target, value) with target being a dotted " + "import string") + value = name + name, target = derive_importpath(target) - """ - oldval = getattr(obj, name, notset) + oldval = getattr(target, name, notset) if raising and oldval is notset: - raise AttributeError("%r has no attribute %r" %(obj, name)) + raise AttributeError("%r has no attribute %r" %(target, name)) # avoid class descriptors like staticmethod/classmethod - if inspect.isclass(obj): - oldval = obj.__dict__.get(name, notset) - self._setattr.insert(0, (obj, name, oldval)) - setattr(obj, name, value) + if inspect.isclass(target): + oldval = target.__dict__.get(name, notset) + self._setattr.insert(0, (target, name, oldval)) + setattr(target, name, value) - def delattr(self, obj, name, raising=True): - """ delete attribute ``name`` from ``obj``, by default raise - AttributeError it the attribute did not previously exist. """ - if not hasattr(obj, name): + def delattr(self, target, name=notset, raising=True): + """ delete attribute ``name`` from ``target``, by default raise + AttributeError it the attribute did not previously exist. + + If no ``name`` is specified and ``target`` is a string + it will be interpreted as a dotted import path with the + last part being the attribute name. + + If raising is set to false, the attribute is allowed to not + pre-exist. + """ + __tracebackhide__ = True + if name is notset: + if not isinstance(target, _basestring): + raise TypeError("use delattr(target, name) or " + "delattr(target) with target being a dotted " + "import string") + name, target = derive_importpath(target) + + if not hasattr(target, name): if raising: raise AttributeError(name) else: - self._setattr.insert(0, (obj, name, getattr(obj, name, notset))) - delattr(obj, name) + self._setattr.insert(0, (target, name, + getattr(target, name, notset))) + delattr(target, name) def setitem(self, dic, name, value): """ set dictionary entry ``name`` to value. """ diff --git a/_pytest/nose.py b/_pytest/nose.py index 8702355e6..ecfc2316b 100644 --- a/_pytest/nose.py +++ b/_pytest/nose.py @@ -1,7 +1,6 @@ """ run test suites written for nose. """ import pytest, py -import inspect import sys from _pytest import unittest @@ -9,7 +8,7 @@ def pytest_runtest_makereport(__multicall__, item, call): SkipTest = getattr(sys.modules.get('nose', None), 'SkipTest', None) if SkipTest: if call.excinfo and call.excinfo.errisinstance(SkipTest): - # let's substitute the excinfo with a py.test.skip one + # let's substitute the excinfo with a pytest.skip one call2 = call.__class__(lambda: pytest.skip(str(call.excinfo.value)), call.when) call.excinfo = call2.excinfo diff --git a/_pytest/pytester.py b/_pytest/pytester.py index 2b96beef9..5442a1398 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -1,10 +1,9 @@ -""" (disabled by default) support for testing py.test and py.test plugins. """ +""" (disabled by default) support for testing pytest and pytest plugins. """ import py, pytest import sys, os import codecs import re -import inspect import time from fnmatch import fnmatch from _pytest.main import Session, EXIT_OK @@ -27,7 +26,6 @@ def pytest_addoption(parser): def pytest_configure(config): # This might be called multiple times. Only take the first. global _pytest_fullpath - import pytest try: _pytest_fullpath except NameError: @@ -83,7 +81,8 @@ class HookRecorder: def finish_recording(self): for recorder in self._recorders.values(): - self._pluginmanager.unregister(recorder) + if self._pluginmanager.isregistered(recorder): + self._pluginmanager.unregister(recorder) self._recorders.clear() def _makecallparser(self, method): @@ -121,7 +120,6 @@ class HookRecorder: def contains(self, entries): __tracebackhide__ = True - from py.builtin import print_ i = 0 entries = list(entries) backlocals = py.std.sys._getframe(1).f_locals @@ -139,7 +137,7 @@ class HookRecorder: break print_("NONAMEMATCH", name, "with", call) else: - py.test.fail("could not find %r check %r" % (name, check)) + pytest.fail("could not find %r check %r" % (name, check)) def popcall(self, name): __tracebackhide__ = True @@ -149,7 +147,7 @@ class HookRecorder: return call lines = ["could not find call %r, in:" % (name,)] lines.extend([" %s" % str(x) for x in self.calls]) - py.test.fail("\n".join(lines)) + pytest.fail("\n".join(lines)) def getcall(self, name): l = self.getcalls(name) @@ -260,9 +258,6 @@ class TmpTestdir: def makefile(self, ext, *args, **kwargs): return self._makefile(ext, args, kwargs) - def makeini(self, source): - return self.makefile('cfg', setup=source) - def makeconftest(self, source): return self.makepyfile(conftest=source) @@ -361,7 +356,7 @@ class TmpTestdir: if not plugins: plugins = [] plugins.append(Collect()) - ret = self.pytestmain(list(args), plugins=plugins) + ret = pytest.main(list(args), plugins=plugins) reprec = rec[0] reprec.ret = ret assert len(rec) == 1 @@ -374,23 +369,24 @@ class TmpTestdir: break else: args.append("--basetemp=%s" % self.tmpdir.dirpath('basetemp')) - import _pytest.core - config = _pytest.core._prepareconfig(args, self.plugins) - # the in-process pytest invocation needs to avoid leaking FDs - # so we register a "reset_capturings" callmon the capturing manager - # and make sure it gets called - config._cleanup.append( - config.pluginmanager.getplugin("capturemanager").reset_capturings) import _pytest.config - self.request.addfinalizer( - lambda: _pytest.config.pytest_unconfigure(config)) + config = _pytest.config._prepareconfig(args, self.plugins) + # we don't know what the test will do with this half-setup config + # object and thus we make sure it gets unconfigured properly in any + # case (otherwise capturing could still be active, for example) + def ensure_unconfigure(): + if hasattr(config.pluginmanager, "_config"): + config.pluginmanager.do_unconfigure(config) + config.pluginmanager.ensure_shutdown() + + self.request.addfinalizer(ensure_unconfigure) return config def parseconfigure(self, *args): config = self.parseconfig(*args) - config.pluginmanager.do_configure(config) + config.do_configure() self.request.addfinalizer(lambda: - config.pluginmanager.do_unconfigure(config)) + config.do_unconfigure()) return config def getitem(self, source, funcname="test_func"): @@ -428,17 +424,6 @@ class TmpTestdir: return py.std.subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) - def pytestmain(self, *args, **kwargs): - class ResetCapturing: - @pytest.mark.trylast - def pytest_unconfigure(self, config): - capman = config.pluginmanager.getplugin("capturemanager") - capman.reset_capturings() - plugins = kwargs.setdefault("plugins", []) - rc = ResetCapturing() - plugins.append(rc) - return pytest.main(*args, **kwargs) - def run(self, *cmdargs): return self._run(*cmdargs) @@ -482,12 +467,12 @@ class TmpTestdir: def _getpybinargs(self, scriptname): if not self.request.config.getvalue("notoolsonpath"): - # XXX we rely on script refering to the correct environment + # XXX we rely on script referring to the correct environment # we cannot use "(py.std.sys.executable,script)" - # becaue on windows the script is e.g. a py.test.exe - return (py.std.sys.executable, _pytest_fullpath,) + # because on windows the script is e.g. a py.test.exe + return (py.std.sys.executable, _pytest_fullpath,) # noqa else: - py.test.skip("cannot run %r with --no-tools-on-path" % scriptname) + pytest.skip("cannot run %r with --no-tools-on-path" % scriptname) def runpython(self, script, prepend=True): if prepend: @@ -524,22 +509,23 @@ class TmpTestdir: def spawn_pytest(self, string, expect_timeout=10.0): if self.request.config.getvalue("notoolsonpath"): - py.test.skip("--no-tools-on-path prevents running pexpect-spawn tests") + pytest.skip("--no-tools-on-path prevents running pexpect-spawn tests") basetemp = self.tmpdir.mkdir("pexpect") invoke = " ".join(map(str, self._getpybinargs("py.test"))) cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string) return self.spawn(cmd, expect_timeout=expect_timeout) def spawn(self, cmd, expect_timeout=10.0): - pexpect = py.test.importorskip("pexpect", "2.4") + pexpect = pytest.importorskip("pexpect", "3.0") if hasattr(sys, 'pypy_version_info') and '64' in py.std.platform.machine(): pytest.skip("pypy-64 bit not supported") if sys.platform == "darwin": pytest.xfail("pexpect does not work reliably on darwin?!") if sys.platform.startswith("freebsd"): pytest.xfail("pexpect does not work reliably on freebsd") - logfile = self.tmpdir.join("spawn.out") - child = pexpect.spawn(cmd, logfile=logfile.open("w")) + logfile = self.tmpdir.join("spawn.out").open("wb") + child = pexpect.spawn(cmd, logfile=logfile) + self.request.addfinalizer(logfile.close) child.timeout = expect_timeout return child @@ -670,6 +656,12 @@ class LineMatcher: else: raise ValueError("line %r not found in output" % line) + def get_lines_after(self, fnline): + for i, line in enumerate(self.lines): + if fnline == line or fnmatch(line, fnline): + return self.lines[i+1:] + raise ValueError("line %r not found in output" % fnline) + def fnmatch_lines(self, lines2): def show(arg1, arg2): py.builtin.print_(arg1, arg2, file=py.std.sys.stderr) @@ -696,4 +688,4 @@ class LineMatcher: show(" and:", repr(nextline)) extralines.append(nextline) else: - py.test.fail("remains unmatched: %r, see stderr" % (line,)) + pytest.fail("remains unmatched: %r, see stderr" % (line,)) diff --git a/_pytest/python.py b/_pytest/python.py index 00b08ec2b..91369fedb 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -3,18 +3,27 @@ import py import inspect import sys import pytest -from _pytest.main import getfslineno -from _pytest.mark import MarkDecorator, MarkInfo -from _pytest.monkeypatch import monkeypatch +from _pytest.mark import MarkDecorator from py._code.code import TerminalRepr import _pytest cutdir = py.path.local(_pytest.__file__).dirpath() NoneType = type(None) +NOTSET = object() callable = py.builtin.callable +def getfslineno(obj): + # xxx let decorators etc specify a sane ordering + while hasattr(obj, "__wrapped__"): + obj = obj.__wrapped__ + if hasattr(obj, 'place_as'): + obj = obj.place_as + fslineno = py.code.getfslineno(obj) + assert isinstance(fslineno[1], int), obj + return fslineno + def getimfunc(func): try: return func.__func__ @@ -26,19 +35,23 @@ def getimfunc(func): class FixtureFunctionMarker: - def __init__(self, scope, params, autouse=False): + def __init__(self, scope, params, + autouse=False, yieldctx=False, ids=None): self.scope = scope self.params = params self.autouse = autouse + self.yieldctx = yieldctx + self.ids = ids def __call__(self, function): if inspect.isclass(function): - raise ValueError("class fixtures not supported (may be in the future)") + raise ValueError( + "class fixtures not supported (may be in the future)") function._pytestfixturefunction = self return function -def fixture(scope="function", params=None, autouse=False): +def fixture(scope="function", params=None, autouse=False, ids=None): """ (return a) decorator to mark a fixture factory function. This decorator can be used (with or or without parameters) to define @@ -59,12 +72,36 @@ def fixture(scope="function", params=None, autouse=False): :arg autouse: if True, the fixture func is activated for all tests that can see it. If False (the default) then an explicit reference is needed to activate the fixture. + + :arg ids: list of string ids each corresponding to the params + so that they are part of the test id. If no ids are provided + they will be generated automatically from the params. + """ if callable(scope) and params is None and autouse == False: # direct decoration - return FixtureFunctionMarker("function", params, autouse)(scope) + return FixtureFunctionMarker( + "function", params, autouse)(scope) + if params is not None and not isinstance(params, (list, tuple)): + params = list(params) + return FixtureFunctionMarker(scope, params, autouse, ids=ids) + +def yield_fixture(scope="function", params=None, autouse=False, ids=None): + """ (return a) decorator to mark a yield-fixture factory function + (EXPERIMENTAL). + + This takes the same arguments as :py:func:`pytest.fixture` but + expects a fixture function to use a ``yield`` instead of a ``return`` + statement to provide a fixture. See + http://pytest.org/en/latest/yieldfixture.html for more info. + """ + if callable(scope) and params is None and autouse == False: + # direct decoration + return FixtureFunctionMarker( + "function", params, autouse, yieldctx=True)(scope) else: - return FixtureFunctionMarker(scope, params, autouse=autouse) + return FixtureFunctionMarker(scope, params, autouse, + yieldctx=True, ids=ids) defaultfuncargprefixmarker = fixture() @@ -110,10 +147,12 @@ def pytest_generate_tests(metafunc): def pytest_configure(config): config.addinivalue_line("markers", "parametrize(argnames, argvalues): call a test function multiple " - "times passing in multiple different argument value sets. Example: " - "@parametrize('arg1', [1,2]) would lead to two calls of the decorated " - "test function, one with arg1=1 and another with arg1=2." - " see http://pytest.org/latest/parametrize.html for more info and " + "times passing in different arguments in turn. argvalues generally " + "needs to be a list of values if argnames specifies only one name " + "or a list of tuples of values if argnames specifies multiple names. " + "Example: @parametrize('arg1', [1,2]) would lead to two calls of the " + "decorated test function, one with arg1=1 and another with arg1=2." + "see http://pytest.org/latest/parametrize.html for more info and " "examples." ) config.addinivalue_line("markers", @@ -129,6 +168,7 @@ def pytest_namespace(): raises.Exception = pytest.fail.Exception return { 'fixture': fixture, + 'yield_fixture': yield_fixture, 'raises' : raises, 'collect': { 'Module': Module, 'Class': Class, 'Instance': Instance, @@ -136,7 +176,7 @@ def pytest_namespace(): '_fillfuncargs': fillfixtures} } -@fixture() +@fixture(scope="session") def pytestconfig(request): """ the pytest config object with access to command line opts.""" return request.config @@ -156,7 +196,6 @@ def pytest_pyfunc_call(__multicall__, pyfuncitem): def pytest_collect_file(path, parent): ext = path.ext - pb = path.purebasename if ext == ".py": if not parent.session.isinitpath(path): for pat in parent.config.getini('python_files'): @@ -248,10 +287,9 @@ class PyobjMixin(PyobjContext): if fspath.endswith(".pyc"): fspath = fspath[:-1] lineno = obj.compat_co_firstlineno - modpath = obj.__module__ else: fspath, lineno = getfslineno(obj) - modpath = self.getmodpath() + modpath = self.getmodpath() assert isinstance(lineno, int) return fspath, lineno, modpath @@ -313,12 +351,70 @@ class PyCollector(PyobjMixin, pytest.Collector): if not metafunc._calls: yield Function(name, parent=self) else: + # add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs + add_funcarg_pseudo_fixture_def(self, metafunc, fm) + for callspec in metafunc._calls: subname = "%s[%s]" %(name, callspec.id) yield Function(name=subname, parent=self, callspec=callspec, callobj=funcobj, keywords={callspec.id:True}) +def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager): + # this function will transform all collected calls to a functions + # if they use direct funcargs (i.e. direct parametrization) + # because we want later test execution to be able to rely on + # an existing FixtureDef structure for all arguments. + # XXX we can probably avoid this algorithm if we modify CallSpec2 + # to directly care for creating the fixturedefs within its methods. + if not metafunc._calls[0].funcargs: + return # this function call does not have direct parametrization + # collect funcargs of all callspecs into a list of values + arg2params = {} + arg2scope = {} + arg2fixturedefs = metafunc._arg2fixturedefs + for param_index, callspec in enumerate(metafunc._calls): + for argname, argvalue in callspec.funcargs.items(): + arg2params.setdefault(argname, []).append(argvalue) + if argname not in arg2scope: + scopenum = callspec._arg2scopenum.get(argname, scopenum_function) + arg2scope[argname] = scopes[scopenum] + callspec.indices[argname] = param_index + + for argname in callspec.funcargs: + assert argname not in callspec.params + callspec.params.update(callspec.funcargs) + callspec.funcargs.clear() + + # register artificial FixtureDef's so that later at test execution + # time we can rely on a proper FixtureDef to exist for fixture setup. + for argname, valuelist in arg2params.items(): + # if we have a scope that is higher than function we need + # to make sure we only ever create an according fixturedef on + # a per-scope basis. We thus store and cache the fixturedef on the + # node related to the scope. + scope = arg2scope[argname] + node = None + if scope != "function": + node = get_scope_node(collector, scope) + if node is None: + assert scope == "class" and isinstance(collector, Module) + # use module-level collector for class-scope (for now) + node = collector + if node and argname in node._name2pseudofixturedef: + arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]] + else: + fixturedef = FixtureDef(fixturemanager, '', argname, + get_direct_param_fixture_func, + arg2scope[argname], + valuelist, False, False) + arg2fixturedefs[argname] = [fixturedef] + if node is not None: + node._name2pseudofixturedef[argname] = fixturedef + + +def get_direct_param_fixture_func(request): + return request.param class FuncFixtureInfo: def __init__(self, argnames, names_closure, name2fixturedefs): @@ -389,7 +485,7 @@ class Module(pytest.File, PyCollector): fin = getattr(self.obj, 'teardown_module', None) if fin is not None: #XXX: nose compat hack, move to nose plugin - # if it takes a positional arg, its probably a py.test style one + # if it takes a positional arg, it's probably a pytest style one # so we pass the current module object if inspect.getargspec(fin)[0]: finalizer = lambda: fin(self.obj) @@ -532,25 +628,24 @@ def hasinit(obj): def fillfixtures(function): """ fill missing funcargs for a test function. """ - if 1 or getattr(function, "_args", None) is None: # not a yielded function - try: - request = function._request - except AttributeError: - # XXX this special code path is only expected to execute - # with the oejskit plugin. It uses classes with funcargs - # and we thus have to work a bit to allow this. - fm = function.session._fixturemanager - fi = fm.getfixtureinfo(function.parent, function.obj, None) - function._fixtureinfo = fi - request = function._request = FixtureRequest(function) - request._fillfixtures() - # prune out funcargs for jstests - newfuncargs = {} - for name in fi.argnames: - newfuncargs[name] = function.funcargs[name] - function.funcargs = newfuncargs - else: - request._fillfixtures() + try: + request = function._request + except AttributeError: + # XXX this special code path is only expected to execute + # with the oejskit plugin. It uses classes with funcargs + # and we thus have to work a bit to allow this. + fm = function.session._fixturemanager + fi = fm.getfixtureinfo(function.parent, function.obj, None) + function._fixtureinfo = fi + request = function._request = FixtureRequest(function) + request._fillfixtures() + # prune out funcargs for jstests + newfuncargs = {} + for name in fi.argnames: + newfuncargs[name] = function.funcargs[name] + function.funcargs = newfuncargs + else: + request._fillfixtures() _notexists = object() @@ -566,12 +661,14 @@ class CallSpec2(object): self._globalparam = _notexists self._arg2scopenum = {} # used for sorting parametrized resources self.keywords = {} + self.indices = {} def copy(self, metafunc): cs = CallSpec2(self.metafunc) cs.funcargs.update(self.funcargs) cs.params.update(self.params) cs.keywords.update(self.keywords) + cs.indices.update(self.indices) cs._arg2scopenum.update(self._arg2scopenum) cs._idlist = list(self._idlist) cs._globalid = self._globalid @@ -595,14 +692,12 @@ class CallSpec2(object): def id(self): return "-".join(map(str, filter(None, self._idlist))) - def setmulti(self, valtype, argnames, valset, id, keywords, scopenum=0): + def setmulti(self, valtype, argnames, valset, id, keywords, scopenum, + param_index): for arg,val in zip(argnames, valset): self._checkargnotcontained(arg) getattr(self, valtype)[arg] = val - # we want self.params to be always set because of - # parametrize_sorted() which groups tests by params/scope - if valtype == "funcargs": - self.params[arg] = id + self.indices[arg] = param_index self._arg2scopenum[arg] = scopenum if val is _notexists: self._emptyparamspecified = True @@ -618,6 +713,8 @@ class CallSpec2(object): if param is not _notexists: assert self._globalparam is _notexists self._globalparam = param + for arg in funcargs: + self._arg2scopenum[arg] = scopenum_function class FuncargnamesCompatAttr: @@ -673,29 +770,30 @@ class Metafunc(FuncargnamesCompatAttr): to set a dynamic scope using test context or configuration. """ - # individual parametrized argument sets can be wrapped in a - # marker in which case we unwrap the values and apply the mark + # individual parametrized argument sets can be wrapped in a series + # of markers in which case we unwrap the values and apply the mark # at Function init newkeywords = {} unwrapped_argvalues = [] for i, argval in enumerate(argvalues): - if isinstance(argval, MarkDecorator): + while isinstance(argval, MarkDecorator): newmark = MarkDecorator(argval.markname, argval.args[:-1], argval.kwargs) - newkeywords[i] = {newmark.markname: newmark} + newmarks = newkeywords.setdefault(i, {}) + newmarks[newmark.markname] = newmark argval = argval.args[-1] unwrapped_argvalues.append(argval) argvalues = unwrapped_argvalues if not isinstance(argnames, (tuple, list)): argnames = [x.strip() for x in argnames.split(",") if x.strip()] - if len(argnames) == 1: - argvalues = [(val,) for val in argvalues] + if len(argnames) == 1: + argvalues = [(val,) for val in argvalues] if not argvalues: argvalues = [(_notexists,) * len(argnames)] if scope is None: - scope = "subfunction" + scope = "function" scopenum = scopes.index(scope) if not indirect: #XXX should we also check for the opposite case? @@ -711,11 +809,12 @@ class Metafunc(FuncargnamesCompatAttr): ids = idmaker(argnames, argvalues) newcalls = [] for callspec in self._calls or [CallSpec2(self)]: - for i, valset in enumerate(argvalues): + for param_index, valset in enumerate(argvalues): assert len(valset) == len(argnames) newcallspec = callspec.copy(self) - newcallspec.setmulti(valtype, argnames, valset, ids[i], - newkeywords.get(i, {}), scopenum) + newcallspec.setmulti(valtype, argnames, valset, ids[param_index], + newkeywords.get(param_index, {}), scopenum, + param_index) newcalls.append(newcallspec) self._calls = newcalls @@ -863,6 +962,22 @@ def raises(ExpectedException, *args, **kwargs): >>> raises(ZeroDivisionError, "f(0)") + + Performance note: + ----------------- + + Similar to caught exception objects in Python, explicitly clearing local + references to returned ``py.code.ExceptionInfo`` objects can help the Python + interpreter speed up its garbage collection. + + Clearing those references breaks a reference cycle (``ExceptionInfo`` --> + caught exception --> frame stack raising the exception --> current frame + stack --> local variables --> ``ExceptionInfo``) which makes Python keep all + objects referenced from that cycle (including all local variables in the + current frame) alive until the next cyclic garbage collection run. See the + official Python ``try`` statement documentation for more detailed + information. + """ __tracebackhide__ = True if ExpectedException is AssertionError: @@ -893,10 +1008,6 @@ def raises(ExpectedException, *args, **kwargs): func(*args[1:], **kwargs) except ExpectedException: return py.code.ExceptionInfo() - k = ", ".join(["%s=%r" % x for x in kwargs.items()]) - if k: - k = ', ' + k - expr = '%s(%r%s)' %(getattr(func, '__name__', func), args, k) pytest.fail("DID NOT RAISE") class RaisesContext(object): @@ -916,20 +1027,20 @@ class RaisesContext(object): return issubclass(self.excinfo.type, self.ExpectedException) # -# the basic py.test Function item +# the basic pytest Function item # -_dummy = object() + class Function(FunctionMixin, pytest.Item, FuncargnamesCompatAttr): """ a Function Item is responsible for setting up and executing a Python test function. """ _genid = None def __init__(self, name, parent, args=None, config=None, - callspec=None, callobj=_dummy, keywords=None, session=None): + callspec=None, callobj=NOTSET, keywords=None, session=None): super(Function, self).__init__(name, parent, config=config, session=session) self._args = args - if callobj is not _dummy: + if callobj is not NOTSET: self.obj = callobj for name, val in (py.builtin._getfuncdict(self.obj) or {}).items(): @@ -941,30 +1052,26 @@ class Function(FunctionMixin, pytest.Item, FuncargnamesCompatAttr): for name, val in keywords.items(): self.keywords[name] = val - fm = self.session._fixturemanager isyield = self._isyieldedfunction() - self._fixtureinfo = fi = fm.getfixtureinfo(self.parent, self.obj, - self.cls, - funcargs=not isyield) + self._fixtureinfo = fi = self.session._fixturemanager.getfixtureinfo( + self.parent, self.obj, self.cls, funcargs=not isyield) self.fixturenames = fi.names_closure if callspec is not None: self.callspec = callspec self._initrequest() def _initrequest(self): + self.funcargs = {} if self._isyieldedfunction(): assert not hasattr(self, "callspec"), ( "yielded functions (deprecated) cannot have funcargs") - self.funcargs = {} else: if hasattr(self, "callspec"): callspec = self.callspec - self.funcargs = callspec.funcargs.copy() + assert not callspec.funcargs self._genid = callspec.id if hasattr(callspec, "param"): self.param = callspec.param - else: - self.funcargs = {} self._request = FixtureRequest(self) @property @@ -994,7 +1101,7 @@ class Function(FunctionMixin, pytest.Item, FuncargnamesCompatAttr): def setup(self): # check if parametrization happend with an empty list try: - empty = self.callspec._emptyparamspecified + self.callspec._emptyparamspecified except AttributeError: pass else: @@ -1004,24 +1111,6 @@ class Function(FunctionMixin, pytest.Item, FuncargnamesCompatAttr): super(Function, self).setup() fillfixtures(self) - def __eq__(self, other): - try: - return (self.name == other.name and - self._args == other._args and - self.parent == other.parent and - self.obj == other.obj and - getattr(self, '_genid', None) == - getattr(other, '_genid', None) - ) - except AttributeError: - pass - return False - - def __ne__(self, other): - return not self == other - - def __hash__(self): - return hash((self.parent, self.name)) scope2props = dict(session=()) scope2props["module"] = ("fspath", "module") @@ -1051,36 +1140,34 @@ class FixtureRequest(FuncargnamesCompatAttr): def __init__(self, pyfuncitem): self._pyfuncitem = pyfuncitem - if hasattr(pyfuncitem, '_requestparam'): - self.param = pyfuncitem._requestparam #: fixture for which this request is being performed self.fixturename = None #: Scope string, one of "function", "cls", "module", "session" self.scope = "function" - self.getparent = pyfuncitem.getparent - self._funcargs = self._pyfuncitem.funcargs.copy() - self._fixtureinfo = fi = pyfuncitem._fixtureinfo - self._arg2fixturedefs = fi.name2fixturedefs + self._funcargs = {} + self._fixturedefs = {} + fixtureinfo = pyfuncitem._fixtureinfo + self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() self._arg2index = {} - self.fixturenames = self._fixtureinfo.names_closure + self.fixturenames = fixtureinfo.names_closure self._fixturemanager = pyfuncitem.session._fixturemanager - self._parentid = pyfuncitem.parent.nodeid - self._fixturestack = [] @property def node(self): """ underlying collection node (depends on current request scope)""" return self._getscopeitem(self.scope) + def _getnextfixturedef(self, argname): fixturedefs = self._arg2fixturedefs.get(argname, None) if fixturedefs is None: - # we arrive here because of a getfuncargvalue(argname) usage which - # was naturally not knowable at parsing/collection time + # we arrive here because of a a dynamic call to + # getfuncargvalue(argname) usage which was naturally + # not known at parsing/collection time fixturedefs = self._fixturemanager.getfixturedefs( - argname, self._parentid) + argname, self._pyfuncitem.parent.nodeid) self._arg2fixturedefs[argname] = fixturedefs - # fixturedefs is immutable so we maintain a decreasing index + # fixturedefs list is immutable so we maintain a decreasing index index = self._arg2index.get(argname, 0) - 1 if fixturedefs is None or (-index > len(fixturedefs)): raise FixtureLookupError(argname, self) @@ -1112,7 +1199,9 @@ class FixtureRequest(FuncargnamesCompatAttr): try: return self._pyfuncitem._testcase except AttributeError: - return py.builtin._getimself(self.function) + function = getattr(self, "function", None) + if function is not None: + return py.builtin._getimself(function) @scopeproperty() def module(self): @@ -1142,12 +1231,7 @@ class FixtureRequest(FuncargnamesCompatAttr): self._addfinalizer(finalizer, scope=self.scope) def _addfinalizer(self, finalizer, scope): - if scope != "function" and hasattr(self, "param"): - # parametrized resources are sorted by param - # so we rather store finalizers per (argname, param) - colitem = (self.fixturename, self.param) - else: - colitem = self._getscopeitem(scope) + colitem = self._getscopeitem(scope) self._pyfuncitem.session._setupstate.addfinalizer( finalizer=finalizer, colitem=colitem) @@ -1157,7 +1241,7 @@ class FixtureRequest(FuncargnamesCompatAttr): on all function invocations. :arg marker: a :py:class:`_pytest.mark.MarkDecorator` object - created by a call to ``py.test.mark.NAME(...)``. + created by a call to ``pytest.mark.NAME(...)``. """ try: self.node.keywords[marker.markname] = marker @@ -1221,91 +1305,82 @@ class FixtureRequest(FuncargnamesCompatAttr): setup time, you may use this function to retrieve it inside a fixture function body. """ + return self._get_active_fixturedef(argname).cached_result[0] + + def _get_active_fixturedef(self, argname): try: - return self._funcargs[argname] + return self._fixturedefs[argname] except KeyError: - pass - try: - fixturedef = self._getnextfixturedef(argname) - except FixtureLookupError: - if argname == "request": - return self - raise - self._fixturestack.append(fixturedef) - try: + try: + fixturedef = self._getnextfixturedef(argname) + except FixtureLookupError: + if argname == "request": + class PseudoFixtureDef: + cached_result = (self, [0]) + return PseudoFixtureDef + raise result = self._getfuncargvalue(fixturedef) self._funcargs[argname] = result - return result - finally: - self._fixturestack.pop() + self._fixturedefs[argname] = fixturedef + return fixturedef + + def _get_fixturestack(self): + current = self + l = [] + while 1: + fixturedef = getattr(current, "_fixturedef", None) + if fixturedef is None: + l.reverse() + return l + l.append(fixturedef) + current = current._parent_request def _getfuncargvalue(self, fixturedef): - try: - return fixturedef.cached_result # set by fixturedef.execute() - except AttributeError: - pass - - # prepare request fixturename and param attributes before - # calling into fixture function + # prepare a subrequest object before calling fixture function + # (latter managed by fixturedef) argname = fixturedef.argname - node = self._pyfuncitem - mp = monkeypatch() - mp.setattr(self, 'fixturename', argname) - try: - param = node.callspec.getparam(argname) - except (AttributeError, ValueError): - pass - else: - mp.setattr(self, 'param', param, raising=False) - - # if a parametrize invocation set a scope it will override - # the static scope defined with the fixture function + funcitem = self._pyfuncitem scope = fixturedef.scope try: - paramscopenum = node.callspec._arg2scopenum[argname] - except (KeyError, AttributeError): - pass + param = funcitem.callspec.getparam(argname) + except (AttributeError, ValueError): + param = NOTSET + param_index = 0 else: - if paramscopenum != scopenum_subfunction: + # indices might not be set if old-style metafunc.addcall() was used + param_index = funcitem.callspec.indices.get(argname, 0) + # if a parametrize invocation set a scope it will override + # the static scope defined with the fixture function + paramscopenum = funcitem.callspec._arg2scopenum.get(argname) + if paramscopenum is not None: scope = scopes[paramscopenum] + subrequest = SubRequest(self, scope, param, param_index, fixturedef) + # check if a higher-level scoped fixture accesses a lower level one if scope is not None: __tracebackhide__ = True if scopemismatch(self.scope, scope): # try to report something helpful - lines = self._factorytraceback() + lines = subrequest._factorytraceback() raise ScopeMismatchError("You tried to access the %r scoped " - "funcarg %r with a %r scoped request object, " + "fixture %r with a %r scoped request object, " "involved factories\n%s" %( (scope, argname, self.scope, "\n".join(lines)))) __tracebackhide__ = False - mp.setattr(self, "scope", scope) - - # route request.addfinalizer to fixturedef - mp.setattr(self, "addfinalizer", fixturedef.addfinalizer) try: - # perform the fixture call - val = fixturedef.execute(request=self) + # call the fixture function + val = fixturedef.execute(request=subrequest) finally: - # if the fixture function failed it might still have - # registered finalizers so we can register - - # prepare finalization according to scope - # (XXX analyse exact finalizing mechanics / cleanup) + # if fixture function failed it might have registered finalizers self.session._setupstate.addfinalizer(fixturedef.finish, - self.node) - self._fixturemanager.addargfinalizer(fixturedef.finish, argname) - for subargname in fixturedef.argnames: # XXX all deps? - self._fixturemanager.addargfinalizer(fixturedef.finish, - subargname) - mp.undo() + subrequest.node) return val def _factorytraceback(self): lines = [] - for fixturedef in self._fixturestack: + for fixturedef in self._get_fixturestack(): factory = fixturedef.func fs, lineno = getfslineno(factory) p = self._pyfuncitem.session.fspath.bestrelpath(fs) @@ -1316,29 +1391,50 @@ class FixtureRequest(FuncargnamesCompatAttr): def _getscopeitem(self, scope): if scope == "function": + # this might also be a non-function Item despite its attribute name return self._pyfuncitem - elif scope == "session": - return self.session - elif scope == "class": - x = self._pyfuncitem.getparent(pytest.Class) - if x is not None: - return x - # fallback to function - return self._pyfuncitem - if scope == "module": - return self._pyfuncitem.getparent(pytest.Module) - raise ValueError("unknown finalization scope %r" %(scope,)) + node = get_scope_node(self._pyfuncitem, scope) + if node is None and scope == "class": + # fallback to function item itself + node = self._pyfuncitem + assert node + return node def __repr__(self): return "" %(self.node) + +class SubRequest(FixtureRequest): + """ a sub request for handling getting a fixture from a + test function/fixture. """ + def __init__(self, request, scope, param, param_index, fixturedef): + self._parent_request = request + self.fixturename = fixturedef.argname + if param is not NOTSET: + self.param = param + self.param_index = param_index + self.scope = scope + self._fixturedef = fixturedef + self.addfinalizer = fixturedef.addfinalizer + self._pyfuncitem = request._pyfuncitem + self._funcargs = request._funcargs + self._fixturedefs = request._fixturedefs + self._arg2fixturedefs = request._arg2fixturedefs + self._arg2index = request._arg2index + self.fixturenames = request.fixturenames + self._fixturemanager = request._fixturemanager + + def __repr__(self): + return "" % (self.fixturename, self._pyfuncitem) + + class ScopeMismatchError(Exception): """ A fixture function tries to use a different fixture function which which has a lower scope (e.g. a Session one calls a function one) """ -scopes = "session module class function subfunction".split() -scopenum_subfunction = scopes.index("subfunction") +scopes = "session module class function".split() +scopenum_function = scopes.index("function") def scopemismatch(currentscope, newscope): return scopes.index(newscope) > scopes.index(currentscope) @@ -1347,7 +1443,7 @@ class FixtureLookupError(LookupError): def __init__(self, argname, request, msg=None): self.argname = argname self.request = request - self.fixturestack = list(request._fixturestack) + self.fixturestack = request._get_fixturestack() self.msg = msg def formatrepr(self): @@ -1371,10 +1467,10 @@ class FixtureLookupError(LookupError): if msg is None: fm = self.request._fixturemanager - nodeid = self.request._parentid available = [] for name, fixturedef in fm._arg2fixturedefs.items(): - faclist = list(fm._matchfactories(fixturedef, self.request._parentid)) + parentid = self.request._pyfuncitem.parent.nodeid + faclist = list(fm._matchfactories(fixturedef, parentid)) if faclist: available.append(name) msg = "fixture %r not found" % (self.argname,) @@ -1449,7 +1545,8 @@ class FixtureManager: self._nodename2fixtureinfo = {} def getfixtureinfo(self, node, func, cls, funcargs=True): - key = (node, func.__name__) + # node is the "collection node" for "func" + key = (node, func) try: return self._nodename2fixtureinfo[key] except KeyError: @@ -1537,57 +1634,28 @@ class FixtureManager: if argname in arg2fixturedefs: continue fixturedefs = self.getfixturedefs(argname, parentid) - arg2fixturedefs[argname] = fixturedefs - if fixturedefs is not None: - for fixturedef in fixturedefs: - merge(fixturedef.argnames) + if fixturedefs: + arg2fixturedefs[argname] = fixturedefs + merge(fixturedefs[-1].argnames) return fixturenames_closure, arg2fixturedefs def pytest_generate_tests(self, metafunc): for argname in metafunc.fixturenames: - faclist = metafunc._arg2fixturedefs[argname] + faclist = metafunc._arg2fixturedefs.get(argname) if faclist is None: continue # will raise FixtureLookupError at setup time for fixturedef in faclist: if fixturedef.params is not None: - metafunc.parametrize(argname, fixturedef.params, indirect=True, - scope=fixturedef.scope) + metafunc.parametrize(argname, fixturedef.params, + indirect=True, scope=fixturedef.scope, + ids=fixturedef.ids) def pytest_collection_modifyitems(self, items): # separate parametrized setups - items[:] = parametrize_sorted(items, set(), {}, 0) + items[:] = reorder_items(items) - def pytest_runtest_teardown(self, item, nextitem): - # XXX teardown needs to be normalized for parametrized and - # no-parametrized functions - try: - cs1 = item.callspec - except AttributeError: - return - - # determine which fixtures are not needed anymore for the next test - keylist = [] - for name in cs1.params: - try: - if name in nextitem.callspec.params and \ - cs1.params[name] == nextitem.callspec.params[name]: - continue - except AttributeError: - pass - key = (-cs1._arg2scopenum[name], name, cs1.params[name]) - keylist.append(key) - - # sort by scope (function scope first, then higher ones) - keylist.sort() - for (scopenum, name, param) in keylist: - item.session._setupstate._callfinalizers((name, param)) - l = self._arg2finish.pop(name, None) - if l is not None: - for fin in reversed(l): - fin() - - def parsefactories(self, node_or_obj, nodeid=_dummy, unittest=False): - if nodeid is not _dummy: + def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False): + if nodeid is not NOTSET: holderobj = node_or_obj else: holderobj = node_or_obj.obj @@ -1616,17 +1684,18 @@ class FixtureManager: assert not name.startswith(self._argprefix) fixturedef = FixtureDef(self, nodeid, name, obj, marker.scope, marker.params, - unittest=unittest) + yieldctx=marker.yieldctx, + unittest=unittest, ids=marker.ids) faclist = self._arg2fixturedefs.setdefault(name, []) - if not fixturedef.has_location: - # All fixturedefs with no location are at the front + if fixturedef.has_location: + faclist.append(fixturedef) + else: + # fixturedefs with no location are at the front # so this inserts the current fixturedef after the # existing fixturedefs from external plugins but # before the fixturedefs provided in conftests. i = len([f for f in faclist if not f.has_location]) - else: - i = len(faclist) # append - faclist.insert(i, fixturedef) + faclist.insert(i, fixturedef) if marker.autouse: autousenames.append(name) if autousenames: @@ -1645,19 +1714,19 @@ class FixtureManager: if nodeid.startswith(fixturedef.baseid): yield fixturedef - def addargfinalizer(self, finalizer, argname): - l = self._arg2finish.setdefault(argname, []) - l.append(finalizer) - def removefinalizer(self, finalizer): - for l in self._arg2finish.values(): - try: - l.remove(finalizer) - except ValueError: - pass +def fail_fixturefunc(fixturefunc, msg): + fs, lineno = getfslineno(fixturefunc) + location = "%s:%s" % (fs, lineno+1) + source = py.code.Source(fixturefunc) + pytest.fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, + pytrace=False) -def call_fixture_func(fixturefunc, request, kwargs): - if is_generator(fixturefunc): +def call_fixture_func(fixturefunc, request, kwargs, yieldctx): + if yieldctx: + if not is_generator(fixturefunc): + fail_fixturefunc(fixturefunc, + msg="yield_fixture requires yield statement in function") iter = fixturefunc(**kwargs) next = getattr(iter, "__next__", None) if next is None: @@ -1669,20 +1738,22 @@ def call_fixture_func(fixturefunc, request, kwargs): except StopIteration: pass else: - fs, lineno = getfslineno(fixturefunc) - location = "%s:%s" % (fs, lineno+1) - pytest.fail( - "fixture function %s has more than one 'yield': \n%s" % - (fixturefunc.__name__, location), pytrace=False) + fail_fixturefunc(fixturefunc, + "yield_fixture function has more than one 'yield'") request.addfinalizer(teardown) else: + if is_generator(fixturefunc): + fail_fixturefunc(fixturefunc, + msg="pytest.fixture functions cannot use ``yield``. " + "Instead write and return an inner function/generator " + "and let the consumer call and iterate over it.") res = fixturefunc(**kwargs) return res class FixtureDef: """ A container for a factory definition. """ def __init__(self, fixturemanager, baseid, argname, func, scope, params, - unittest=False): + yieldctx, unittest=False, ids=None): self._fixturemanager = fixturemanager self.baseid = baseid or '' self.has_location = baseid is not None @@ -1693,7 +1764,9 @@ class FixtureDef: self.params = params startindex = unittest and 1 or None self.argnames = getfuncargnames(func, startindex=startindex) + self.yieldctx = yieldctx self.unittest = unittest + self.ids = ids self._finalizer = [] def addfinalizer(self, finalizer): @@ -1703,41 +1776,56 @@ class FixtureDef: while self._finalizer: func = self._finalizer.pop() func() - # check neccesity of next commented call - self._fixturemanager.removefinalizer(self.finish) - #print "finished", self try: del self.cached_result except AttributeError: pass def execute(self, request): + # get required arguments and register our own finish() + # with their finalization kwargs = {} - for newname in self.argnames: - kwargs[newname] = request.getfuncargvalue(newname) + for argname in self.argnames: + fixturedef = request._get_active_fixturedef(argname) + result, arg_cache_key = fixturedef.cached_result + kwargs[argname] = result + if argname != "request": + fixturedef.addfinalizer(self.finish) + + my_cache_key = request.param_index + cached_result = getattr(self, "cached_result", None) + if cached_result is not None: + #print argname, "Found cached_result", cached_result + #print argname, "param_index", param_index + result, cache_key = cached_result + if my_cache_key == cache_key: + #print request.fixturename, "CACHE HIT", repr(my_cache_key) + return result + #print request.fixturename, "CACHE MISS" + # we have a previous but differently parametrized fixture instance + # so we need to tear it down before creating a new one + self.finish() + assert not hasattr(self, "cached_result") + if self.unittest: result = self.func(request.instance, **kwargs) else: fixturefunc = self.func # the fixture function needs to be bound to the actual # request.instance so that code working with "self" behaves - # as expected. XXX request.instance should maybe return None - # instead of raising AttributeError - try: - if request.instance is not None: - fixturefunc = getimfunc(self.func) - if fixturefunc != self.func: - fixturefunc = fixturefunc.__get__(request.instance) - except AttributeError: - pass - result = call_fixture_func(fixturefunc, request, kwargs) - assert not hasattr(self, "cached_result") - self.cached_result = result + # as expected. + if request.instance is not None: + fixturefunc = getimfunc(self.func) + if fixturefunc != self.func: + fixturefunc = fixturefunc.__get__(request.instance) + result = call_fixture_func(fixturefunc, request, kwargs, + self.yieldctx) + self.cached_result = (result, my_cache_key) return result def __repr__(self): - return ("" % - (self.argname, self.scope, self.baseid, self.func.__module__)) + return ("" % + (self.argname, self.scope, self.baseid)) def getfuncargnames(function, startindex=None): # XXX merge with main.py's varnames @@ -1759,85 +1847,90 @@ def getfuncargnames(function, startindex=None): return tuple(argnames[startindex:]) # algorithm for sorting on a per-parametrized resource setup basis +# it is called for scopenum==0 (session) first and performs sorting +# down to the lower scopes such as to minimize number of "high scope" +# setups and teardowns -def parametrize_sorted(items, ignore, cache, scopenum): - if scopenum >= 3: +def reorder_items(items): + argkeys_cache = {} + for scopenum in range(0, scopenum_function): + argkeys_cache[scopenum] = d = {} + for item in items: + keys = set(get_parametrized_fixture_keys(item, scopenum)) + if keys: + d[item] = keys + return reorder_items_atscope(items, set(), argkeys_cache, 0) + +def reorder_items_atscope(items, ignore, argkeys_cache, scopenum): + if scopenum >= scopenum_function or len(items) < 3: return items + items_done = [] + while 1: + items_before, items_same, items_other, newignore = \ + slice_items(items, ignore, argkeys_cache[scopenum]) + items_before = reorder_items_atscope( + items_before, ignore, argkeys_cache,scopenum+1) + if items_same is None: + # nothing to reorder in this scope + assert items_other is None + return items_done + items_before + items_done.extend(items_before) + items = items_same + items_other + ignore = newignore - # we pick the first item which has a arg/param combo in the - # requested scope and sort other items with the same combo - # into "newitems" which then is a list of all items using this - # arg/param. - similar_items = [] - other_items = [] - slicing_argparam = None - slicing_index = 0 - for item in items: - argparamlist = getfuncargparams(item, ignore, scopenum, cache) - if slicing_argparam is None and argparamlist: - slicing_argparam = argparamlist[0] - slicing_index = len(other_items) - if slicing_argparam in argparamlist: - similar_items.append(item) - else: - other_items.append(item) +def slice_items(items, ignore, scoped_argkeys_cache): + # we pick the first item which uses a fixture instance in the + # requested scope and which we haven't seen yet. We slice the input + # items list into a list of items_nomatch, items_same and + # items_other + if scoped_argkeys_cache: # do we need to do work at all? + it = iter(items) + # first find a slicing key + for i, item in enumerate(it): + argkeys = scoped_argkeys_cache.get(item) + if argkeys is not None: + argkeys = argkeys.difference(ignore) + if argkeys: # found a slicing key + slicing_argkey = argkeys.pop() + items_before = items[:i] + items_same = [item] + items_other = [] + # now slice the remainder of the list + for item in it: + argkeys = scoped_argkeys_cache.get(item) + if argkeys and slicing_argkey in argkeys and \ + slicing_argkey not in ignore: + items_same.append(item) + else: + items_other.append(item) + newignore = ignore.copy() + newignore.add(slicing_argkey) + return (items_before, items_same, items_other, newignore) + return items, None, None, None - if (len(similar_items) + slicing_index) > 1: - newignore = ignore.copy() - newignore.add(slicing_argparam) - part2 = parametrize_sorted( - similar_items + other_items[slicing_index:], - newignore, cache, scopenum) - part1 = parametrize_sorted( - other_items[:slicing_index], newignore, - cache, scopenum+1) - return part1 + part2 - else: - other_items = parametrize_sorted(other_items, ignore, cache, scopenum+1) - return other_items + similar_items - -def getfuncargparams(item, ignore, scopenum, cache): - """ return list of (arg,param) tuple, sorted by broader scope first. """ - assert scopenum < 3 # function +def get_parametrized_fixture_keys(item, scopenum): + """ return list of keys for all parametrized arguments which match + the specified scope. """ + assert scopenum < scopenum_function # function try: cs = item.callspec except AttributeError: - return [] - if scopenum == 0: - argparams = [x for x in cs.params.items() if x not in ignore - and cs._arg2scopenum[x[0]] == scopenum] - elif scopenum == 1: # module - argparams = [] - for argname, param in cs.params.items(): - if cs._arg2scopenum[argname] == scopenum: - key = (argname, param, item.fspath) - if key in ignore: - continue - argparams.append(key) - elif scopenum == 2: # class - argparams = [] - for argname, param in cs.params.items(): - if cs._arg2scopenum[argname] == scopenum: - l = cache.setdefault(item.fspath, []) - try: - i = l.index(item.cls) - except ValueError: - i = len(l) - l.append(item.cls) - key = (argname, param, item.fspath, i) - if key in ignore: - continue - argparams.append(key) - #elif scopenum == 3: - # argparams = [] - # for argname, param in cs.params.items(): - # if cs._arg2scopenum[argname] == scopenum: - # key = (argname, param, getfslineno(item.obj)) - # if key in ignore: - # continue - # argparams.append(key) - return argparams + pass + else: + # cs.indictes.items() is random order of argnames but + # then again different functions (items) can change order of + # arguments so it doesn't matter much probably + for argname, param_index in cs.indices.items(): + if cs._arg2scopenum[argname] != scopenum: + continue + if scopenum == 0: # session + key = (argname, param_index) + elif scopenum == 1: # module + key = (argname, param_index, item.fspath) + elif scopenum == 2: # class + key = (argname, param_index, item.fspath, item.cls) + yield key def xunitsetup(obj, name): @@ -1857,3 +1950,15 @@ def getfixturemarker(obj): # we don't expect them to be fixture functions return None +scopename2class = { + 'class': Class, + 'module': Module, + 'function': pytest.Item, +} +def get_scope_node(node, scope): + cls = scopename2class.get(scope) + if cls is None: + if scope == "session": + return node.session + raise ValueError("unknown scope") + return node.getparent(cls) diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index 5e5a92aa6..987ff8f97 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -1,14 +1,14 @@ """ recording warnings during test function execution. """ import py -import sys, os +import sys def pytest_funcarg__recwarn(request): """Return a WarningsRecorder instance that provides these methods: * ``pop(category=None)``: return last warning matching the category. * ``clear()``: clear list of warnings - + See http://docs.python.org/library/warnings.html for information on warning categories. """ diff --git a/_pytest/resultlog.py b/_pytest/resultlog.py index d7f37a53c..0c100552f 100644 --- a/_pytest/resultlog.py +++ b/_pytest/resultlog.py @@ -6,7 +6,7 @@ import py def pytest_addoption(parser): group = parser.getgroup("terminal reporting", "resultlog plugin options") - group.addoption('--resultlog', '--result-log', action="store", + group.addoption('--resultlog', '--result-log', action="store", metavar="path", default=None, help="path for machine-readable result log.") @@ -85,7 +85,7 @@ class ResultLog(object): if not report.passed: if report.failed: code = "F" - longrepr = str(report.longrepr.reprcrash) + longrepr = str(report.longrepr) else: assert report.skipped code = "S" diff --git a/_pytest/runner.py b/_pytest/runner.py index 9abc9a7fe..794c0eed1 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -1,6 +1,8 @@ """ basic collect and runtest protocol implementations """ -import py, sys +import py +import pytest +import sys from time import time from py._code.code import TerminalRepr @@ -193,11 +195,10 @@ def pytest_runtest_makereport(item, call): outcome = "passed" longrepr = None else: - excinfo = call.excinfo if not isinstance(excinfo, py.code.ExceptionInfo): outcome = "failed" longrepr = excinfo - elif excinfo.errisinstance(py.test.skip.Exception): + elif excinfo.errisinstance(pytest.skip.Exception): outcome = "skipped" r = excinfo._getreprcrash() longrepr = (str(r.path), r.lineno, r.message) @@ -328,9 +329,18 @@ class SetupState(object): def _callfinalizers(self, colitem): finalizers = self._finalizers.pop(colitem, None) + exc = None while finalizers: fin = finalizers.pop() - fin() + try: + fin() + except Exception: + # XXX Only first exception will be seen by user, + # ideally all should be reported. + if exc is None: + exc = sys.exc_info() + if exc: + py.builtin._reraise(*exc) def _teardown_with_finalization(self, colitem): self._callfinalizers(colitem) @@ -410,7 +420,7 @@ class Skipped(OutcomeException): __module__ = 'builtins' class Failed(OutcomeException): - """ raised from an explicit call to py.test.fail() """ + """ raised from an explicit call to pytest.fail() """ __module__ = 'builtins' class Exit(KeyboardInterrupt): @@ -430,7 +440,7 @@ exit.Exception = Exit def skip(msg=""): """ skip an executing test with the given message. Note: it's usually - better to use the py.test.mark.skipif marker to declare a test to be + better to use the pytest.mark.skipif marker to declare a test to be skipped under certain conditions like mismatching platforms or dependencies. See the pytest_skipping plugin for details. """ @@ -450,25 +460,25 @@ fail.Exception = Failed def importorskip(modname, minversion=None): - """ return imported module if it has a higher __version__ than the - optionally specified 'minversion' - otherwise call py.test.skip() - with a message detailing the mismatch. + """ return imported module if it has at least "minversion" as its + __version__ attribute. If no minversion is specified the a skip + is only triggered if the module can not be imported. + Note that version comparison only works with simple version strings + like "1.2.3" but not "1.2.3.dev1" or others. """ __tracebackhide__ = True compile(modname, '', 'eval') # to catch syntaxerrors try: __import__(modname) except ImportError: - py.test.skip("could not import %r" %(modname,)) + skip("could not import %r" %(modname,)) mod = sys.modules[modname] if minversion is None: return mod verattr = getattr(mod, '__version__', None) - if isinstance(minversion, str): - minver = minversion.split(".") - else: - minver = list(minversion) - if verattr is None or verattr.split(".") < minver: - py.test.skip("module %r has __version__ %r, required is: %r" %( - modname, verattr, minversion)) + def intver(verstring): + return [int(x) for x in verstring.split(".")] + if verattr is None or intver(verattr) < intver(minversion): + skip("module %r has __version__ %r, required is: %r" %( + modname, verattr, minversion)) return mod diff --git a/_pytest/skipping.py b/_pytest/skipping.py index d691d9fd8..a370b64e4 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -10,6 +10,14 @@ def pytest_addoption(parser): help="run tests even if they are marked xfail") def pytest_configure(config): + if config.option.runxfail: + old = pytest.xfail + config._cleanup.append(lambda: setattr(pytest, "xfail", old)) + def nop(*args, **kwargs): + pass + nop.Exception = XFailed + setattr(pytest, "xfail", nop) + config.addinivalue_line("markers", "skipif(condition): skip the given test function if eval(condition) " "results in a True value. Evaluation happens within the " @@ -29,7 +37,7 @@ def pytest_namespace(): return dict(xfail=xfail) class XFailed(pytest.fail.Exception): - """ raised from an explicit call to py.test.xfail() """ + """ raised from an explicit call to pytest.xfail() """ def xfail(reason=""): """ xfail an executing test or setup functions with the given reason.""" @@ -121,7 +129,7 @@ def pytest_runtest_setup(item): return evalskip = MarkEvaluator(item, 'skipif') if evalskip.istrue(): - py.test.skip(evalskip.getexplanation()) + pytest.skip(evalskip.getexplanation()) item._evalxfail = MarkEvaluator(item, 'xfail') check_xfail_no_run(item) @@ -133,7 +141,7 @@ def check_xfail_no_run(item): evalxfail = item._evalxfail if evalxfail.istrue(): if not evalxfail.get('run', True): - py.test.xfail("[NOTRUN] " + evalxfail.getexplanation()) + pytest.xfail("[NOTRUN] " + evalxfail.getexplanation()) def pytest_runtest_makereport(__multicall__, item, call): if not isinstance(item, pytest.Function): @@ -142,16 +150,16 @@ def pytest_runtest_makereport(__multicall__, item, call): if hasattr(item, '_unexpectedsuccess'): rep = __multicall__.execute() if rep.when == "call": - # we need to translate into how py.test encodes xpass + # we need to translate into how pytest encodes xpass rep.wasxfail = "reason: " + repr(item._unexpectedsuccess) rep.outcome = "failed" return rep if not (call.excinfo and - call.excinfo.errisinstance(py.test.xfail.Exception)): + call.excinfo.errisinstance(pytest.xfail.Exception)): evalxfail = getattr(item, '_evalxfail', None) if not evalxfail: return - if call.excinfo and call.excinfo.errisinstance(py.test.xfail.Exception): + if call.excinfo and call.excinfo.errisinstance(pytest.xfail.Exception): if not item.config.getvalue("runxfail"): rep = __multicall__.execute() rep.wasxfail = "reason: " + call.excinfo.value.msg @@ -209,7 +217,6 @@ def pytest_terminal_summary(terminalreporter): tr._tw.line(line) def show_simple(terminalreporter, lines, stat, format): - tw = terminalreporter._tw failed = terminalreporter.stats.get(stat) if failed: for rep in failed: diff --git a/_pytest/standalonetemplate.py b/_pytest/standalonetemplate.py index 6b3c1b51f..b67bf20f3 100755 --- a/_pytest/standalonetemplate.py +++ b/_pytest/standalonetemplate.py @@ -6,7 +6,6 @@ sources = """ import sys import base64 import zlib -import imp class DictImporter(object): def __init__(self, sources): @@ -40,7 +39,7 @@ class DictImporter(object): if is_pkg: module.__path__ = [fullname] - do_exec(co, module.__dict__) + do_exec(co, module.__dict__) # noqa return sys.modules[fullname] def get_source(self, name): @@ -64,4 +63,4 @@ if __name__ == "__main__": sys.meta_path.insert(0, importer) entry = "@ENTRY@" - do_exec(entry, locals()) + do_exec(entry, locals()) # noqa diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 2fbe9c6b3..1b9f8905f 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -5,7 +5,6 @@ This is a good source for looking at the various reporting hooks. import pytest import py import sys -import os def pytest_addoption(parser): group = parser.getgroup("terminal reporting", "reporting", after="general") @@ -30,24 +29,14 @@ def pytest_addoption(parser): group._addoption('--fulltrace', '--full-trace', action="store_true", default=False, help="don't cut any tracebacks (default is to cut).") + group._addoption('--color', metavar="color", + action="store", dest="color", default='auto', + choices=['yes', 'no', 'auto'], + help="color terminal output (yes/no/auto).") def pytest_configure(config): config.option.verbose -= config.option.quiet - # we try hard to make printing resilient against - # later changes on FD level. - stdout = py.std.sys.stdout - if hasattr(os, 'dup') and hasattr(stdout, 'fileno'): - try: - newstdout = py.io.dupfile(stdout, buffering=1, - encoding=stdout.encoding) - except ValueError: - pass - else: - config._cleanup.append(lambda: newstdout.close()) - assert stdout.encoding == newstdout.encoding - stdout = newstdout - - reporter = TerminalReporter(config, stdout) + reporter = TerminalReporter(config, sys.stdout) config.pluginmanager.register(reporter, 'terminalreporter') if config.option.debug or config.option.traceconfig: def mywriter(tags, args): @@ -99,7 +88,11 @@ class TerminalReporter: self.startdir = self.curdir = py.path.local() if file is None: file = py.std.sys.stdout - self._tw = py.io.TerminalWriter(file) + self._tw = self.writer = py.io.TerminalWriter(file) + if self.config.option.color == 'yes': + self._tw.hasmarkup = True + if self.config.option.color == 'no': + self._tw.hasmarkup = False self.currentfspath = None self.reportchars = getreportopt(config) self.hasmarkup = self._tw.hasmarkup @@ -147,6 +140,12 @@ class TerminalReporter: self.ensure_newline() self._tw.sep(sep, title, **markup) + def section(self, title, sep="=", **kw): + self._tw.sep(sep, title, **kw) + + def line(self, msg, **kw): + self._tw.line(msg, **kw) + def pytest_internalerror(self, excrepr): for line in str(excrepr).split("\n"): self.write_line("INTERNALERROR> " + line) @@ -178,6 +177,7 @@ class TerminalReporter: res = self.config.hook.pytest_report_teststatus(report=rep) cat, letter, word = res self.stats.setdefault(cat, []).append(rep) + self._tests_ran = True if not letter and not word: # probably passed setup/teardown return @@ -259,7 +259,7 @@ class TerminalReporter: if hasattr(sys, 'pypy_version_info'): verinfo = ".".join(map(str, sys.pypy_version_info[:3])) msg += "[pypy-%s-%s]" % (verinfo, sys.pypy_version_info[3]) - msg += " -- pytest-%s" % (py.test.__version__) + msg += " -- py-%s -- pytest-%s" % (py.__version__, pytest.__version__) if self.verbosity > 0 or self.config.option.debug or \ getattr(self.config.option, 'pastebin', None): msg += " -- " + str(sys.executable) @@ -334,6 +334,7 @@ class TerminalReporter: if exitstatus in (0, 1, 2, 4): self.summary_errors() self.summary_failures() + self.summary_hints() self.config.hook.pytest_terminal_summary(terminalreporter=self) if exitstatus == 2: self._report_keyboardinterrupt() @@ -399,6 +400,11 @@ class TerminalReporter: l.append(x) return l + def summary_hints(self): + if self.config.option.traceconfig: + for hint in self.config.pluginmanager._hints: + self._tw.line("hint: %s" % hint) + def summary_failures(self): if self.config.option.tbstyle != "no": reports = self.getreports('failed') diff --git a/_pytest/tmpdir.py b/_pytest/tmpdir.py index 3907c92b7..8eb0cdec0 100644 --- a/_pytest/tmpdir.py +++ b/_pytest/tmpdir.py @@ -64,5 +64,8 @@ def tmpdir(request): """ name = request.node.name name = py.std.re.sub("[\W]", "_", name) + MAXVAL = 30 + if len(name) > MAXVAL: + name = name[:MAXVAL] x = request.config._tmpdirhandler.mktemp(name, numbered=True) return x diff --git a/_pytest/unittest.py b/_pytest/unittest.py index f283d0348..e6fefbd43 100644 --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -1,6 +1,6 @@ """ discovery and running of std-library "unittest" style tests. """ import pytest, py -import sys, pdb +import sys # for transfering markers from _pytest.python import transfer_markers @@ -50,8 +50,6 @@ class UnitTestCase(pytest.Class): x = getattr(self.obj, name) funcobj = getattr(x, 'im_func', x) transfer_markers(funcobj, cls, module) - if hasattr(funcobj, 'todo'): - pytest.mark.xfail(reason=str(funcobj.todo))(funcobj) yield TestCaseFunction(name, parent=self) foundsomething = True @@ -70,10 +68,6 @@ class TestCaseFunction(pytest.Function): def setup(self): self._testcase = self.parent.obj(self.name) self._obj = getattr(self._testcase, self.name) - if hasattr(self._testcase, 'skip'): - pytest.skip(self._testcase.skip) - if hasattr(self._obj, 'skip'): - pytest.skip(self._obj.skip) if hasattr(self._testcase, 'setup_method'): self._testcase.setup_method(self._obj) if hasattr(self, "_request"): @@ -150,7 +144,10 @@ def pytest_runtest_makereport(item, call): if isinstance(item, TestCaseFunction): if item._excinfo: call.excinfo = item._excinfo.pop(0) - del call.result + try: + del call.result + except AttributeError: + pass # twisted trial support def pytest_runtest_protocol(item, __multicall__): diff --git a/bench/bench.py b/bench/bench.py index c9644ef61..bb95f86fc 100644 --- a/bench/bench.py +++ b/bench/bench.py @@ -1,10 +1,12 @@ +import sys if __name__ == '__main__': import cProfile - import py + import pytest import pstats - stats = cProfile.run('py.test.cmdline.main(["empty.py", ])', 'prof') + script = sys.argv[1] if len(sys.argv) > 1 else "empty.py" + stats = cProfile.run('pytest.cmdline.main([%r])' % script, 'prof') p = pstats.Stats("prof") p.strip_dirs() p.sort_stats('cumulative') - print(p.print_stats(30)) + print(p.print_stats(250)) diff --git a/bench/manyparam.py b/bench/manyparam.py new file mode 100644 index 000000000..d2bca0e8a --- /dev/null +++ b/bench/manyparam.py @@ -0,0 +1,12 @@ + +import pytest + +@pytest.fixture(scope='module', params=range(966)) +def foo(request): + return request.param + +def test_it(foo): + pass +def test_it2(foo): + pass + diff --git a/bench/skip.py b/bench/skip.py new file mode 100644 index 000000000..960b30864 --- /dev/null +++ b/bench/skip.py @@ -0,0 +1,10 @@ + +import pytest + + +SKIP = True + +@pytest.mark.parametrize("x", xrange(5000)) +def test_foo(x): + if SKIP: + pytest.skip("heh") diff --git a/doc/en/Makefile b/doc/en/Makefile index 7a05008ac..17fab6115 100644 --- a/doc/en/Makefile +++ b/doc/en/Makefile @@ -12,7 +12,7 @@ PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -SITETARGET=dev +SITETARGET=latest .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest diff --git a/doc/en/_static/sphinxdoc.css b/doc/en/_static/sphinxdoc.css deleted file mode 100644 index ab8ab5dce..000000000 --- a/doc/en/_static/sphinxdoc.css +++ /dev/null @@ -1,339 +0,0 @@ -/* - * sphinxdoc.css_t - * ~~~~~~~~~~~~~~~ - * - * Sphinx stylesheet -- sphinxdoc theme. Originally created by - * Armin Ronacher for Werkzeug. - * - * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', - 'Verdana', sans-serif; - font-size: 1.1em; - letter-spacing: -0.01em; - line-height: 150%; - text-align: center; - background-color: #BFD1D4; - color: black; - padding: 0; - border: 1px solid #aaa; - - margin: 0px 80px 0px 80px; - min-width: 740px; -} - -div.document { - background-color: white; - text-align: left; - background-image: url(contents.png); - background-repeat: repeat-x; -} - -div.bodywrapper { - margin: 0 240px 0 0; - border-right: 1px solid #ccc; -} - -div.body { - margin: 0; - padding: 0.5em 20px 20px 20px; -} - -div.related { - font-size: 0.8em; -} - -div.related ul { - background-image: url(navigation.png); - height: 2em; - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; -} - -div.related ul li { - margin: 0; - padding: 0; - height: 2em; - float: left; -} - -div.related ul li.right { - float: right; - margin-right: 5px; -} - -div.related ul li a { - margin: 0; - padding: 0 5px 0 5px; - line-height: 1.75em; - color: #EE9816; -} - -div.related ul li a:hover { - color: #3CA8E7; -} - -div.sphinxsidebarwrapper { - padding: 0; -} - -div.sphinxsidebar { - margin: 0; - padding: 0.5em 15px 15px 0; - width: 210px; - float: right; - font-size: 1em; - text-align: left; -} - -div.sphinxsidebar h3, div.sphinxsidebar h4 { - margin: 1em 0 0.5em 0; - font-size: 1em; - padding: 0.1em 0 0.1em 0.5em; - color: white; - border: 1px solid #86989B; - background-color: #AFC1C4; -} - -div.sphinxsidebar h3 a { - color: white; -} - -div.sphinxsidebar ul { - padding-left: 1.5em; - margin-top: 7px; - padding: 0; - line-height: 130%; -} - -div.sphinxsidebar ul ul { - margin-left: 20px; -} - -div.footer { - background-color: #E3EFF1; - color: #86989B; - padding: 3px 8px 3px 0; - clear: both; - font-size: 0.8em; - text-align: right; -} - -div.footer a { - color: #86989B; - text-decoration: underline; -} - -/* -- body styles ----------------------------------------------------------- */ - -p { - margin: 0.8em 0 0.5em 0; -} - -a { - color: #CA7900; - text-decoration: none; -} - -a:hover { - color: #2491CF; -} - -div.body a { - text-decoration: underline; -} - -h1 { - margin: 0; - padding: 0.7em 0 0.3em 0; - font-size: 1.5em; - color: #11557C; -} - -h2 { - margin: 1.3em 0 0.2em 0; - font-size: 1.35em; - padding: 0; -} - -h3 { - margin: 1em 0 -0.3em 0; - font-size: 1.2em; -} - -div.body h1 a, div.body h2 a, div.body h3 a, div.body h4 a, div.body h5 a, div.body h6 a { - color: black!important; -} - -h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor { - display: none; - margin: 0 0 0 0.3em; - padding: 0 0.2em 0 0.2em; - color: #aaa!important; -} - -h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, -h5:hover a.anchor, h6:hover a.anchor { - display: inline; -} - -h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover, -h5 a.anchor:hover, h6 a.anchor:hover { - color: #777; - background-color: #eee; -} - -a.headerlink { - color: #c60f0f!important; - font-size: 1em; - margin-left: 6px; - padding: 0 4px 0 4px; - text-decoration: none!important; -} - -a.headerlink:hover { - background-color: #ccc; - color: white!important; -} - -cite, code, tt { - font-family: 'Consolas', 'Deja Vu Sans Mono', - 'Bitstream Vera Sans Mono', monospace; - font-size: 0.95em; - letter-spacing: 0.01em; -} - -tt { - background-color: #f2f2f2; - border-bottom: 1px solid #ddd; - color: #333; -} - -tt.descname, tt.descclassname, tt.xref { - border: 0; -} - -hr { - border: 1px solid #abc; - margin: 2em; -} - -a tt { - border: 0; - color: #CA7900; -} - -a tt:hover { - color: #2491CF; -} - -pre { - font-family: 'Consolas', 'Deja Vu Sans Mono', - 'Bitstream Vera Sans Mono', monospace; - font-size: 0.95em; - letter-spacing: 0.015em; - line-height: 120%; - padding: 0.5em; - border: 1px solid #ccc; - background-color: #f8f8f8; -} - -pre a { - color: inherit; - text-decoration: underline; -} - -td.linenos pre { - padding: 0.5em 0; -} - -div.quotebar { - background-color: #f8f8f8; - max-width: 250px; - float: right; - padding: 2px 7px; - border: 1px solid #ccc; -} - -div.topic { - background-color: #f8f8f8; -} - -table { - border-collapse: collapse; - margin: 0 -0.5em 0 -0.5em; -} - -table td, table th { - padding: 0.2em 0.5em 0.2em 0.5em; -} - -div.admonition, div.warning { - font-size: 0.9em; - margin: 1em 0 1em 0; - border: 1px solid #86989B; - background-color: #f7f7f7; - padding: 0; -} - -div.admonition p, div.warning p { - margin: 0.5em 1em 0.5em 1em; - padding: 0; -} - -div.admonition pre, div.warning pre { - margin: 0.4em 1em 0.4em 1em; -} - -div.admonition p.admonition-title, -div.warning p.admonition-title { - margin: 0; - padding: 0.1em 0 0.1em 0.5em; - color: white; - border-bottom: 1px solid #86989B; - font-weight: bold; - background-color: #AFC1C4; -} - -div.warning { - border: 1px solid #940000; -} - -div.warning p.admonition-title { - background-color: #CF0000; - border-bottom-color: #940000; -} - -div.admonition ul, div.admonition ol, -div.warning ul, div.warning ol { - margin: 0.1em 0.5em 0.5em 3em; - padding: 0; -} - -div.versioninfo { - margin: 1em 0 0 0; - border: 1px solid #ccc; - background-color: #DDEAF0; - padding: 8px; - line-height: 1.3em; - font-size: 0.9em; -} - -.viewcode-back { - font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', - 'Verdana', sans-serif; -} - -div.viewcode-block:target { - background-color: #f4debf; - border-top: 1px solid #ac9; - border-bottom: 1px solid #ac9; -} diff --git a/doc/en/_templates/globaltoc.html b/doc/en/_templates/globaltoc.html new file mode 100644 index 000000000..f764729fd --- /dev/null +++ b/doc/en/_templates/globaltoc.html @@ -0,0 +1,17 @@ +

{{ _('Table Of Contents') }}

+ + + +{%- if display_toc %} +
+ {{ toc }} +{%- endif %} diff --git a/doc/en/_templates/indexsidebar.html b/doc/en/_templates/indexsidebar.html deleted file mode 100644 index 13d69cbd5..000000000 --- a/doc/en/_templates/indexsidebar.html +++ /dev/null @@ -1,22 +0,0 @@ -

Download

-{% if version.endswith('(hg)') %} -

This documentation is for version {{ version }}, which is - not released yet.

-

You can use it from the - Bitbucket Repo or look for - released versions in the Python - Package Index.

-{% else %} -

{{ release }} release -[Changelog]

-

-pytest/PyPI -

-
easy_install pytest
-
pip install pytest
-{% endif %} - -

Questions? Suggestions?

- -

contact channels -

diff --git a/doc/en/_templates/layout.html b/doc/en/_templates/layout.html index daa020cea..5ec94fd7e 100644 --- a/doc/en/_templates/layout.html +++ b/doc/en/_templates/layout.html @@ -1,16 +1,5 @@ {% extends "!layout.html" %} -{% block relbaritems %} -{{ super() }} - - - - - -{% endblock %} - {% block footer %} {{ super() }} - {% endblock %} diff --git a/doc/en/_templates/links.html b/doc/en/_templates/links.html new file mode 100644 index 000000000..0b7f87e7b --- /dev/null +++ b/doc/en/_templates/links.html @@ -0,0 +1,10 @@ +

Useful Links

+ + diff --git a/doc/en/_templates/localtoc.html b/doc/en/_templates/localtoc.html deleted file mode 100644 index 25d56cad0..000000000 --- a/doc/en/_templates/localtoc.html +++ /dev/null @@ -1,39 +0,0 @@ - -{%- if pagename != "search" %} - - -{%- endif %} - -

quicklinks

-
- - -
- home - - TOC/contents -
- install - - changelog -
- examples - - customize -
- issues[bb] - - contact -
- Talks/Posts -
-
-{% extends "basic/localtoc.html" %} - diff --git a/doc/en/_templates/searchbox.html b/doc/en/_templates/searchbox.html deleted file mode 100644 index e69de29bb..000000000 diff --git a/doc/en/_templates/sidebarintro.html b/doc/en/_templates/sidebarintro.html new file mode 100644 index 000000000..ae860c172 --- /dev/null +++ b/doc/en/_templates/sidebarintro.html @@ -0,0 +1,5 @@ +

About pytest

+

+ pytest is a mature full-featured Python testing tool that helps + you write better programs. +

diff --git a/doc/en/_themes/.gitignore b/doc/en/_themes/.gitignore new file mode 100644 index 000000000..66b6e4c2f --- /dev/null +++ b/doc/en/_themes/.gitignore @@ -0,0 +1,3 @@ +*.pyc +*.pyo +.DS_Store diff --git a/doc/en/_themes/LICENSE b/doc/en/_themes/LICENSE new file mode 100644 index 000000000..8daab7ee6 --- /dev/null +++ b/doc/en/_themes/LICENSE @@ -0,0 +1,37 @@ +Copyright (c) 2010 by Armin Ronacher. + +Some rights reserved. + +Redistribution and use in source and binary forms of the theme, with or +without modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +* The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +We kindly ask you to only use these themes in an unmodified manner just +for Flask and Flask-related products, not for unrelated projects. If you +like the visual style and want to use it for your own projects, please +consider making some larger changes to the themes (such as changing +font faces, sizes, colors or margins). + +THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/doc/en/_themes/README b/doc/en/_themes/README new file mode 100644 index 000000000..b3292bdff --- /dev/null +++ b/doc/en/_themes/README @@ -0,0 +1,31 @@ +Flask Sphinx Styles +=================== + +This repository contains sphinx styles for Flask and Flask related +projects. To use this style in your Sphinx documentation, follow +this guide: + +1. put this folder as _themes into your docs folder. Alternatively + you can also use git submodules to check out the contents there. +2. add this to your conf.py: + + sys.path.append(os.path.abspath('_themes')) + html_theme_path = ['_themes'] + html_theme = 'flask' + +The following themes exist: + +- 'flask' - the standard flask documentation theme for large + projects +- 'flask_small' - small one-page theme. Intended to be used by + very small addon libraries for flask. + +The following options exist for the flask_small theme: + + [options] + index_logo = '' filename of a picture in _static + to be used as replacement for the + h1 in the index.rst file. + index_logo_height = 120px height of the index logo + github_fork = '' repository name on github for the + "fork me" badge diff --git a/doc/en/_themes/flask/layout.html b/doc/en/_themes/flask/layout.html new file mode 100644 index 000000000..19c43fbbe --- /dev/null +++ b/doc/en/_themes/flask/layout.html @@ -0,0 +1,24 @@ +{%- extends "basic/layout.html" %} +{%- block extrahead %} + {{ super() }} + {% if theme_touch_icon %} + + {% endif %} + +{% endblock %} +{%- block relbar2 %}{% endblock %} +{% block header %} + {{ super() }} + {% if pagename == 'index' %} +
+ {% endif %} +{% endblock %} +{%- block footer %} + + {% if pagename == 'index' %} +
+ {% endif %} +{%- endblock %} diff --git a/doc/en/_themes/flask/relations.html b/doc/en/_themes/flask/relations.html new file mode 100644 index 000000000..3bbcde85b --- /dev/null +++ b/doc/en/_themes/flask/relations.html @@ -0,0 +1,19 @@ +

Related Topics

+ diff --git a/doc/en/_themes/flask/static/flasky.css_t b/doc/en/_themes/flask/static/flasky.css_t new file mode 100644 index 000000000..b3f39fd92 --- /dev/null +++ b/doc/en/_themes/flask/static/flasky.css_t @@ -0,0 +1,555 @@ +/* + * flasky.css_t + * ~~~~~~~~~~~~ + * + * :copyright: Copyright 2010 by Armin Ronacher. + * :license: Flask Design License, see LICENSE for details. + */ + +{% set page_width = '1020px' %} +{% set sidebar_width = '220px' %} +{% set link_color = '#490' %} +{% set link_hover_color = '#9c0' %} +{% set base_font = 'sans-serif' %} +{% set header_font = 'sans-serif' %} + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: {{ base_font }}; + font-size: 17px; + background-color: white; + color: #000; + margin: 0; + padding: 0; +} + +div.document { + width: {{ page_width }}; + margin: 30px auto 0 auto; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 {{ sidebar_width }}; +} + +div.sphinxsidebar { + width: {{ sidebar_width }}; +} + +hr { + border: 0; + border-top: 1px solid #B1B4B6; +} + +div.body { + background-color: #ffffff; + color: #3E4349; + padding: 0 30px 0 30px; +} + +img.floatingflask { + padding: 0 0 10px 10px; + float: right; +} + +div.footer { + width: {{ page_width }}; + margin: 20px auto 30px auto; + font-size: 14px; + color: #888; + text-align: right; +} + +div.footer a { + color: #888; +} + +div.related { + display: none; +} + +div.sphinxsidebar a { + color: #444; + text-decoration: none; + border-bottom: 1px dotted #999; +} + +div.sphinxsidebar a:hover { + border-bottom: 1px solid #999; +} + +div.sphinxsidebar { + font-size: 14px; + line-height: 1.5; +} + +div.sphinxsidebarwrapper { + padding: 18px 10px; +} + +div.sphinxsidebarwrapper p.logo { + padding: 0 0 20px 0; + margin: 0; + text-align: center; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: {{ header_font }}; + color: #444; + font-size: 24px; + font-weight: normal; + margin: 0 0 5px 0; + padding: 0; +} + +div.sphinxsidebar h4 { + font-size: 20px; +} + +div.sphinxsidebar h3 a { + color: #444; +} + +div.sphinxsidebar p.logo a, +div.sphinxsidebar h3 a, +div.sphinxsidebar p.logo a:hover, +div.sphinxsidebar h3 a:hover { + border: none; +} + +div.sphinxsidebar p { + color: #555; + margin: 10px 0; +} + +div.sphinxsidebar ul { + margin: 10px 0; + padding: 0; + color: #000; +} + +div.sphinxsidebar input { + border: 1px solid #ccc; + font-family: {{ base_font }}; + font-size: 1em; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: {{ link_color }}; + text-decoration: underline; +} + +a:hover { + color: {{ link_hover_color }}; + text-decoration: underline; +} + +a.reference.internal em { + font-style: normal; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: {{ header_font }}; + font-weight: normal; + margin: 30px 0px 10px 0px; + padding: 0; +} + +{% if theme_index_logo %} +div.indexwrapper h1 { + text-indent: -999999px; + background: url({{ theme_index_logo }}) no-repeat center center; + height: {{ theme_index_logo_height }}; +} +{% else %} +div.indexwrapper div.body h1 { + font-size: 200%; +} +{% endif %} +div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } +div.body h2 { font-size: 180%; } +div.body h3 { font-size: 150%; } +div.body h4 { font-size: 130%; } +div.body h5 { font-size: 100%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #ddd; + padding: 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + color: #444; + background: #eaeaea; +} + +div.body p, div.body dd, div.body li { + line-height: 1.4em; +} + +div.admonition { + background: #fafafa; + margin: 20px -30px; + padding: 10px 30px; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; +} + +div.admonition tt.xref, div.admonition a tt { + border-bottom: 1px solid #fafafa; +} + +dd div.admonition { + margin-left: -60px; + padding-left: 60px; +} + +div.admonition p.admonition-title { + font-family: {{ header_font }}; + font-weight: normal; + font-size: 24px; + margin: 0 0 10px 0; + padding: 0; + line-height: 1; +} + +div.admonition p.last { + margin-bottom: 0; +} + +div.highlight { + background-color: white; +} + +dt:target, .highlight { + background: #FAF3E8; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre, tt { + font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + font-size: 0.9em; +} + +img.screenshot { +} + +tt.descname, tt.descclassname { + font-size: 0.95em; +} + +tt.descname { + padding-right: 0.08em; +} + +img.screenshot { + -moz-box-shadow: 2px 2px 4px #eee; + -webkit-box-shadow: 2px 2px 4px #eee; + box-shadow: 2px 2px 4px #eee; +} + +table.docutils { + border: 1px solid #888; + -moz-box-shadow: 2px 2px 4px #eee; + -webkit-box-shadow: 2px 2px 4px #eee; + box-shadow: 2px 2px 4px #eee; +} + +table.docutils td, table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +table.field-list, table.footnote { + border: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +table.footnote { + margin: 15px 0; + width: 100%; + border: 1px solid #eee; + background: #fdfdfd; + font-size: 0.9em; +} + +table.footnote + table.footnote { + margin-top: -15px; + border-top: none; +} + +table.field-list th { + padding: 0 0.8em 0 0; +} + +table.field-list td { + padding: 0; +} + +table.footnote td.label { + width: 0px; + padding: 0.3em 0 0.3em 0.5em; +} + +table.footnote td { + padding: 0.3em 0.5em; +} + +dl { + margin: 0; + padding: 0; +} + +dl dd { + margin-left: 30px; +} + +blockquote { + margin: 0 0 0 30px; + padding: 0; +} + +ul, ol { + margin: 10px 0 10px 30px; + padding: 0; +} + +pre { + background: #eee; + padding: 7px 30px; + margin: 15px -30px; + line-height: 1.3em; +} + +dl pre, blockquote pre, li pre { + margin-left: -60px; + padding-left: 60px; +} + +dl dl pre { + margin-left: -90px; + padding-left: 90px; +} + +tt { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ +} + +tt.xref, a tt { + background-color: #FBFBFB; + border-bottom: 1px solid white; +} + +a.reference { + text-decoration: none; + border-bottom: 1px dotted {{ link_color }}; +} + +a.reference:hover { + border-bottom: 1px solid {{ link_hover_color }}; +} + +a.footnote-reference { + text-decoration: none; + font-size: 0.7em; + vertical-align: top; + border-bottom: 1px dotted {{ link_color }}; +} + +a.footnote-reference:hover { + border-bottom: 1px solid {{ link_hover_color }}; +} + +a:hover tt { + background: #EEE; +} + + +@media screen and (max-width: 870px) { + + div.sphinxsidebar { + display: none; + } + + div.document { + width: 100%; + + } + + div.documentwrapper { + margin-left: 0; + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + } + + div.bodywrapper { + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + margin-left: 0; + } + + ul { + margin-left: 0; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .bodywrapper { + margin: 0; + } + + .footer { + width: auto; + } + + .github { + display: none; + } + + + +} + + + +@media screen and (max-width: 875px) { + + body { + margin: 0; + padding: 20px 30px; + } + + div.documentwrapper { + float: none; + background: white; + } + + div.sphinxsidebar { + display: block; + float: none; + width: 102.5%; + margin: 50px -30px -20px -30px; + padding: 10px 20px; + background: #333; + color: white; + } + + div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, + div.sphinxsidebar h3 a, div.sphinxsidebar ul { + color: white; + } + + div.sphinxsidebar a { + color: #aaa; + } + + div.sphinxsidebar p.logo { + display: none; + } + + div.document { + width: 100%; + margin: 0; + } + + div.related { + display: block; + margin: 0; + padding: 10px 0 20px 0; + } + + div.related ul, + div.related ul li { + margin: 0; + padding: 0; + } + + div.footer { + display: none; + } + + div.bodywrapper { + margin: 0; + } + + div.body { + min-height: 0; + padding: 0; + } + + .rtd_doc_footer { + display: none; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .footer { + width: auto; + } + + .github { + display: none; + } +} + +/* misc. */ + +.revsys-inline { + display: none!important; +} diff --git a/doc/en/_themes/flask/theme.conf b/doc/en/_themes/flask/theme.conf new file mode 100644 index 000000000..18c720f80 --- /dev/null +++ b/doc/en/_themes/flask/theme.conf @@ -0,0 +1,9 @@ +[theme] +inherit = basic +stylesheet = flasky.css +pygments_style = flask_theme_support.FlaskyStyle + +[options] +index_logo = '' +index_logo_height = 120px +touch_icon = diff --git a/doc/en/_themes/flask_theme_support.py b/doc/en/_themes/flask_theme_support.py new file mode 100644 index 000000000..33f47449c --- /dev/null +++ b/doc/en/_themes/flask_theme_support.py @@ -0,0 +1,86 @@ +# flasky extensions. flasky pygments style based on tango style +from pygments.style import Style +from pygments.token import Keyword, Name, Comment, String, Error, \ + Number, Operator, Generic, Whitespace, Punctuation, Other, Literal + + +class FlaskyStyle(Style): + background_color = "#f8f8f8" + default_style = "" + + styles = { + # No corresponding class for the following: + #Text: "", # class: '' + Whitespace: "underline #f8f8f8", # class: 'w' + Error: "#a40000 border:#ef2929", # class: 'err' + Other: "#000000", # class 'x' + + Comment: "italic #8f5902", # class: 'c' + Comment.Preproc: "noitalic", # class: 'cp' + + Keyword: "bold #004461", # class: 'k' + Keyword.Constant: "bold #004461", # class: 'kc' + Keyword.Declaration: "bold #004461", # class: 'kd' + Keyword.Namespace: "bold #004461", # class: 'kn' + Keyword.Pseudo: "bold #004461", # class: 'kp' + Keyword.Reserved: "bold #004461", # class: 'kr' + Keyword.Type: "bold #004461", # class: 'kt' + + Operator: "#582800", # class: 'o' + Operator.Word: "bold #004461", # class: 'ow' - like keywords + + Punctuation: "bold #000000", # class: 'p' + + # because special names such as Name.Class, Name.Function, etc. + # are not recognized as such later in the parsing, we choose them + # to look the same as ordinary variables. + Name: "#000000", # class: 'n' + Name.Attribute: "#c4a000", # class: 'na' - to be revised + Name.Builtin: "#004461", # class: 'nb' + Name.Builtin.Pseudo: "#3465a4", # class: 'bp' + Name.Class: "#000000", # class: 'nc' - to be revised + Name.Constant: "#000000", # class: 'no' - to be revised + Name.Decorator: "#888", # class: 'nd' - to be revised + Name.Entity: "#ce5c00", # class: 'ni' + Name.Exception: "bold #cc0000", # class: 'ne' + Name.Function: "#000000", # class: 'nf' + Name.Property: "#000000", # class: 'py' + Name.Label: "#f57900", # class: 'nl' + Name.Namespace: "#000000", # class: 'nn' - to be revised + Name.Other: "#000000", # class: 'nx' + Name.Tag: "bold #004461", # class: 'nt' - like a keyword + Name.Variable: "#000000", # class: 'nv' - to be revised + Name.Variable.Class: "#000000", # class: 'vc' - to be revised + Name.Variable.Global: "#000000", # class: 'vg' - to be revised + Name.Variable.Instance: "#000000", # class: 'vi' - to be revised + + Number: "#990000", # class: 'm' + + Literal: "#000000", # class: 'l' + Literal.Date: "#000000", # class: 'ld' + + String: "#4e9a06", # class: 's' + String.Backtick: "#4e9a06", # class: 'sb' + String.Char: "#4e9a06", # class: 'sc' + String.Doc: "italic #8f5902", # class: 'sd' - like a comment + String.Double: "#4e9a06", # class: 's2' + String.Escape: "#4e9a06", # class: 'se' + String.Heredoc: "#4e9a06", # class: 'sh' + String.Interpol: "#4e9a06", # class: 'si' + String.Other: "#4e9a06", # class: 'sx' + String.Regex: "#4e9a06", # class: 'sr' + String.Single: "#4e9a06", # class: 's1' + String.Symbol: "#4e9a06", # class: 'ss' + + Generic: "#000000", # class: 'g' + Generic.Deleted: "#a40000", # class: 'gd' + Generic.Emph: "italic #000000", # class: 'ge' + Generic.Error: "#ef2929", # class: 'gr' + Generic.Heading: "bold #000080", # class: 'gh' + Generic.Inserted: "#00A000", # class: 'gi' + Generic.Output: "#888", # class: 'go' + Generic.Prompt: "#745334", # class: 'gp' + Generic.Strong: "bold #000000", # class: 'gs' + Generic.Subheading: "bold #800080", # class: 'gu' + Generic.Traceback: "bold #a40000", # class: 'gt' + } diff --git a/doc/en/announce/index.txt b/doc/en/announce/index.txt index 333542b98..0f0fc47d1 100644 --- a/doc/en/announce/index.txt +++ b/doc/en/announce/index.txt @@ -5,6 +5,11 @@ Release announcements .. toctree:: :maxdepth: 2 + release-2.5.1 + release-2.5.0 + release-2.4.2 + release-2.4.1 + release-2.4.0 release-2.3.5 release-2.3.4 release-2.3.3 diff --git a/doc/en/announce/release-2.4.0.txt b/doc/en/announce/release-2.4.0.txt new file mode 100644 index 000000000..5e4eec61e --- /dev/null +++ b/doc/en/announce/release-2.4.0.txt @@ -0,0 +1,225 @@ +pytest-2.4.0: new fixture features/hooks and bug fixes +=========================================================================== + +The just released pytest-2.4.0 brings many improvements and numerous +bug fixes while remaining plugin- and test-suite compatible apart +from a few supposedly very minor incompatibilities. See below for +a full list of details. A few feature highlights: + +- new yield-style fixtures `pytest.yield_fixture + `_, allowing to use + existing with-style context managers in fixture functions. + +- improved pdb support: ``import pdb ; pdb.set_trace()`` now works + without requiring prior disabling of stdout/stderr capturing. + Also the ``--pdb`` options works now on collection and internal errors + and we introduced a new experimental hook for IDEs/plugins to + intercept debugging: ``pytest_exception_interact(node, call, report)``. + +- shorter monkeypatch variant to allow specifying an import path as + a target, for example: ``monkeypatch.setattr("requests.get", myfunc)`` + +- better unittest/nose compatibility: all teardown methods are now only + called if the corresponding setup method succeeded. + +- integrate tab-completion on command line options if you + have `argcomplete `_ + configured. + +- allow boolean expression directly with skipif/xfail + if a "reason" is also specified. + +- a new hook ``pytest_load_initial_conftests`` allows plugins like + `pytest-django `_ to + influence the environment before conftest files import ``django``. + +- reporting: color the last line red or green depending if + failures/errors occured or everything passed. + +The documentation has been updated to accomodate the changes, +see `http://pytest.org `_ + +To install or upgrade pytest:: + + pip install -U pytest # or + easy_install -U pytest + + +**Many thanks to all who helped, including Floris Bruynooghe, +Brianna Laugher, Andreas Pelme, Anthon van der Neut, Anatoly Bubenkoff, +Vladimir Keleshev, Mathieu Agopian, Ronny Pfannschmidt, Christian +Theunert and many others.** + +may passing tests be with you, + +holger krekel + +Changes between 2.3.5 and 2.4 +----------------------------------- + +known incompatibilities: + +- if calling --genscript from python2.7 or above, you only get a + standalone script which works on python2.7 or above. Use Python2.6 + to also get a python2.5 compatible version. + +- all xunit-style teardown methods (nose-style, pytest-style, + unittest-style) will not be called if the corresponding setup method failed, + see issue322 below. + +- the pytest_plugin_unregister hook wasn't ever properly called + and there is no known implementation of the hook - so it got removed. + +- pytest.fixture-decorated functions cannot be generators (i.e. use + yield) anymore. This change might be reversed in 2.4.1 if it causes + unforeseen real-life issues. However, you can always write and return + an inner function/generator and change the fixture consumer to iterate + over the returned generator. This change was done in lieu of the new + ``pytest.yield_fixture`` decorator, see below. + +new features: + +- experimentally introduce a new ``pytest.yield_fixture`` decorator + which accepts exactly the same parameters as pytest.fixture but + mandates a ``yield`` statement instead of a ``return statement`` from + fixture functions. This allows direct integration with "with-style" + context managers in fixture functions and generally avoids registering + of finalization callbacks in favour of treating the "after-yield" as + teardown code. Thanks Andreas Pelme, Vladimir Keleshev, Floris + Bruynooghe, Ronny Pfannschmidt and many others for discussions. + +- allow boolean expression directly with skipif/xfail + if a "reason" is also specified. Rework skipping documentation + to recommend "condition as booleans" because it prevents surprises + when importing markers between modules. Specifying conditions + as strings will remain fully supported. + +- reporting: color the last line red or green depending if + failures/errors occured or everything passed. thanks Christian + Theunert. + +- make "import pdb ; pdb.set_trace()" work natively wrt capturing (no + "-s" needed anymore), making ``pytest.set_trace()`` a mere shortcut. + +- fix issue181: --pdb now also works on collect errors (and + on internal errors) . This was implemented by a slight internal + refactoring and the introduction of a new hook + ``pytest_exception_interact`` hook (see next item). + +- fix issue341: introduce new experimental hook for IDEs/terminals to + intercept debugging: ``pytest_exception_interact(node, call, report)``. + +- new monkeypatch.setattr() variant to provide a shorter + invocation for patching out classes/functions from modules: + + monkeypatch.setattr("requests.get", myfunc) + + will replace the "get" function of the "requests" module with ``myfunc``. + +- fix issue322: tearDownClass is not run if setUpClass failed. Thanks + Mathieu Agopian for the initial fix. Also make all of pytest/nose + finalizer mimick the same generic behaviour: if a setupX exists and + fails, don't run teardownX. This internally introduces a new method + "node.addfinalizer()" helper which can only be called during the setup + phase of a node. + +- simplify pytest.mark.parametrize() signature: allow to pass a + CSV-separated string to specify argnames. For example: + ``pytest.mark.parametrize("input,expected", [(1,2), (2,3)])`` + works as well as the previous: + ``pytest.mark.parametrize(("input", "expected"), ...)``. + +- add support for setUpModule/tearDownModule detection, thanks Brian Okken. + +- integrate tab-completion on options through use of "argcomplete". + Thanks Anthon van der Neut for the PR. + +- change option names to be hyphen-separated long options but keep the + old spelling backward compatible. py.test -h will only show the + hyphenated version, for example "--collect-only" but "--collectonly" + will remain valid as well (for backward-compat reasons). Many thanks to + Anthon van der Neut for the implementation and to Hynek Schlawack for + pushing us. + +- fix issue 308 - allow to mark/xfail/skip individual parameter sets + when parametrizing. Thanks Brianna Laugher. + +- call new experimental pytest_load_initial_conftests hook to allow + 3rd party plugins to do something before a conftest is loaded. + +Bug fixes: + +- fix issue358 - capturing options are now parsed more properly + by using a new parser.parse_known_args method. + +- pytest now uses argparse instead of optparse (thanks Anthon) which + means that "argparse" is added as a dependency if installing into python2.6 + environments or below. + +- fix issue333: fix a case of bad unittest/pytest hook interaction. + +- PR27: correctly handle nose.SkipTest during collection. Thanks + Antonio Cuni, Ronny Pfannschmidt. + +- fix issue355: junitxml puts name="pytest" attribute to testsuite tag. + +- fix issue336: autouse fixture in plugins should work again. + +- fix issue279: improve object comparisons on assertion failure + for standard datatypes and recognise collections.abc. Thanks to + Brianna Laugher and Mathieu Agopian. + +- fix issue317: assertion rewriter support for the is_package method + +- fix issue335: document py.code.ExceptionInfo() object returned + from pytest.raises(), thanks Mathieu Agopian. + +- remove implicit distribute_setup support from setup.py. + +- fix issue305: ignore any problems when writing pyc files. + +- SO-17664702: call fixture finalizers even if the fixture function + partially failed (finalizers would not always be called before) + +- fix issue320 - fix class scope for fixtures when mixed with + module-level functions. Thanks Anatloy Bubenkoff. + +- you can specify "-q" or "-qq" to get different levels of "quieter" + reporting (thanks Katarzyna Jachim) + +- fix issue300 - Fix order of conftest loading when starting py.test + in a subdirectory. + +- fix issue323 - sorting of many module-scoped arg parametrizations + +- make sessionfinish hooks execute with the same cwd-context as at + session start (helps fix plugin behaviour which write output files + with relative path such as pytest-cov) + +- fix issue316 - properly reference collection hooks in docs + +- fix issue 306 - cleanup of -k/-m options to only match markers/test + names/keywords respectively. Thanks Wouter van Ackooy. + +- improved doctest counting for doctests in python modules -- + files without any doctest items will not show up anymore + and doctest examples are counted as separate test items. + thanks Danilo Bellini. + +- fix issue245 by depending on the released py-1.4.14 + which fixes py.io.dupfile to work with files with no + mode. Thanks Jason R. Coombs. + +- fix junitxml generation when test output contains control characters, + addressing issue267, thanks Jaap Broekhuizen + +- fix issue338: honor --tb style for setup/teardown errors as well. Thanks Maho. + +- fix issue307 - use yaml.safe_load in example, thanks Mark Eichin. + +- better parametrize error messages, thanks Brianna Laugher + +- pytest_terminal_summary(terminalreporter) hooks can now use + ".section(title)" and ".line(msg)" methods to print extra + information at the end of a test run. + diff --git a/doc/en/announce/release-2.4.1.txt b/doc/en/announce/release-2.4.1.txt new file mode 100644 index 000000000..64ba170f8 --- /dev/null +++ b/doc/en/announce/release-2.4.1.txt @@ -0,0 +1,25 @@ +pytest-2.4.1: fixing three regressions compared to 2.3.5 +=========================================================================== + +pytest-2.4.1 is a quick follow up release to fix three regressions +compared to 2.3.5 before they hit more people: + +- When using parser.addoption() unicode arguments to the + "type" keyword should also be converted to the respective types. + thanks Floris Bruynooghe, @dnozay. (fixes issue360 and issue362) + +- fix dotted filename completion when using argcomplete + thanks Anthon van der Neuth. (fixes issue361) + +- fix regression when a 1-tuple ("arg",) is used for specifying + parametrization (the values of the parametrization were passed + nested in a tuple). Thanks Donald Stufft. + +- also merge doc typo fixes, thanks Andy Dirnberger + +as usual, docs at http://pytest.org and upgrades via:: + + pip install -U pytest + +have fun, +holger krekel diff --git a/doc/en/announce/release-2.4.2.txt b/doc/en/announce/release-2.4.2.txt new file mode 100644 index 000000000..fd087e141 --- /dev/null +++ b/doc/en/announce/release-2.4.2.txt @@ -0,0 +1,39 @@ +pytest-2.4.2: colorama on windows, plugin/tmpdir fixes +=========================================================================== + +pytest-2.4.2 is another bug-fixing release: + +- on Windows require colorama and a newer py lib so that py.io.TerminalWriter() + now uses colorama instead of its own ctypes hacks. (fixes issue365) + thanks Paul Moore for bringing it up. + +- fix "-k" matching of tests where "repr" and "attr" and other names would + cause wrong matches because of an internal implementation quirk + (don't ask) which is now properly implemented. fixes issue345. + +- avoid tmpdir fixture to create too long filenames especially + when parametrization is used (issue354) + +- fix pytest-pep8 and pytest-flakes / pytest interactions + (collection names in mark plugin was assuming an item always + has a function which is not true for those plugins etc.) + Thanks Andi Zeidler. + +- introduce node.get_marker/node.add_marker API for plugins + like pytest-pep8 and pytest-flakes to avoid the messy + details of the node.keywords pseudo-dicts. Adapated + docs. + +- remove attempt to "dup" stdout at startup as it's icky. + the normal capturing should catch enough possibilities + of tests messing up standard FDs. + +- add pluginmanager.do_configure(config) as a link to + config.do_configure() for plugin-compatibility + +as usual, docs at http://pytest.org and upgrades via:: + + pip install -U pytest + +have fun, +holger krekel diff --git a/doc/en/announce/release-2.5.0.txt b/doc/en/announce/release-2.5.0.txt new file mode 100644 index 000000000..b8f28d6fd --- /dev/null +++ b/doc/en/announce/release-2.5.0.txt @@ -0,0 +1,175 @@ +pytest-2.5.0: now down to ZERO reported bugs! +=========================================================================== + +pytest-2.5.0 is a big fixing release, the result of two community bug +fixing days plus numerous additional works from many people and +reporters. The release should be fully compatible to 2.4.2, existing +plugins and test suites. We aim at maintaining this level of ZERO reported +bugs because it's no fun if your testing tool has bugs, is it? Under a +condition, though: when submitting a bug report please provide +clear information about the circumstances and a simple example which +reproduces the problem. + +The issue tracker is of course not empty now. We have many remaining +"enhacement" issues which we'll hopefully can tackle in 2014 with your +help. + +For those who use older Python versions, please note that pytest is not +automatically tested on python2.5 due to virtualenv, setuptools and tox +not supporting it anymore. Manual verification shows that it mostly +works fine but it's not going to be part of the automated release +process and thus likely to break in the future. + +As usual, current docs are at + + http://pytest.org + +and you can upgrade from pypi via:: + + pip install -U pytest + +Particular thanks for helping with this release go to Anatoly Bubenkoff, +Floris Bruynooghe, Marc Abramowitz, Ralph Schmitt, Ronny Pfannschmidt, +Donald Stufft, James Lan, Rob Dennis, Jason R. Coombs, Mathieu Agopian, +Virgil Dupras, Bruno Oliveira, Alex Gaynor and others. + +have fun, +holger krekel + + +2.5.0 +----------------------------------- + +- dropped python2.5 from automated release testing of pytest itself + which means it's probably going to break soon (but still works + with this release we believe). + +- simplified and fixed implementation for calling finalizers when + parametrized fixtures or function arguments are involved. finalization + is now performed lazily at setup time instead of in the "teardown phase". + While this might sound odd at first, it helps to ensure that we are + correctly handling setup/teardown even in complex code. User-level code + should not be affected unless it's implementing the pytest_runtest_teardown + hook and expecting certain fixture instances are torn down within (very + unlikely and would have been unreliable anyway). + +- PR90: add --color=yes|no|auto option to force terminal coloring + mode ("auto" is default). Thanks Marc Abramowitz. + +- fix issue319 - correctly show unicode in assertion errors. Many + thanks to Floris Bruynooghe for the complete PR. Also means + we depend on py>=1.4.19 now. + +- fix issue396 - correctly sort and finalize class-scoped parametrized + tests independently from number of methods on the class. + +- refix issue323 in a better way -- parametrization should now never + cause Runtime Recursion errors because the underlying algorithm + for re-ordering tests per-scope/per-fixture is not recursive + anymore (it was tail-call recursive before which could lead + to problems for more than >966 non-function scoped parameters). + +- fix issue290 - there is preliminary support now for parametrizing + with repeated same values (sometimes useful to to test if calling + a second time works as with the first time). + +- close issue240 - document precisely how pytest module importing + works, discuss the two common test directory layouts, and how it + interacts with PEP420-namespace packages. + +- fix issue246 fix finalizer order to be LIFO on independent fixtures + depending on a parametrized higher-than-function scoped fixture. + (was quite some effort so please bear with the complexity of this sentence :) + Thanks Ralph Schmitt for the precise failure example. + +- fix issue244 by implementing special index for parameters to only use + indices for paramentrized test ids + +- fix issue287 by running all finalizers but saving the exception + from the first failing finalizer and re-raising it so teardown will + still have failed. We reraise the first failing exception because + it might be the cause for other finalizers to fail. + +- fix ordering when mock.patch or other standard decorator-wrappings + are used with test methods. This fixues issue346 and should + help with random "xdist" collection failures. Thanks to + Ronny Pfannschmidt and Donald Stufft for helping to isolate it. + +- fix issue357 - special case "-k" expressions to allow for + filtering with simple strings that are not valid python expressions. + Examples: "-k 1.3" matches all tests parametrized with 1.3. + "-k None" filters all tests that have "None" in their name + and conversely "-k 'not None'". + Previously these examples would raise syntax errors. + +- fix issue384 by removing the trial support code + since the unittest compat enhancements allow + trial to handle it on its own + +- don't hide an ImportError when importing a plugin produces one. + fixes issue375. + +- fix issue275 - allow usefixtures and autouse fixtures + for running doctest text files. + +- fix issue380 by making --resultlog only rely on longrepr instead + of the "reprcrash" attribute which only exists sometimes. + +- address issue122: allow @pytest.fixture(params=iterator) by exploding + into a list early on. + +- fix pexpect-3.0 compatibility for pytest's own tests. + (fixes issue386) + +- allow nested parametrize-value markers, thanks James Lan for the PR. + +- fix unicode handling with new monkeypatch.setattr(import_path, value) + API. Thanks Rob Dennis. Fixes issue371. + +- fix unicode handling with junitxml, fixes issue368. + +- In assertion rewriting mode on Python 2, fix the detection of coding + cookies. See issue #330. + +- make "--runxfail" turn imperative pytest.xfail calls into no ops + (it already did neutralize pytest.mark.xfail markers) + +- refine pytest / pkg_resources interactions: The AssertionRewritingHook + PEP302 compliant loader now registers itself with setuptools/pkg_resources + properly so that the pkg_resources.resource_stream method works properly. + Fixes issue366. Thanks for the investigations and full PR to Jason R. Coombs. + +- pytestconfig fixture is now session-scoped as it is the same object during the + whole test run. Fixes issue370. + +- avoid one surprising case of marker malfunction/confusion:: + + @pytest.mark.some(lambda arg: ...) + def test_function(): + + would not work correctly because pytest assumes @pytest.mark.some + gets a function to be decorated already. We now at least detect if this + arg is an lambda and thus the example will work. Thanks Alex Gaynor + for bringing it up. + +- xfail a test on pypy that checks wrong encoding/ascii (pypy does + not error out). fixes issue385. + +- internally make varnames() deal with classes's __init__, + although it's not needed by pytest itself atm. Also + fix caching. Fixes issue376. + +- fix issue221 - handle importing of namespace-package with no + __init__.py properly. + +- refactor internal FixtureRequest handling to avoid monkeypatching. + One of the positive user-facing effects is that the "request" object + can now be used in closures. + +- fixed version comparison in pytest.importskip(modname, minverstring) + +- fix issue377 by clarifying in the nose-compat docs that pytest + does not duplicate the unittest-API into the "plain" namespace. + +- fix verbose reporting for @mock'd test functions + diff --git a/doc/en/announce/release-2.5.1.txt b/doc/en/announce/release-2.5.1.txt new file mode 100644 index 000000000..a3a74cec6 --- /dev/null +++ b/doc/en/announce/release-2.5.1.txt @@ -0,0 +1,47 @@ +pytest-2.5.1: fixes and new home page styling +=========================================================================== + +pytest is a mature Python testing tool with more than a 1000 tests +against itself, passing on many different interpreters and platforms. + +The 2.5.1 release maintains the "zero-reported-bugs" promise by fixing +the three bugs reported since the last release a few days ago. It also +features a new home page styling implemented by Tobias Bieniek, based on +the flask theme from Armin Ronacher: + + http://pytest.org + +If you have anything more to improve styling and docs, +we'd be very happy to merge further pull requests. + +On the coding side, the release also contains a little enhancement to +fixture decorators allowing to directly influence generation of test +ids, thanks to Floris Bruynooghe. Other thanks for helping with +this release go to Anatoly Bubenkoff and Ronny Pfannschmidt. + +As usual, you can upgrade from pypi via:: + + pip install -U pytest + +have fun and a nice remaining "bug-free" time of the year :) +holger krekel + +2.5.1 +----------------------------------- + +- merge new documentation styling PR from Tobias Bieniek. + +- fix issue403: allow parametrize of multiple same-name functions within + a collection node. Thanks Andreas Kloeckner and Alex Gaynor for reporting + and analysis. + +- Allow parameterized fixtures to specify the ID of the parameters by + adding an ids argument to pytest.fixture() and pytest.yield_fixture(). + Thanks Floris Bruynooghe. + +- fix issue404 by always using the binary xml escape in the junitxml + plugin. Thanks Ronny Pfannschmidt. + +- fix issue407: fix addoption docstring to point to argparse instead of + optparse. Thanks Daniel D. Wright. + diff --git a/doc/en/apiref.txt b/doc/en/apiref.txt index 876537451..5cf1c0d98 100644 --- a/doc/en/apiref.txt +++ b/doc/en/apiref.txt @@ -1,7 +1,7 @@ .. _apiref: -py.test reference documentation +pytest reference documentation ================================================ .. toctree:: @@ -11,6 +11,7 @@ py.test reference documentation customize.txt assert.txt fixture.txt + yieldfixture.txt parametrize.txt xunit_setup.txt capture.txt diff --git a/doc/en/assert.txt b/doc/en/assert.txt index 247522c45..d390947f5 100644 --- a/doc/en/assert.txt +++ b/doc/en/assert.txt @@ -10,7 +10,7 @@ The writing and reporting of assertions in tests Asserting with the ``assert`` statement --------------------------------------------------------- -``py.test`` allows you to use the standard python ``assert`` for verifying +``pytest`` allows you to use the standard python ``assert`` for verifying expectations and values in Python tests. For example, you can write the following:: @@ -26,23 +26,23 @@ you will see the return value of the function call:: $ py.test test_assert1.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 1 items - + test_assert1.py F - + ================================= FAILURES ================================= ______________________________ test_function _______________________________ - + def test_function(): > assert f() == 4 E assert 3 == 4 E + where 3 = f() - + test_assert1.py:5: AssertionError ========================= 1 failed in 0.01 seconds ========================= -py.test has support for showing the values of the most common subexpressions +``pytest`` has support for showing the values of the most common subexpressions including calls, attributes, comparisons, and binary and unary operators. (See :ref:`tbreportdemo`). This allows you to use the idiomatic python constructs without boilerplate code while not losing @@ -102,7 +102,7 @@ Making use of context-sensitive comparisons .. versionadded:: 2.0 -py.test has rich support for providing context-sensitive information +``pytest`` has rich support for providing context-sensitive information when it encounters comparisons. For example:: # content of test_assert2.py @@ -116,14 +116,14 @@ if you run this module:: $ py.test test_assert2.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 1 items - + test_assert2.py F - + ================================= FAILURES ================================= ___________________________ test_set_comparison ____________________________ - + def test_set_comparison(): set1 = set("1308") set2 = set("8035") @@ -133,7 +133,7 @@ if you run this module:: E '1' E Extra items in the right set: E '5' - + test_assert2.py:5: AssertionError ========================= 1 failed in 0.01 seconds ========================= @@ -175,22 +175,23 @@ now, given this test module:: f2 = Foo(2) assert f1 == f2 -you can run the test module and get the custom output defined in +you can run the test module and get the custom output defined in the conftest file:: $ py.test -q test_foocompare.py F ================================= FAILURES ================================= _______________________________ test_compare _______________________________ - + def test_compare(): f1 = Foo(1) f2 = Foo(2) > assert f1 == f2 E assert Comparing Foo instances: E vals: 1 != 2 - + test_foocompare.py:8: AssertionError + 1 failed in 0.01 seconds .. _assert-details: .. _`assert introspection`: @@ -204,33 +205,33 @@ Advanced assertion introspection Reporting details about a failing assertion is achieved either by rewriting assert statements before they are run or re-evaluating the assert expression and recording the intermediate values. Which technique is used depends on the -location of the assert, py.test's configuration, and Python version being used -to run py.test. Note that for assert statements with a manually provided +location of the assert, ``pytest`` configuration, and Python version being used +to run ``pytest``. Note that for assert statements with a manually provided message, i.e. ``assert expr, message``, no assertion introspection takes place and the manually provided message will be rendered in tracebacks. -By default, if the Python version is greater than or equal to 2.6, py.test +By default, if the Python version is greater than or equal to 2.6, ``pytest`` rewrites assert statements in test modules. Rewritten assert statements put -introspection information into the assertion failure message. py.test only +introspection information into the assertion failure message. ``pytest`` only rewrites test modules directly discovered by its test collection process, so asserts in supporting modules which are not themselves test modules will not be rewritten. .. note:: - py.test rewrites test modules on import. It does this by using an import hook - to write a new pyc files. Most of the time this works transparently. However, - if you are messing with import yourself, the import hook may interfere. If - this is the case, simply use ``--assert=reinterp`` or + ``pytest`` rewrites test modules on import. It does this by using an import + hook to write a new pyc files. Most of the time this works transparently. + However, if you are messing with import yourself, the import hook may + interfere. If this is the case, simply use ``--assert=reinterp`` or ``--assert=plain``. Additionally, rewriting will fail silently if it cannot write new pycs, i.e. in a read-only filesystem or a zipfile. If an assert statement has not been rewritten or the Python version is less than -2.6, py.test falls back on assert reinterpretation. In assert reinterpretation, -py.test walks the frame of the function containing the assert statement to -discover sub-expression results of the failing assert statement. You can force -py.test to always use assertion reinterpretation by passing the -``--assert=reinterp`` option. +2.6, ``pytest`` falls back on assert reinterpretation. In assert +reinterpretation, ``pytest`` walks the frame of the function containing the +assert statement to discover sub-expression results of the failing assert +statement. You can force ``pytest`` to always use assertion reinterpretation by +passing the ``--assert=reinterp`` option. Assert reinterpretation has a caveat not present with assert rewriting: If evaluating the assert expression has side effects you may get a warning that the @@ -249,7 +250,7 @@ easy to rewrite the assertion and avoid any trouble:: All assert introspection can be turned off by passing ``--assert=plain``. -For further information, Benjamin Peterson wrote up `Behind the scenes of py.test's new assertion rewriting `_. +For further information, Benjamin Peterson wrote up `Behind the scenes of pytest's new assertion rewriting `_. .. versionadded:: 2.1 Add assert rewriting as an alternate introspection technique. diff --git a/doc/en/attic_fixtures.txt b/doc/en/attic_fixtures.txt index df14030e4..8b796a637 100644 --- a/doc/en/attic_fixtures.txt +++ b/doc/en/attic_fixtures.txt @@ -129,6 +129,7 @@ Let's run this module without output-capturing:: E NameError: global name 'globresource' is not defined test_glob.py:5: NameError + 2 failed in 0.01 seconds The two tests see the same global ``globresource`` object. @@ -177,6 +178,7 @@ And then re-run our test module:: E NameError: global name 'globresource' is not defined test_glob.py:5: NameError + 2 failed in 0.01 seconds We are now running the two tests twice with two different global resource instances. Note that the tests are ordered such that only diff --git a/doc/en/bash-completion.txt b/doc/en/bash-completion.txt index 0fbe83f38..b2a52fa63 100644 --- a/doc/en/bash-completion.txt +++ b/doc/en/bash-completion.txt @@ -4,7 +4,7 @@ Setting up bash completion ========================== -When using bash as your shell, ``py.test`` can use argcomplete +When using bash as your shell, ``pytest`` can use argcomplete (https://argcomplete.readthedocs.org/) for auto-completion. For this ``argcomplete`` needs to be installed **and** enabled. @@ -16,11 +16,11 @@ For global activation of all argcomplete enabled python applications run:: sudo activate-global-python-argcomplete -For permanent (but not global) ``py.test`` activation, use:: +For permanent (but not global) ``pytest`` activation, use:: register-python-argcomplete py.test >> ~/.bashrc -For one-time activation of argcomplete for ``py.test`` only, use:: +For one-time activation of argcomplete for ``pytest`` only, use:: eval "$(register-python-argcomplete py.test)" diff --git a/doc/en/builtin.txt b/doc/en/builtin.txt index f06882ce1..368d99fde 100644 --- a/doc/en/builtin.txt +++ b/doc/en/builtin.txt @@ -120,3 +120,4 @@ You can ask for available builtin or project-custom path object. + in 0.00 seconds diff --git a/doc/en/capture.txt b/doc/en/capture.txt index 639412857..0c1527ba2 100644 --- a/doc/en/capture.txt +++ b/doc/en/capture.txt @@ -23,7 +23,7 @@ a test. Setting capturing methods or disabling capturing ------------------------------------------------- -There are two ways in which ``py.test`` can perform capturing: +There are two ways in which ``pytest`` can perform capturing: * file descriptor (FD) level capturing (default): All writes going to the operating system file descriptors 1 and 2 will be captured. @@ -49,7 +49,7 @@ One primary benefit of the default capturing of stdout/stderr output is that you can use print statements for debugging:: # content of test_module.py - + def setup_function(function): print ("setting up %s" % function) @@ -64,21 +64,21 @@ of the failing function and hide the other one:: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 2 items - + test_module.py .F - + ================================= FAILURES ================================= ________________________________ test_func2 ________________________________ - + def test_func2(): > assert False E assert False - + test_module.py:9: AssertionError ----------------------------- Captured stdout ------------------------------ - setting up + setting up ==================== 1 failed, 1 passed in 0.01 seconds ==================== Accessing captured output from a test function @@ -105,7 +105,7 @@ and capturing will be continued. After the test function finishes the original streams will be restored. Using ``capsys`` this way frees your test from having to care about setting/resetting -output streams and also interacts well with py.test's +output streams and also interacts well with pytest's own per-test capturing. If you want to capture on ``fd`` level you can use diff --git a/doc/en/conf.py b/doc/en/conf.py index ee0559754..6437d5abb 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -17,7 +17,8 @@ # # The full version, including alpha/beta/rc tags. # The short X.Y version. -version = release = "2.4.0.dev" +version = "2.5.1" +release = "2.5.1" import sys, os @@ -53,7 +54,7 @@ master_doc = 'contents' # General information about the project. project = u'pytest' -copyright = u'2012, holger krekel' +copyright = u'2013, holger krekel' @@ -104,14 +105,19 @@ pygments_style = 'sphinx' # -- Options for HTML output --------------------------------------------------- +sys.path.append(os.path.abspath('_themes')) +html_theme_path = ['_themes'] + # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinxdoc' +html_theme = 'flask' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -html_theme_options = {} +html_theme_options = { + 'index_logo': None +} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] @@ -149,6 +155,23 @@ html_static_path = ['_static'] #html_sidebars = {} #html_sidebars = {'index': 'indexsidebar.html'} +html_sidebars = { + 'index': [ + 'sidebarintro.html', + 'globaltoc.html', + 'links.html', + 'sourcelink.html', + 'searchbox.html' + ], + '**': [ + 'globaltoc.html', + 'relations.html', + 'links.html', + 'sourcelink.html', + 'searchbox.html' + ] +} + # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} @@ -197,7 +220,7 @@ htmlhelp_basename = 'pytestdoc' # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('contents', 'pytest.tex', u'pytest Documentation', - u'holger krekel, http://merlinux.eu', 'manual'), + u'holger krekel, trainer and consultant, http://merlinux.eu', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -239,7 +262,7 @@ man_pages = [ epub_title = u'pytest' epub_author = u'holger krekel at merlinux eu' epub_publisher = u'holger krekel at merlinux eu' -epub_copyright = u'2012, holger krekel et alii' +epub_copyright = u'2013, holger krekel et alii' # The language of the text. It defaults to the language option # or en if the language is not set. diff --git a/doc/en/contact.txt b/doc/en/contact.txt index 853f20a26..c42067594 100644 --- a/doc/en/contact.txt +++ b/doc/en/contact.txt @@ -18,6 +18,9 @@ Contact channels - `pytest-commit at python.org (mailing list)`_: for commits and new issues +- :doc:`contribution guide ` for help on submitting pull + requests to bitbucket (including using git via gitifyhg). + - #pylib on irc.freenode.net IRC channel for random questions. - private mail to Holger.Krekel at gmail com if you want to communicate sensitive issues diff --git a/doc/en/contents.txt b/doc/en/contents.txt index 169524483..94a772abf 100644 --- a/doc/en/contents.txt +++ b/doc/en/contents.txt @@ -16,7 +16,7 @@ Full pytest documentation plugins example/index talks - develop + contributing funcarg_compare.txt announce/index diff --git a/doc/en/contributing.txt b/doc/en/contributing.txt new file mode 100644 index 000000000..2b6578f6b --- /dev/null +++ b/doc/en/contributing.txt @@ -0,0 +1,3 @@ +.. _contributing: + +.. include:: ../../CONTRIBUTING.rst diff --git a/doc/en/customize.txt b/doc/en/customize.txt index d436a0672..bb873a771 100644 --- a/doc/en/customize.txt +++ b/doc/en/customize.txt @@ -17,7 +17,7 @@ which were registered by installed plugins. How test configuration is read from configuration INI-files ------------------------------------------------------------- -py.test searches for the first matching ini-style configuration file +``pytest`` searches for the first matching ini-style configuration file in the directories of command line argument and the directories above. It looks for file basenames in this order:: @@ -26,8 +26,8 @@ It looks for file basenames in this order:: setup.cfg Searching stops when the first ``[pytest]`` section is found in any of -these files. There is no merging of configuration values from multiple -files. Example:: +these files. There is no merging of configuration values from multiple +files. Example:: py.test path/to/testdir @@ -41,7 +41,7 @@ will look in the following dirs for a config file:: path/to/setup.cfg ... # up until root of filesystem -If argument is provided to a py.test run, the current working directory +If argument is provided to a ``pytest`` run, the current working directory is used to start the search. .. _`how to change command line options defaults`: @@ -51,7 +51,7 @@ How to change command line options defaults ------------------------------------------------ It can be tedious to type the same series of command line options -every time you use py.test . For example, if you always want to see +every time you use ``pytest``. For example, if you always want to see detailed info on skipped and xfailed tests, as well as have terser "dot" progress output, you can write it into a configuration file:: @@ -60,7 +60,7 @@ progress output, you can write it into a configuration file:: [pytest] addopts = -rsxX -q -From now on, running ``py.test`` will add the specified options. +From now on, running ``pytest`` will add the specified options. Builtin configuration file options ---------------------------------------------- @@ -105,7 +105,7 @@ Builtin configuration file options [pytest] norecursedirs = .svn _build tmp* - This would tell py.test to not look into typical subversion or + This would tell ``pytest`` to not look into typical subversion or sphinx-build directories or into any ``tmp`` prefixed directory. .. confval:: python_files @@ -121,6 +121,8 @@ Builtin configuration file options .. confval:: python_functions One or more name prefixes determining which test functions - and methods are considered as test modules. + and methods are considered as test modules. Note that this + has no effect on methods that live on a ``unittest.TestCase`` + derived class. See :ref:`change naming conventions` for examples. diff --git a/doc/en/develop.txt b/doc/en/develop.txt deleted file mode 100644 index 3ee9380e8..000000000 --- a/doc/en/develop.txt +++ /dev/null @@ -1,40 +0,0 @@ -================================================= -Feedback and contribute to py.test -================================================= - -.. toctree:: - :maxdepth: 2 - - contact.txt - -.. _checkout: - -Working from version control or a tarball -================================================= - -To follow development or start experiments, checkout the -complete code and documentation source with mercurial_:: - - hg clone https://bitbucket.org/hpk42/pytest/ - -You can also go to the python package index and -download and unpack a TAR file:: - - http://pypi.python.org/pypi/pytest/ - -Activating a checkout with setuptools --------------------------------------------- - -With a working Distribute_ or setuptools_ installation you can type:: - - python setup.py develop - -in order to work inline with the tools and the lib of your checkout. - -If this command complains that it could not find the required version -of "py" then you need to use the development pypi repository:: - - python setup.py develop -i http://pypi.testrun.org - - -.. include:: links.inc diff --git a/doc/en/doctest.txt b/doc/en/doctest.txt index dc1c125f5..2d29e57de 100644 --- a/doc/en/doctest.txt +++ b/doc/en/doctest.txt @@ -44,15 +44,19 @@ then you can just invoke ``py.test`` without command line options:: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 1 items mymodule.py . - ========================= 1 passed in 0.02 seconds ========================= + ========================= 1 passed in 0.01 seconds ========================= It is possible to use fixtures using the ``getfixture`` helper:: # content of example.rst >>> tmp = getfixture('tmpdir') >>> ... + >>> + +Also, :ref:`usefixtures` and :ref:`autouse` fixtures are supported +when executing text doctest files. diff --git a/doc/en/example/assertion/failure_demo.py b/doc/en/example/assertion/failure_demo.py index c1165b23a..ed2776ee1 100644 --- a/doc/en/example/assertion/failure_demo.py +++ b/doc/en/example/assertion/failure_demo.py @@ -1,4 +1,4 @@ -from py.test import raises +from pytest import raises import py def otherfunc(a,b): diff --git a/doc/en/example/markers.txt b/doc/en/example/markers.txt index afc6aa3b5..09b21f178 100644 --- a/doc/en/example/markers.txt +++ b/doc/en/example/markers.txt @@ -28,24 +28,24 @@ You can then restrict a test run to only run tests marked with ``webtest``:: $ py.test -v -m webtest =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 -- /home/hpk/p/pytest/.tox/regen/bin/python collecting ... collected 3 items - + test_server.py:3: test_send_http PASSED - + =================== 2 tests deselected by "-m 'webtest'" =================== ================== 1 passed, 2 deselected in 0.01 seconds ================== Or the inverse, running all tests except the webtest ones:: - + $ py.test -v -m "not webtest" =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 -- /home/hpk/p/pytest/.tox/regen/bin/python collecting ... collected 3 items - + test_server.py:6: test_something_quick PASSED test_server.py:8: test_another PASSED - + ================= 1 tests deselected by "-m 'not webtest'" ================= ================== 2 passed, 1 deselected in 0.01 seconds ================== @@ -61,11 +61,11 @@ select tests based on their names:: $ py.test -v -k http # running with the above defined example module =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 -- /home/hpk/p/pytest/.tox/regen/bin/python collecting ... collected 3 items - + test_server.py:3: test_send_http PASSED - + ====================== 2 tests deselected by '-khttp' ====================== ================== 1 passed, 2 deselected in 0.01 seconds ================== @@ -73,12 +73,12 @@ And you can also run all tests except the ones that match the keyword:: $ py.test -k "not send_http" -v =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 -- /home/hpk/p/pytest/.tox/regen/bin/python collecting ... collected 3 items - + test_server.py:6: test_something_quick PASSED test_server.py:8: test_another PASSED - + ================= 1 tests deselected by '-knot send_http' ================== ================== 2 passed, 1 deselected in 0.01 seconds ================== @@ -86,15 +86,26 @@ Or to select "http" and "quick" tests:: $ py.test -k "http or quick" -v =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 -- /home/hpk/p/pytest/.tox/regen/bin/python collecting ... collected 3 items - + test_server.py:3: test_send_http PASSED test_server.py:6: test_something_quick PASSED - + ================= 1 tests deselected by '-khttp or quick' ================== ================== 2 passed, 1 deselected in 0.01 seconds ================== +.. note:: + + If you are using expressions such as "X and Y" then both X and Y + need to be simple non-keyword names. For example, "pass" or "from" + will result in SyntaxErrors because "-k" evaluates the expression. + + However, if the "-k" argument is a simple string, no such restrictions + apply. Also "-k 'not STRING'" has no restrictions. You can also + specify numbers like "-k 1.3" to match tests which are parametrized + with the float "1.3". + Registering markers ------------------------------------- @@ -113,19 +124,19 @@ You can ask which markers exist for your test suite - the list includes our just $ py.test --markers @pytest.mark.webtest: mark a test as a webtest. - + @pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see http://pytest.org/latest/skipping.html - + @pytest.mark.xfail(condition, reason=None, run=True): mark the the test function as an expected failure if eval(condition) has a True value. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. See http://pytest.org/latest/skipping.html - - @pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in multiple different argument value sets. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2. see http://pytest.org/latest/parametrize.html for more info and examples. - - @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures - + + @pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see http://pytest.org/latest/parametrize.html for more info and examples. + + @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures + @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. - + @pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. - + For an example on how to add and work with markers from a plugin, see :ref:`adding a custom marker from a plugin`. @@ -139,8 +150,8 @@ For an example on how to add and work with markers from a plugin, see * asking for existing markers via ``py.test --markers`` gives good output * typos in function markers are treated as an error if you use - the ``--strict`` option. Later versions of py.test are probably - going to treat non-registered markers as an error. + the ``--strict`` option. Future versions of ``pytest`` are probably + going to start treating non-registered markers as errors at some point. .. _`scoped-marking`: @@ -235,7 +246,7 @@ specifies via named environments:: "env(name): mark test to run only on named environment") def pytest_runtest_setup(item): - envmarker = item.keywords.get("env", None) + envmarker = item.get_marker("env") if envmarker is not None: envname = envmarker.args[0] if envname != item.config.getoption("-E"): @@ -255,42 +266,42 @@ the test needs:: $ py.test -E stage2 =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 1 items - + test_someenv.py s - + ======================== 1 skipped in 0.01 seconds ========================= - + and here is one that specifies exactly the environment needed:: $ py.test -E stage1 =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 1 items - + test_someenv.py . - + ========================= 1 passed in 0.01 seconds ========================= The ``--markers`` option always gives you a list of available markers:: $ py.test --markers @pytest.mark.env(name): mark test to run only on named environment - + @pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see http://pytest.org/latest/skipping.html - + @pytest.mark.xfail(condition, reason=None, run=True): mark the the test function as an expected failure if eval(condition) has a True value. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. See http://pytest.org/latest/skipping.html - - @pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in multiple different argument value sets. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2. see http://pytest.org/latest/parametrize.html for more info and examples. - - @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures - + + @pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see http://pytest.org/latest/parametrize.html for more info and examples. + + @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures + @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. - + @pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. - - + + Reading markers which were set from multiple places ---------------------------------------------------- @@ -318,7 +329,7 @@ test function. From a conftest file we can read it like this:: import sys def pytest_runtest_setup(item): - g = item.keywords.get("glob", None) + g = item.get_marker("glob") if g is not None: for info in g: print ("glob args=%s kwargs=%s" %(info.args, info.kwargs)) @@ -326,11 +337,12 @@ test function. From a conftest file we can read it like this:: Let's run this without capturing output and see what we get:: - $ py.test -q -s + $ py.test -q -s glob args=('function',) kwargs={'x': 3} glob args=('class',) kwargs={'x': 2} glob args=('module',) kwargs={'x': 1} . + 1 passed in 0.01 seconds marking platform specific tests with pytest -------------------------------------------------------------- @@ -340,7 +352,7 @@ marking platform specific tests with pytest Consider you have a test suite which marks tests for particular platforms, namely ``pytest.mark.osx``, ``pytest.mark.win32`` etc. and you also have tests that run on all platforms and have no specific -marker. If you now want to have a way to only run the tests +marker. If you now want to have a way to only run the tests for your particular platform, you could use the following plugin:: # content of conftest.py @@ -353,7 +365,7 @@ for your particular platform, you could use the following plugin:: def pytest_runtest_setup(item): if isinstance(item, item.Function): plat = sys.platform - if plat not in item.keywords: + if not item.get_marker(plat): if ALL.intersection(item.keywords): pytest.skip("cannot run on platform %s" %(plat)) @@ -383,28 +395,28 @@ then you will see two test skipped and two executed tests as expected:: $ py.test -rs # this option reports skip reasons =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 4 items - + test_plat.py s.s. ========================= short test summary info ========================== - SKIP [2] /tmp/doc-exec-273/conftest.py:12: cannot run on platform linux2 - + SKIP [2] /tmp/doc-exec-63/conftest.py:12: cannot run on platform linux2 + =================== 2 passed, 2 skipped in 0.01 seconds ==================== Note that if you specify a platform via the marker-command line option like this:: $ py.test -m linux2 =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 4 items - + test_plat.py . - + =================== 3 tests deselected by "-m 'linux2'" ==================== ================== 1 passed, 3 deselected in 0.01 seconds ================== -then the unmarked-tests will not be run. It is thus a way to restrict the run to the specific tests. +then the unmarked-tests will not be run. It is thus a way to restrict the run to the specific tests. Automatically adding markers based on test names -------------------------------------------------------- @@ -423,7 +435,7 @@ at this test module:: def test_interface_complex(): assert 0 - + def test_event_simple(): assert 0 @@ -434,24 +446,24 @@ We want to dynamically define two markers and can do it in a ``conftest.py`` plugin:: # content of conftest.py - + import pytest def pytest_collection_modifyitems(items): for item in items: if "interface" in item.nodeid: - item.keywords["interface"] = pytest.mark.interface + item.add_marker(pytest.mark.interface) elif "event" in item.nodeid: - item.keywords["event"] = pytest.mark.event + item.add_marker(pytest.mark.event) We can now use the ``-m option`` to select one set:: $ py.test -m interface --tb=short =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 4 items - + test_module.py FF - + ================================= FAILURES ================================= __________________________ test_interface_simple ___________________________ test_module.py:3: in test_interface_simple @@ -468,11 +480,11 @@ or to select both "event" and "interface" tests:: $ py.test -m "interface or event" --tb=short =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 4 items - + test_module.py FFF - + ================================= FAILURES ================================= __________________________ test_interface_simple ___________________________ test_module.py:3: in test_interface_simple @@ -487,4 +499,4 @@ or to select both "event" and "interface" tests:: > assert 0 E assert 0 ============= 1 tests deselected by "-m 'interface or event'" ============== - ================== 3 failed, 1 deselected in 0.02 seconds ================== + ================== 3 failed, 1 deselected in 0.01 seconds ================== diff --git a/doc/en/example/multipython.py b/doc/en/example/multipython.py index 142ccc7ee..0e2175a1e 100644 --- a/doc/en/example/multipython.py +++ b/doc/en/example/multipython.py @@ -2,7 +2,8 @@ module containing a parametrized tests testing cross-python serialization via the pickle module. """ -import py, pytest +import py +import pytest pythonlist = ['python2.4', 'python2.5', 'python2.6', 'python2.7', 'python2.8'] @pytest.fixture(params=pythonlist) @@ -18,7 +19,7 @@ class Python: def __init__(self, version, picklefile): self.pythonpath = py.path.local.sysfind(version) if not self.pythonpath: - py.test.skip("%r not found" %(version,)) + pytest.skip("%r not found" %(version,)) self.picklefile = picklefile def dumps(self, obj): dumpfile = self.picklefile.dirpath("dump.py") diff --git a/doc/en/example/nonpython.txt b/doc/en/example/nonpython.txt index 632da4478..fe6563e27 100644 --- a/doc/en/example/nonpython.txt +++ b/doc/en/example/nonpython.txt @@ -27,7 +27,7 @@ now execute the test specification:: nonpython $ py.test test_simple.yml =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 2 items test_simple.yml .F @@ -37,7 +37,7 @@ now execute the test specification:: usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.05 seconds ==================== + ==================== 1 failed, 1 passed in 0.03 seconds ==================== You get one dot for the passing ``sub1: sub1`` check and one failure. Obviously in the above ``conftest.py`` you'll want to implement a more @@ -56,7 +56,7 @@ consulted when reporting in ``verbose`` mode:: nonpython $ py.test -v =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 -- /home/hpk/p/pytest/.tox/regen/bin/python collecting ... collected 2 items test_simple.yml:1: usecase: ok PASSED @@ -67,17 +67,17 @@ consulted when reporting in ``verbose`` mode:: usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.05 seconds ==================== + ==================== 1 failed, 1 passed in 0.03 seconds ==================== While developing your custom test collection and execution it's also interesting to just look at the collection tree:: nonpython $ py.test --collect-only =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 2 items - ============================= in 0.05 seconds ============================= + ============================= in 0.02 seconds ============================= diff --git a/doc/en/example/parametrize.txt b/doc/en/example/parametrize.txt index f6a3cb263..c7b1249af 100644 --- a/doc/en/example/parametrize.txt +++ b/doc/en/example/parametrize.txt @@ -6,7 +6,7 @@ Parametrizing tests .. currentmodule:: _pytest.python -py.test allows to easily parametrize test functions. +``pytest`` allows to easily parametrize test functions. For basic docs, see :ref:`parametrize-basics`. In the following we provide some examples using @@ -46,6 +46,7 @@ This means that we only run 2 tests if we do not pass ``--all``:: $ py.test -q test_compute.py .. + 2 passed in 0.01 seconds We run only two computations, so we see two dots. let's run the full monty:: @@ -54,14 +55,15 @@ let's run the full monty:: ....F ================================= FAILURES ================================= _____________________________ test_compute[4] ______________________________ - + param1 = 4 - + def test_compute(param1): > assert param1 < 4 E assert 4 < 4 - + test_compute.py:3: AssertionError + 1 failed, 4 passed in 0.01 seconds As expected when running the full range of ``param1`` values we'll get an error on the last one. @@ -104,11 +106,11 @@ this is a fully self-contained example which you can run with:: $ py.test test_scenarios.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 4 items - + test_scenarios.py .... - + ========================= 4 passed in 0.01 seconds ========================= If you just collect tests you'll also nicely see 'advanced' and 'basic' as variants for the test function:: @@ -116,7 +118,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia $ py.test --collect-only test_scenarios.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 4 items @@ -125,7 +127,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia - + ============================= in 0.01 seconds ============================= Note that we told ``metafunc.parametrize()`` that your scenario values @@ -180,12 +182,12 @@ Let's first see how it looks like at collection time:: $ py.test test_backends.py --collect-only =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 2 items - + ============================= in 0.00 seconds ============================= And then when we run the test:: @@ -194,16 +196,17 @@ And then when we run the test:: .F ================================= FAILURES ================================= _________________________ test_db_initialized[d2] __________________________ - - db = - + + db = + def test_db_initialized(db): # a dummy test if db.__class__.__name__ == "DB2": > pytest.fail("deliberately failing for demo purposes") E Failed: deliberately failing for demo purposes - + test_backends.py:6: Failed + 1 failed, 1 passed in 0.01 seconds The first invocation with ``db == "DB1"`` passed while the second with ``db == "DB2"`` failed. Our ``db`` fixture function has instantiated each of the DB values during the setup phase while the ``pytest_generate_tests`` generated two according calls to the ``test_db_initialized`` during the collection phase. @@ -248,15 +251,16 @@ argument sets to use for each test function. Let's run it:: $ py.test -q F.. ================================= FAILURES ================================= - ________________________ TestClass.test_equals[1-2] ________________________ - - self = , a = 1, b = 2 - + ________________________ TestClass.test_equals[2-1] ________________________ + + self = , a = 1, b = 2 + def test_equals(self, a, b): > assert a == b E assert 1 == 2 - + test_parametrize.py:18: AssertionError + 1 failed, 2 passed in 0.01 seconds Indirect parametrization with multiple fixtures -------------------------------------------------------------- @@ -278,6 +282,7 @@ Running it results in some skips if we don't have all the python interpreters in ............sss............sss............sss............ssssssssssssssssss ========================= short test summary info ========================== SKIP [27] /home/hpk/p/pytest/doc/en/example/multipython.py:21: 'python2.8' not found + 48 passed, 27 skipped in 1.34 seconds Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- @@ -285,7 +290,7 @@ Indirect parametrization of optional implementations/imports If you want to compare the outcomes of several implementations of a given API, you can write test functions that receive the already imported implementations and get skipped in case the implementation is not importable/available. Let's -say we have a "base" implementation and the other (possibly optimized ones) +say we have a "base" implementation and the other (possibly optimized ones) need to provide similar results:: # content of conftest.py @@ -324,26 +329,26 @@ If you run this with reporting for skips enabled:: $ py.test -rs test_module.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 2 items - + test_module.py .s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-275/conftest.py:10: could not import 'opt2' - + SKIP [1] /tmp/doc-exec-65/conftest.py:10: could not import 'opt2' + =================== 1 passed, 1 skipped in 0.01 seconds ==================== You'll see that we don't have a ``opt2`` module and thus the second test run of our ``test_func1`` was skipped. A few notes: - the fixture functions in the ``conftest.py`` file are "session-scoped" because we - don't need to import more than once + don't need to import more than once - if you have multiple test functions and a skipped import, you will see the ``[1]`` count increasing in the report - you can put :ref:`@pytest.mark.parametrize <@pytest.mark.parametrize>` style - parametrization on the test functions to parametrize input/output + parametrization on the test functions to parametrize input/output values as well. diff --git a/doc/en/example/pythoncollection.txt b/doc/en/example/pythoncollection.txt index 2f65b1faf..4a23d39dc 100644 --- a/doc/en/example/pythoncollection.txt +++ b/doc/en/example/pythoncollection.txt @@ -10,7 +10,7 @@ You can set the :confval:`norecursedirs` option in an ini-file, for example your [pytest] norecursedirs = .svn _build tmp* -This would tell py.test to not recurse into typical subversion or sphinx-build directories or into any ``tmp`` prefixed directory. +This would tell ``pytest`` to not recurse into typical subversion or sphinx-build directories or into any ``tmp`` prefixed directory. .. _`change naming conventions`: @@ -28,7 +28,7 @@ the :confval:`python_files`, :confval:`python_classes` and python_classes=Check python_functions=check -This would make py.test look for ``check_`` prefixes in +This would make ``pytest`` look for ``check_`` prefixes in Python filenames, ``Check`` prefixes in classes and ``check`` prefixes in functions and classes. For example, if we have:: @@ -43,20 +43,26 @@ then the test collection looks like this:: $ py.test --collect-only =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 2 items - + ============================= in 0.01 seconds ============================= +.. note:: + + the ``python_functions`` and ``python_classes`` has no effect + for ``unittest.TestCase`` test discovery because pytest delegates + detection of test case methods to unittest code. + Interpreting cmdline arguments as Python packages ----------------------------------------------------- -You can use the ``--pyargs`` option to make py.test try +You can use the ``--pyargs`` option to make ``pytest`` try interpreting arguments as python package names, deriving their file system path and then running the test. For example if you have unittest2 installed you can type:: @@ -82,7 +88,7 @@ You can always peek at the collection tree without running tests like this:: . $ py.test --collect-only pythoncollection.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 3 items @@ -90,7 +96,7 @@ You can always peek at the collection tree without running tests like this:: - + ============================= in 0.01 seconds ============================= customizing test collection to find all .py files @@ -98,7 +104,7 @@ customizing test collection to find all .py files .. regendoc:wipe -You can easily instruct py.test to discover tests from every python file:: +You can easily instruct ``pytest`` to discover tests from every python file:: # content of pytest.ini @@ -106,8 +112,8 @@ You can easily instruct py.test to discover tests from every python file:: python_files = *.py However, many projects will have a ``setup.py`` which they don't want to be imported. Moreover, there may files only importable by a specific python version. -For such cases you can dynamically define files to be ignored by listing -them in a ``conftest.py`` file:: +For such cases you can dynamically define files to be ignored by listing +them in a ``conftest.py`` file:: # content of conftest.py import sys @@ -130,16 +136,16 @@ and a setup.py dummy file like this:: # content of setup.py 0/0 # will raise exeption if imported -then a pytest run on python2 will find the one test when run with a python2 +then a pytest run on python2 will find the one test when run with a python2 interpreters and will leave out the setup.py file:: - + $ py.test --collect-only =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 1 items - + ============================= in 0.01 seconds ============================= If you run with a Python3 interpreter the moduled added through the conftest.py file will not be considered for test collection. diff --git a/doc/en/example/reportingdemo.txt b/doc/en/example/reportingdemo.txt index 71e875270..c9b2fe4bb 100644 --- a/doc/en/example/reportingdemo.txt +++ b/doc/en/example/reportingdemo.txt @@ -1,11 +1,11 @@ .. _`tbreportdemo`: - -Demo of Python failure reports with py.test + +Demo of Python failure reports with pytest ================================================== Here is a nice run of several tens of failures -and how py.test presents things (unfortunately +and how ``pytest`` presents things (unfortunately not showing the nice colors here in the HTML that you get on the terminal - we are working on that): @@ -13,84 +13,84 @@ get on the terminal - we are working on that): assertion $ py.test failure_demo.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 39 items - + failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - + ================================= FAILURES ================================= ____________________________ test_generative[0] ____________________________ - + param1 = 3, param2 = 6 - + def test_generative(param1, param2): > assert param1 * 2 < param2 E assert (3 * 2) < 6 - + failure_demo.py:15: AssertionError _________________________ TestFailing.test_simple __________________________ - - self = - + + self = + def test_simple(self): def f(): return 42 def g(): return 43 - + > assert f() == g() E assert 42 == 43 - E + where 42 = () - E + and 43 = () - + E + where 42 = () + E + and 43 = () + failure_demo.py:28: AssertionError ____________________ TestFailing.test_simple_multiline _____________________ - - self = - + + self = + def test_simple_multiline(self): otherfunc_multi( 42, > 6*9) - - failure_demo.py:33: - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - + + failure_demo.py:33: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + a = 42, b = 54 - + def otherfunc_multi(a,b): > assert (a == b) E assert 42 == 54 - + failure_demo.py:11: AssertionError ___________________________ TestFailing.test_not ___________________________ - - self = - + + self = + def test_not(self): def f(): return 42 > assert not f() E assert not 42 - E + where 42 = () - + E + where 42 = () + failure_demo.py:38: AssertionError _________________ TestSpecialisedExplanations.test_eq_text _________________ - - self = - + + self = + def test_eq_text(self): > assert 'spam' == 'eggs' E assert 'spam' == 'eggs' E - spam E + eggs - + failure_demo.py:42: AssertionError _____________ TestSpecialisedExplanations.test_eq_similar_text _____________ - - self = - + + self = + def test_eq_similar_text(self): > assert 'foo 1 bar' == 'foo 2 bar' E assert 'foo 1 bar' == 'foo 2 bar' @@ -98,12 +98,12 @@ get on the terminal - we are working on that): E ? ^ E + foo 2 bar E ? ^ - + failure_demo.py:45: AssertionError ____________ TestSpecialisedExplanations.test_eq_multiline_text ____________ - - self = - + + self = + def test_eq_multiline_text(self): > assert 'foo\nspam\nbar' == 'foo\neggs\nbar' E assert 'foo\nspam\nbar' == 'foo\neggs\nbar' @@ -111,12 +111,12 @@ get on the terminal - we are working on that): E - spam E + eggs E bar - + failure_demo.py:48: AssertionError ______________ TestSpecialisedExplanations.test_eq_long_text _______________ - - self = - + + self = + def test_eq_long_text(self): a = '1'*100 + 'a' + '2'*100 b = '1'*100 + 'b' + '2'*100 @@ -128,12 +128,12 @@ get on the terminal - we are working on that): E ? ^ E + 1111111111b222222222 E ? ^ - + failure_demo.py:53: AssertionError _________ TestSpecialisedExplanations.test_eq_long_text_multiline __________ - - self = - + + self = + def test_eq_long_text_multiline(self): a = '1\n'*100 + 'a' + '2\n'*100 b = '1\n'*100 + 'b' + '2\n'*100 @@ -152,50 +152,50 @@ get on the terminal - we are working on that): E 2 E 2 E 2 - + failure_demo.py:58: AssertionError _________________ TestSpecialisedExplanations.test_eq_list _________________ - - self = - + + self = + def test_eq_list(self): > assert [0, 1, 2] == [0, 1, 3] E assert [0, 1, 2] == [0, 1, 3] E At index 2 diff: 2 != 3 - + failure_demo.py:61: AssertionError ______________ TestSpecialisedExplanations.test_eq_list_long _______________ - - self = - + + self = + def test_eq_list_long(self): a = [0]*100 + [1] + [3]*100 b = [0]*100 + [2] + [3]*100 > assert a == b E assert [0, 0, 0, 0, 0, 0, ...] == [0, 0, 0, 0, 0, 0, ...] E At index 100 diff: 1 != 2 - + failure_demo.py:66: AssertionError _________________ TestSpecialisedExplanations.test_eq_dict _________________ - - self = - + + self = + def test_eq_dict(self): > assert {'a': 0, 'b': 1, 'c': 0} == {'a': 0, 'b': 2, 'd': 0} E assert {'a': 0, 'b': 1, 'c': 0} == {'a': 0, 'b': 2, 'd': 0} - E Hiding 1 identical items, use -v to show + E Omitting 1 identical items, use -v to show E Differing items: E {'b': 1} != {'b': 2} E Left contains more items: E {'c': 0} E Right contains more items: E {'d': 0} - + failure_demo.py:69: AssertionError _________________ TestSpecialisedExplanations.test_eq_set __________________ - - self = - + + self = + def test_eq_set(self): > assert set([0, 10, 11, 12]) == set([0, 20, 21]) E assert set([0, 10, 11, 12]) == set([0, 20, 21]) @@ -206,31 +206,31 @@ get on the terminal - we are working on that): E Extra items in the right set: E 20 E 21 - + failure_demo.py:72: AssertionError _____________ TestSpecialisedExplanations.test_eq_longer_list ______________ - - self = - + + self = + def test_eq_longer_list(self): > assert [1,2] == [1,2,3] E assert [1, 2] == [1, 2, 3] E Right contains more items, first extra item: 3 - + failure_demo.py:75: AssertionError _________________ TestSpecialisedExplanations.test_in_list _________________ - - self = - + + self = + def test_in_list(self): > assert 1 in [0, 2, 3, 4, 5] E assert 1 in [0, 2, 3, 4, 5] - + failure_demo.py:78: AssertionError __________ TestSpecialisedExplanations.test_not_in_text_multiline __________ - - self = - + + self = + def test_not_in_text_multiline(self): text = 'some multiline\ntext\nwhich\nincludes foo\nand a\ntail' > assert 'foo' not in text @@ -243,12 +243,12 @@ get on the terminal - we are working on that): E ? +++ E and a E tail - + failure_demo.py:82: AssertionError ___________ TestSpecialisedExplanations.test_not_in_text_single ____________ - - self = - + + self = + def test_not_in_text_single(self): text = 'single foo line' > assert 'foo' not in text @@ -256,58 +256,58 @@ get on the terminal - we are working on that): E 'foo' is contained here: E single foo line E ? +++ - + failure_demo.py:86: AssertionError _________ TestSpecialisedExplanations.test_not_in_text_single_long _________ - - self = - + + self = + def test_not_in_text_single_long(self): text = 'head ' * 50 + 'foo ' + 'tail ' * 20 > assert 'foo' not in text E assert 'foo' not in 'head head head head hea...ail tail tail tail tail ' E 'foo' is contained here: - E head head foo tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail + E head head foo tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? +++ - + failure_demo.py:90: AssertionError ______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______ - - self = - + + self = + def test_not_in_text_single_long_term(self): text = 'head ' * 50 + 'f'*70 + 'tail ' * 20 > assert 'f'*70 not in text E assert 'fffffffffff...ffffffffffff' not in 'head head he...l tail tail ' E 'ffffffffffffffffff...fffffffffffffffffff' is contained here: - E head head fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffftail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail + E head head fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffftail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - + failure_demo.py:94: AssertionError ______________________________ test_attribute ______________________________ - + def test_attribute(): class Foo(object): b = 1 i = Foo() > assert i.b == 2 E assert 1 == 2 - E + where 1 = .b - + E + where 1 = .b + failure_demo.py:101: AssertionError _________________________ test_attribute_instance __________________________ - + def test_attribute_instance(): class Foo(object): b = 1 > assert Foo().b == 2 E assert 1 == 2 - E + where 1 = .b - E + where = () - + E + where 1 = .b + E + where = () + failure_demo.py:107: AssertionError __________________________ test_attribute_failure __________________________ - + def test_attribute_failure(): class Foo(object): def _get_b(self): @@ -315,19 +315,19 @@ get on the terminal - we are working on that): b = property(_get_b) i = Foo() > assert i.b == 2 - - failure_demo.py:116: - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - - self = - + + failure_demo.py:116: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + self = + def _get_b(self): > raise Exception('Failed to get attrib') E Exception: Failed to get attrib - + failure_demo.py:113: Exception _________________________ test_attribute_multiple __________________________ - + def test_attribute_multiple(): class Foo(object): b = 1 @@ -335,78 +335,78 @@ get on the terminal - we are working on that): b = 2 > assert Foo().b == Bar().b E assert 1 == 2 - E + where 1 = .b - E + where = () - E + and 2 = .b - E + where = () - + E + where 1 = .b + E + where = () + E + and 2 = .b + E + where = () + failure_demo.py:124: AssertionError __________________________ TestRaises.test_raises __________________________ - - self = - + + self = + def test_raises(self): s = 'qwe' > raises(TypeError, "int(s)") - - failure_demo.py:133: - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - + + failure_demo.py:133: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + > int(s) E ValueError: invalid literal for int() with base 10: 'qwe' - - <0-codegen /home/hpk/p/pytest/.tox/regen/local/lib/python2.7/site-packages/_pytest/python.py:858>:1: ValueError + + <0-codegen /home/hpk/p/pytest/.tox/regen/local/lib/python2.7/site-packages/_pytest/python.py:983>:1: ValueError ______________________ TestRaises.test_raises_doesnt _______________________ - - self = - + + self = + def test_raises_doesnt(self): > raises(IOError, "int('3')") E Failed: DID NOT RAISE - + failure_demo.py:136: Failed __________________________ TestRaises.test_raise ___________________________ - - self = - + + self = + def test_raise(self): > raise ValueError("demo error") E ValueError: demo error - + failure_demo.py:139: ValueError ________________________ TestRaises.test_tupleerror ________________________ - - self = - + + self = + def test_tupleerror(self): > a,b = [1] E ValueError: need more than 1 value to unpack - + failure_demo.py:142: ValueError ______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______ - - self = - + + self = + def test_reinterpret_fails_with_print_for_the_fun_of_it(self): l = [1,2,3] print ("l is %r" % l) > a,b = l.pop() E TypeError: 'int' object is not iterable - + failure_demo.py:147: TypeError ----------------------------- Captured stdout ------------------------------ l is [1, 2, 3] ________________________ TestRaises.test_some_error ________________________ - - self = - + + self = + def test_some_error(self): > if namenotexi: E NameError: global name 'namenotexi' is not defined - + failure_demo.py:150: NameError ____________________ test_dynamic_compile_shows_nicely _____________________ - + def test_dynamic_compile_shows_nicely(): src = 'def foo():\n assert 1 == 0\n' name = 'abc-123' @@ -415,132 +415,132 @@ get on the terminal - we are working on that): py.builtin.exec_(code, module.__dict__) py.std.sys.modules[name] = module > module.foo() - - failure_demo.py:165: - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - + + failure_demo.py:165: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + def foo(): > assert 1 == 0 E assert 1 == 0 - + <2-codegen 'abc-123' /home/hpk/p/pytest/doc/en/example/assertion/failure_demo.py:162>:2: AssertionError ____________________ TestMoreErrors.test_complex_error _____________________ - - self = - + + self = + def test_complex_error(self): def f(): return 44 def g(): return 43 > somefunc(f(), g()) - - failure_demo.py:175: - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - + + failure_demo.py:175: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + x = 44, y = 43 - + def somefunc(x,y): > otherfunc(x,y) - - failure_demo.py:8: - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - + + failure_demo.py:8: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + a = 44, b = 43 - + def otherfunc(a,b): > assert a==b E assert 44 == 43 - + failure_demo.py:5: AssertionError ___________________ TestMoreErrors.test_z1_unpack_error ____________________ - - self = - + + self = + def test_z1_unpack_error(self): l = [] > a,b = l E ValueError: need more than 0 values to unpack - + failure_demo.py:179: ValueError ____________________ TestMoreErrors.test_z2_type_error _____________________ - - self = - + + self = + def test_z2_type_error(self): l = 3 > a,b = l E TypeError: 'int' object is not iterable - + failure_demo.py:183: TypeError ______________________ TestMoreErrors.test_startswith ______________________ - - self = - + + self = + def test_startswith(self): s = "123" g = "456" > assert s.startswith(g) - E assert ('456') - E + where = '123'.startswith - + E assert ('456') + E + where = '123'.startswith + failure_demo.py:188: AssertionError __________________ TestMoreErrors.test_startswith_nested ___________________ - - self = - + + self = + def test_startswith_nested(self): def f(): return "123" def g(): return "456" > assert f().startswith(g()) - E assert ('456') - E + where = '123'.startswith - E + where '123' = () - E + and '456' = () - + E assert ('456') + E + where = '123'.startswith + E + where '123' = () + E + and '456' = () + failure_demo.py:195: AssertionError _____________________ TestMoreErrors.test_global_func ______________________ - - self = - + + self = + def test_global_func(self): > assert isinstance(globf(42), float) E assert isinstance(43, float) E + where 43 = globf(42) - + failure_demo.py:198: AssertionError _______________________ TestMoreErrors.test_instance _______________________ - - self = - + + self = + def test_instance(self): self.x = 6*7 > assert self.x != 42 E assert 42 != 42 - E + where 42 = .x - + E + where 42 = .x + failure_demo.py:202: AssertionError _______________________ TestMoreErrors.test_compare ________________________ - - self = - + + self = + def test_compare(self): > assert globf(10) < 5 E assert 11 < 5 E + where 11 = globf(10) - + failure_demo.py:205: AssertionError _____________________ TestMoreErrors.test_try_finally ______________________ - - self = - + + self = + def test_try_finally(self): x = 1 try: > assert x == 0 E assert 1 == 0 - + failure_demo.py:210: AssertionError - ======================== 39 failed in 0.21 seconds ========================= + ======================== 39 failed in 0.20 seconds ========================= diff --git a/doc/en/example/simple.txt b/doc/en/example/simple.txt index 084855daa..cb4c757ac 100644 --- a/doc/en/example/simple.txt +++ b/doc/en/example/simple.txt @@ -41,9 +41,9 @@ Let's run this without supplying our new option:: F ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - + cmdopt = 'type1' - + def test_answer(cmdopt): if cmdopt == "type1": print ("first") @@ -51,10 +51,11 @@ Let's run this without supplying our new option:: print ("second") > assert 0 # to see what was printed E assert 0 - + test_sample.py:6: AssertionError ----------------------------- Captured stdout ------------------------------ first + 1 failed in 0.01 seconds And now with supplying a command line option:: @@ -62,9 +63,9 @@ And now with supplying a command line option:: F ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - + cmdopt = 'type2' - + def test_answer(cmdopt): if cmdopt == "type1": print ("first") @@ -72,10 +73,11 @@ And now with supplying a command line option:: print ("second") > assert 0 # to see what was printed E assert 0 - + test_sample.py:6: AssertionError ----------------------------- Captured stdout ------------------------------ second + 1 failed in 0.01 seconds You can see that the command line option arrived in our test. This completes the basic pattern. However, one often rather wants to process @@ -106,9 +108,9 @@ directory with the above conftest.py:: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 0 items - + ============================= in 0.00 seconds ============================= .. _`excontrolskip`: @@ -150,24 +152,24 @@ and when running it will see a skipped "slow" test:: $ py.test -rs # "-rs" means report details on the little 's' =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 2 items - + test_module.py .s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-278/conftest.py:9: need --runslow option to run - + SKIP [1] /tmp/doc-exec-68/conftest.py:9: need --runslow option to run + =================== 1 passed, 1 skipped in 0.01 seconds ==================== Or run it including the ``slow`` marked test:: $ py.test --runslow =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 2 items - + test_module.py .. - + ========================= 2 passed in 0.01 seconds ========================= Writing well integrated assertion helpers @@ -191,7 +193,7 @@ Example:: def test_something(): checkconfig(42) -The ``__tracebackhide__`` setting influences py.test showing +The ``__tracebackhide__`` setting influences ``pytest`` showing of tracebacks: the ``checkconfig`` function will not be shown unless the ``--fulltrace`` command line option is specified. Let's run our little function:: @@ -200,14 +202,15 @@ Let's run our little function:: F ================================= FAILURES ================================= ______________________________ test_something ______________________________ - + def test_something(): > checkconfig(42) E Failed: not configured: 42 - - test_checkconfig.py:8: Failed -Detect if running from within a py.test run + test_checkconfig.py:8: Failed + 1 failed in 0.01 seconds + +Detect if running from within a pytest run -------------------------------------------------------------- .. regendoc:wipe @@ -242,10 +245,10 @@ Adding info to test report header .. regendoc:wipe -It's easy to present extra information in a py.test run:: +It's easy to present extra information in a ``pytest`` run:: # content of conftest.py - + def pytest_report_header(config): return "project deps: mylib-1.1" @@ -253,10 +256,10 @@ which will add the string to the test header accordingly:: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 project deps: mylib-1.1 collected 0 items - + ============================= in 0.00 seconds ============================= .. regendoc:wipe @@ -276,20 +279,20 @@ which will add info only when run with "--v":: $ py.test -v =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 -- /home/hpk/p/pytest/.tox/regen/bin/python info1: did you know that ... did you? collecting ... collected 0 items - + ============================= in 0.00 seconds ============================= and nothing when run plainly:: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 0 items - + ============================= in 0.00 seconds ============================= profiling test duration @@ -319,11 +322,11 @@ Now we can profile which test functions execute the slowest:: $ py.test --durations=3 =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 3 items - + test_some_are_slow.py ... - + ========================= slowest 3 test durations ========================= 0.20s call test_some_are_slow.py::test_funcslow2 0.10s call test_some_are_slow.py::test_funcslow1 @@ -380,20 +383,20 @@ If we run this:: $ py.test -rx =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 4 items - + test_step.py .Fx. - + ================================= FAILURES ================================= ____________________ TestUserHandling.test_modification ____________________ - - self = - + + self = + def test_modification(self): > assert 0 E assert 0 - + test_step.py:9: AssertionError ========================= short test summary info ========================== XFAIL test_step.py::TestUserHandling::()::test_deletion @@ -445,55 +448,55 @@ the ``db`` fixture:: # content of b/test_error.py def test_root(db): # no db here, will error out pass - + We can run this:: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 7 items - + test_step.py .Fx. a/test_db.py F a/test_db2.py F b/test_error.py E - + ================================== ERRORS ================================== _______________________ ERROR at setup of test_root ________________________ - file /tmp/doc-exec-278/b/test_error.py, line 1 + file /tmp/doc-exec-68/b/test_error.py, line 1 def test_root(db): # no db here, will error out fixture 'db' not found - available fixtures: pytestconfig, recwarn, monkeypatch, capfd, capsys, tmpdir + available fixtures: recwarn, capfd, pytestconfig, capsys, tmpdir, monkeypatch use 'py.test --fixtures [testpath]' for help on them. - - /tmp/doc-exec-278/b/test_error.py:1 + + /tmp/doc-exec-68/b/test_error.py:1 ================================= FAILURES ================================= ____________________ TestUserHandling.test_modification ____________________ - - self = - + + self = + def test_modification(self): > assert 0 E assert 0 - + test_step.py:9: AssertionError _________________________________ test_a1 __________________________________ - - db = - + + db = + def test_a1(db): > assert 0, db # to show value - E AssertionError: - + E AssertionError: + a/test_db.py:2: AssertionError _________________________________ test_a2 __________________________________ - - db = - + + db = + def test_a2(db): > assert 0, db # to show value - E AssertionError: - + E AssertionError: + a/test_db2.py:2: AssertionError ========== 3 failed, 2 passed, 1 xfailed, 1 error in 0.03 seconds ========== @@ -509,7 +512,7 @@ post-process test reports / failures --------------------------------------- If you want to postprocess test reports and need access to the executing -environment you can implement a hook that gets called when the test +environment you can implement a hook that gets called when the test "report" object is about to be created. Here we write out all failing test calls and also access a fixture (if it was used by the test) in case you want to query/look at it during your post processing. In our @@ -526,7 +529,7 @@ case we just write some informations out to a ``failures`` file:: rep = __multicall__.execute() # we only look at actual failing test calls, not setup/teardown - if rep.when == "call" and rep.failed: + if rep.when == "call" and rep.failed: mode = "a" if os.path.exists("failures") else "w" with open("failures", mode) as f: # let's also access a fixture for the fun of it @@ -534,7 +537,7 @@ case we just write some informations out to a ``failures`` file:: extra = " (%s)" % item.funcargs["tmpdir"] else: extra = "" - + f.write(rep.nodeid + extra + "\n") return rep @@ -542,42 +545,42 @@ if you then have failing tests:: # content of test_module.py def test_fail1(tmpdir): - assert 0 + assert 0 def test_fail2(): - assert 0 - + assert 0 + and run them:: $ py.test test_module.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 2 items - + test_module.py FF - + ================================= FAILURES ================================= ________________________________ test_fail1 ________________________________ - - tmpdir = local('/tmp/pytest-326/test_fail10') - + + tmpdir = local('/tmp/pytest-42/test_fail10') + def test_fail1(tmpdir): > assert 0 E assert 0 - + test_module.py:2: AssertionError ________________________________ test_fail2 ________________________________ - + def test_fail2(): > assert 0 E assert 0 - + test_module.py:4: AssertionError - ========================= 2 failed in 0.02 seconds ========================= + ========================= 2 failed in 0.01 seconds ========================= you will have a "failures" file which contains the failing test ids:: $ cat failures - test_module.py::test_fail1 (/tmp/pytest-326/test_fail10) + test_module.py::test_fail1 (/tmp/pytest-42/test_fail10) test_module.py::test_fail2 Making test result information available in fixtures @@ -620,60 +623,60 @@ here is a little example implemented via a local plugin:: if you then have failing tests:: # content of test_module.py - + import pytest @pytest.fixture def other(): assert 0 - + def test_setup_fails(something, other): pass def test_call_fails(something): - assert 0 + assert 0 def test_fail2(): - assert 0 - + assert 0 + and run it:: $ py.test -s test_module.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 3 items - - test_module.py EFF - + + test_module.py Esetting up a test failed! test_module.py::test_setup_fails + Fexecuting test failed test_module.py::test_call_fails + F + ================================== ERRORS ================================== ____________________ ERROR at setup of test_setup_fails ____________________ - + @pytest.fixture def other(): > assert 0 E assert 0 - + test_module.py:6: AssertionError ================================= FAILURES ================================= _____________________________ test_call_fails ______________________________ - + something = None - + def test_call_fails(something): > assert 0 E assert 0 - + test_module.py:12: AssertionError ________________________________ test_fail2 ________________________________ - + def test_fail2(): > assert 0 E assert 0 - + test_module.py:15: AssertionError ==================== 2 failed, 1 error in 0.01 seconds ===================== - setting up a test failed! test_module.py::test_setup_fails - executing test failed test_module.py::test_call_fails You'll see that the fixture finalizers could use the precise reporting information. diff --git a/doc/en/example/special.txt b/doc/en/example/special.txt index 28051ce23..28a51af10 100644 --- a/doc/en/example/special.txt +++ b/doc/en/example/special.txt @@ -1,5 +1,4 @@ - -A sesssion-fixture which can look at all collected tests +A session-fixture which can look at all collected tests ---------------------------------------------------------------- A session-scoped fixture effectively has access to all @@ -61,12 +60,13 @@ will be called ahead of running any tests:: If you run this without output capturing:: $ py.test -q -s test_module.py - .... callattr_ahead_of_alltests called callme called! callme other called SomeTest callme called test_method1 called - test_method1 called - test other - test_unit1 method called + .test_method1 called + .test other + .test_unit1 method called + . + 4 passed in 0.01 seconds diff --git a/doc/en/faq.txt b/doc/en/faq.txt index 534c05eaf..b4312ac12 100644 --- a/doc/en/faq.txt +++ b/doc/en/faq.txt @@ -3,37 +3,38 @@ Some Issues and Questions .. note:: - If you don't find an answer here, you may checkout - `pytest Q&A at Stackoverflow `_ - or other :ref:`contact channels` to get help. + This FAQ is here only mostly for historic reasons. Checkout + `pytest Q&A at Stackoverflow `_ + for many questions and answers related to pytest and/or use + :ref:`contact channels` to get help. On naming, nosetests, licensing and magic ------------------------------------------------ -How does py.test relate to nose and unittest? +How does pytest relate to nose and unittest? +++++++++++++++++++++++++++++++++++++++++++++++++ -py.test and nose_ share basic philosophy when it comes +``pytest`` and nose_ share basic philosophy when it comes to running and writing Python tests. In fact, you can run many tests -written for nose with py.test. nose_ was originally created -as a clone of ``py.test`` when py.test was in the ``0.8`` release +written for nose with ``pytest``. nose_ was originally created +as a clone of ``pytest`` when ``pytest`` was in the ``0.8`` release cycle. Note that starting with pytest-2.0 support for running unittest test suites is majorly improved. -how does py.test relate to twisted's trial? +how does pytest relate to twisted's trial? ++++++++++++++++++++++++++++++++++++++++++++++ -Since some time py.test has builtin support for supporting tests +Since some time ``pytest`` has builtin support for supporting tests written using trial. It does not itself start a reactor, however, -and does not handle Deferreds returned from a test in pytest style. +and does not handle Deferreds returned from a test in pytest style. If you are using trial's unittest.TestCase chances are that you can just run your tests even if you return Deferreds. In addition, there also is a dedicated `pytest-twisted `_ plugin which allows to return deferreds from pytest-style tests, allowing to use :ref:`fixtures` and other features. - -how does py.test work with Django? + +how does pytest work with Django? ++++++++++++++++++++++++++++++++++++++++++++++ In 2012, some work is going into the `pytest-django plugin `_. It substitutes the usage of Django's @@ -43,36 +44,36 @@ are not available from Django directly. .. _features: features.html -What's this "magic" with py.test? (historic notes) +What's this "magic" with pytest? (historic notes) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -Around 2007 (version ``0.8``) some people thought that py.test +Around 2007 (version ``0.8``) some people thought that ``pytest`` was using too much "magic". It had been part of the `pylib`_ which contains a lot of unreleated python library code. Around 2010 there -was a major cleanup refactoring, which removed unused or deprecated code +was a major cleanup refactoring, which removed unused or deprecated code and resulted in the new ``pytest`` PyPI package which strictly contains -only test-related code. This relese also brought a complete pluginification +only test-related code. This release also brought a complete pluginification such that the core is around 300 lines of code and everything else is -implemented in plugins. Thus ``pytest`` today is a small, universally runnable +implemented in plugins. Thus ``pytest`` today is a small, universally runnable and customizable testing framework for Python. Note, however, that -``pytest`` uses metaprogramming techniques and reading its source is +``pytest`` uses metaprogramming techniques and reading its source is thus likely not something for Python beginners. -A second "magic" issue was the assert statement debugging feature. -Nowadays, py.test explicitely rewrites assert statements in test modules +A second "magic" issue was the assert statement debugging feature. +Nowadays, ``pytest`` explicitely rewrites assert statements in test modules in order to provide more useful :ref:`assert feedback `. -This completely avoids previous issues of confusing assertion-reporting. +This completely avoids previous issues of confusing assertion-reporting. It also means, that you can use Python's ``-O`` optimization without loosing assertions in test modules. -py.test contains a second mostly obsolete assert debugging technique, +``pytest`` contains a second, mostly obsolete, assert debugging technique, invoked via ``--assert=reinterpret``, activated by default on -Python-2.5: When an ``assert`` statement fails, py.test re-interprets +Python-2.5: When an ``assert`` statement fails, ``pytest`` re-interprets the expression part to show intermediate values. This technique suffers from a caveat that the rewriting does not: If your expression has side effects (better to avoid them anyway!) the intermediate values may not be the same, confusing the reinterpreter and obfuscating the initial -error (this is also explained at the command line if it happens). +error (this is also explained at the command line if it happens). You can also turn off all assertion interaction using the ``--assertmode=off`` option. @@ -84,7 +85,7 @@ You can also turn off all assertion interaction using the Why a ``py.test`` instead of a ``pytest`` command? ++++++++++++++++++++++++++++++++++++++++++++++++++ -Some of the reasons are historic, others are practical. ``py.test`` +Some of the reasons are historic, others are practical. ``pytest`` used to be part of the ``py`` package which provided several developer utilities, all starting with ``py.``, thus providing nice TAB-completion. If @@ -94,12 +95,12 @@ but since many people have gotten used to the old name and there is another tool named "pytest" we just decided to stick with ``py.test`` for now. -Function arguments, parametrized tests and setup +pytest fixtures, parametrized tests ------------------------------------------------------- .. _funcargs: funcargs.html -Is using funcarg- versus xUnit setup a style question? +Is using pytest fixtures versus xUnit setup a style question? +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ For simple applications and for people experienced with nose_ or @@ -117,20 +118,6 @@ in a managed class/module/function scope. .. _`why pytest_pyfuncarg__ methods?`: -Why the ``pytest_funcarg__*`` name for funcarg factories? -++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -We like `Convention over Configuration`_ and didn't see much point -in allowing a more flexible or abstract mechanism. Moreover, -it is nice to be able to search for ``pytest_funcarg__MYARG`` in -source code and safely find all factory functions for -the ``MYARG`` function argument. - -.. note:: - - With pytest-2.3 you can use the :ref:`@pytest.fixture` decorator - to mark a function as a fixture function. - .. _`Convention over Configuration`: http://en.wikipedia.org/wiki/Convention_over_Configuration Can I yield multiple values from a fixture function function? @@ -139,30 +126,30 @@ Can I yield multiple values from a fixture function function? There are two conceptual reasons why yielding from a factory function is not possible: -* Calling factories for obtaining test function arguments - is part of setting up and running a test. At that - point it is not possible to add new test calls to - the test collection anymore. - * If multiple factories yielded values there would be no natural place to determine the combination policy - in real-world examples some combinations often should not run. +* Calling factories for obtaining test function arguments + is part of setting up and running a test. At that + point it is not possible to add new test calls to + the test collection anymore. + However, with pytest-2.3 you can use the :ref:`@pytest.fixture` decorator and specify ``params`` so that all tests depending on the factory-created resource will run multiple times with different parameters. -You can also use the `pytest_generate_tests`_ hook to +You can also use the `pytest_generate_tests`_ hook to implement the `parametrization scheme of your choice`_. .. _`pytest_generate_tests`: test/funcargs.html#parametrizing-tests .. _`parametrization scheme of your choice`: http://tetamap.wordpress.com/2009/05/13/parametrizing-python-tests-generalized/ -py.test interaction with other packages +pytest interaction with other packages --------------------------------------------------- -Issues with py.test, multiprocess and setuptools? +Issues with pytest, multiprocess and setuptools? +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ On windows the multiprocess package will instantiate sub processes diff --git a/doc/en/fixture.txt b/doc/en/fixture.txt index b7b3df77e..ba05431a1 100644 --- a/doc/en/fixture.txt +++ b/doc/en/fixture.txt @@ -15,7 +15,7 @@ pytest fixtures: explicit, modular, scalable The `purpose of test fixtures`_ is to provide a fixed baseline upon which tests can reliably and repeatedly execute. pytest fixtures -offer dramatic improvements over the classic xUnit style of setup/teardown +offer dramatic improvements over the classic xUnit style of setup/teardown functions: * fixtures have explicit names and are activated by declaring their use @@ -34,17 +34,23 @@ both styles, moving incrementally from classic to new style, as you prefer. You can also start out from existing :ref:`unittest.TestCase style ` or :ref:`nose based ` projects. +.. note:: + + pytest-2.4 introduced an additional experimental + :ref:`yield fixture mechanism ` for easier context manager + integration and more linear writing of teardown code. + .. _`funcargs`: .. _`funcarg mechanism`: .. _`fixture function`: .. _`@pytest.fixture`: .. _`pytest.fixture`: -Fixtures as Function arguments (funcargs) +Fixtures as Function arguments ----------------------------------------- Test functions can receive fixture objects by naming them as an input -argument. For each argument name, a fixture function with that name provides +argument. For each argument name, a fixture function with that name provides the fixture object. Fixture functions are registered by marking them with :py:func:`@pytest.fixture <_pytest.python.fixture>`. Let's look at a simple self-contained test module containing a fixture and a test function @@ -64,36 +70,36 @@ using it:: assert "merlinux" in msg assert 0 # for demo purposes -Here, the ``test_ehlo`` needs the ``smtp`` fixture value. pytest -will discover and call the :py:func:`@pytest.fixture <_pytest.python.fixture>` +Here, the ``test_ehlo`` needs the ``smtp`` fixture value. pytest +will discover and call the :py:func:`@pytest.fixture <_pytest.python.fixture>` marked ``smtp`` fixture function. Running the test looks like this:: $ py.test test_smtpsimple.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 1 items - + test_smtpsimple.py F - + ================================= FAILURES ================================= ________________________________ test_ehlo _________________________________ - - smtp = - + + smtp = + def test_ehlo(smtp): response, msg = smtp.ehlo() assert response == 250 assert "merlinux" in msg > assert 0 # for demo purposes E assert 0 - + test_smtpsimple.py:12: AssertionError - ========================= 1 failed in 0.20 seconds ========================= + ========================= 1 failed in 0.21 seconds ========================= In the failure traceback we see that the test function was called with a ``smtp`` argument, the ``smtplib.SMTP()`` instance created by the fixture function. The test function fails on our deliberate ``assert 0``. Here is -an exact protocol of how py.test comes to call the test function this way: +the exact protocol used by ``pytest`` to call the test function this way: 1. pytest :ref:`finds ` the ``test_ehlo`` because of the ``test_`` prefix. The test function needs a function argument @@ -119,11 +125,11 @@ with a list of available function arguments. In versions prior to 2.3 there was no ``@pytest.fixture`` marker and you had to use a magic ``pytest_funcarg__NAME`` prefix - for the fixture factory. This remains and will remain supported + for the fixture factory. This remains and will remain supported but is not anymore advertised as the primary means of declaring fixture functions. -Funcargs a prime example of dependency injection +"Funcargs" a prime example of dependency injection --------------------------------------------------- When injecting fixtures to test functions, pytest-2.0 introduced the @@ -142,20 +148,20 @@ functions take the role of the *injector* and test functions are the .. _smtpshared: -Working with a module-shared fixture +Sharing a fixture across tests in a module (or class/session) ----------------------------------------------------------------- .. regendoc:wipe -Fixtures requiring network access depend on connectivity and are +Fixtures requiring network access depend on connectivity and are usually time-expensive to create. Extending the previous example, we -can add a ``scope='module'`` parameter to the +can add a ``scope='module'`` parameter to the :py:func:`@pytest.fixture <_pytest.python.fixture>` invocation -to cause the decorated ``smtp`` fixture function to only be invoked once +to cause the decorated ``smtp`` fixture function to only be invoked once per test module. Multiple test functions in a test module will thus each receive the same ``smtp`` fixture instance. The next example puts the fixture function into a separate ``conftest.py`` file so -that tests from multiple test modules in the directory can +that tests from multiple test modules in the directory can access the fixture function:: # content of conftest.py @@ -174,7 +180,7 @@ function (in or below the directory where ``conftest.py`` is located):: def test_ehlo(smtp): response = smtp.ehlo() - assert response[0] == 250 + assert response[0] == 250 assert "merlinux" in response[1] assert 0 # for demo purposes @@ -188,107 +194,62 @@ inspect what is going on and can now run the tests:: $ py.test test_module.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 2 items - + test_module.py FF - + ================================= FAILURES ================================= ________________________________ test_ehlo _________________________________ - - smtp = - + + smtp = + def test_ehlo(smtp): response = smtp.ehlo() assert response[0] == 250 assert "merlinux" in response[1] > assert 0 # for demo purposes E assert 0 - + test_module.py:6: AssertionError ________________________________ test_noop _________________________________ - - smtp = - + + smtp = + def test_noop(smtp): response = smtp.noop() assert response[0] == 250 > assert 0 # for demo purposes E assert 0 - - test_module.py:11: AssertionError - ========================= 2 failed in 0.26 seconds ========================= -You see the two ``assert 0`` failing and more importantly you can also see -that the same (module-scoped) ``smtp`` object was passed into the two -test functions because pytest shows the incoming argument values in the + test_module.py:11: AssertionError + ========================= 2 failed in 0.17 seconds ========================= + +You see the two ``assert 0`` failing and more importantly you can also see +that the same (module-scoped) ``smtp`` object was passed into the two +test functions because pytest shows the incoming argument values in the traceback. As a result, the two test functions using ``smtp`` run as quick as a single one because they reuse the same instance. If you decide that you rather want to have a session-scoped ``smtp`` instance, you can simply declare it:: - @pytest.fixture(scope=``session``) + @pytest.fixture(scope="session") def smtp(...): - # the returned fixture value will be shared for + # the returned fixture value will be shared for # all tests needing it -.. _`contextfixtures`: - -fixture finalization / teardowns +.. _`finalization`: + +fixture finalization / executing teardown code ------------------------------------------------------------- -pytest supports two styles of fixture finalization: +pytest supports execution of fixture specific finalization code +when the fixture goes out of scope. By accepting a ``request`` object +into your fixture function you can call its ``request.addfinalizer`` one +or multiple times:: -- (new in pytest-2.4) by writing a contextmanager fixture - generator where a fixture value is "yielded" and the remainder - of the function serves as the teardown code. This integrates - very well with existing context managers. - -- by making a fixture function accept a ``request`` argument - with which it can call ``request.addfinalizer(teardownfunction)`` - to register a teardown callback function. - -Both methods are strictly equivalent from pytest's view and will -remain supported in the future. - -Because a number of people prefer the new contextmanager style -we describe it first:: - - # content of test_ctxfixture.py - - import smtplib - import pytest - - @pytest.fixture(scope="module") - def smtp(): - smtp = smtplib.SMTP("merlinux.eu") - yield smtp # provide the fixture value - print ("teardown smtp") - smtp.close() - -pytest detects that you are using a ``yield`` in your fixture function, -turns it into a generator and: - -a) iterates once into it for producing the value -b) iterates a second time for tearing the fixture down, expecting - a StopIteration (which is produced automatically from the Python - runtime when the generator returns). - -.. note:: - - The teardown will execute independently of the status of test functions. - You do not need to write the teardown code into a ``try-finally`` clause - like you would usually do with ``contextlib.contextmanager`` decorated - functions. - - If the fixture generator yields a second value pytest will report - an error. Yielding cannot be used for parametrization. We'll describe - ways to implement parametrization further below. - -Prior to pytest-2.4 you always needed to register a finalizer by accepting -a ``request`` object into your fixture function and calling -``request.addfinalizer`` with a teardown function:: + # content of conftest.py import smtplib import pytest @@ -299,24 +260,36 @@ a ``request`` object into your fixture function and calling def fin(): print ("teardown smtp") smtp.close() + request.addfinalizer(fin) return smtp # provide the fixture value -This method of registering a finalizer reads more indirect -than the new contextmanager style syntax because ``fin`` -is a callback function. +The ``fin`` function will execute when the last test using +the fixture in the module has finished execution. + +Let's execute it:: + + $ py.test -s -q --tb=no + FFteardown smtp + + 2 failed in 0.17 seconds + +We see that the ``smtp`` instance is finalized after the two +tests finished execution. Note that if we decorated our fixture +function with ``scope='function'`` then fixture setup and cleanup would +occur around each single test. In either case the test +module itself does not need to change or know about these details +of fixture setup. .. _`request-context`: -Fixtures can interact with the requesting test context +Fixtures can introspect the requesting test context ------------------------------------------------------------- -pytest provides a builtin :py:class:`request ` object, -which fixture functions can use to introspect the function, class or module -for which they are invoked. - -Further extending the previous ``smtp`` fixture example, let's -read an optional server URL from the module namespace:: +Fixture function can accept the :py:class:`request ` object +to introspect the "requesting" test function, class or module context. +Further extending the previous ``smtp`` fixture example, let's +read an optional server URL from the test module which uses our fixture:: # content of conftest.py import pytest @@ -326,28 +299,26 @@ read an optional server URL from the module namespace:: def smtp(request): server = getattr(request.module, "smtpserver", "merlinux.eu") smtp = smtplib.SMTP(server) - yield smtp # provide the fixture - print ("finalizing %s" % smtp) - smtp.close() -The finalizing part after the ``yield smtp`` statement will execute -when the last test using the ``smtp`` fixture has executed:: + def fin(): + print ("finalizing %s (%s)" % (smtp, server)) + smtp.close() + + return smtp + +We use the ``request.module`` attribute to optionally obtain an +``smtpserver`` attribute from the test module. If we just execute +again, nothing much has changed:: $ py.test -s -q --tb=no FF - finalizing - -We see that the ``smtp`` instance is finalized after the two -tests which use it finished executin. If we rather specify -``scope='function'`` then fixture setup and cleanup occurs -around each single test. Note that in either case the test -module itself does not need to change! + 2 failed in 0.21 seconds Let's quickly create another test module that actually sets the server URL in its module namespace:: - + # content of test_anothersmtp.py - + smtpserver = "mail.python.org" # will be read by smtp fixture def test_showhelo(smtp): @@ -366,7 +337,6 @@ Running it:: voila! The ``smtp`` fixture function picked up our mail server name from the module namespace. - .. _`fixture-parametrize`: Parametrizing a fixture @@ -376,7 +346,7 @@ Fixture functions can be parametrized in which case they will be called multiple times, each time executing the set of dependent tests, i. e. the tests that depend on this fixture. Test functions do usually not need to be aware of their re-running. Fixture parametrization helps to -write exhaustive functional tests for components which themselves can be +write exhaustive functional tests for components which themselves can be configured in multiple ways. Extending the previous example, we can flag the fixture to create two @@ -388,72 +358,77 @@ through the special :py:class:`request ` object:: import pytest import smtplib - @pytest.fixture(scope="module", + @pytest.fixture(scope="module", params=["merlinux.eu", "mail.python.org"]) def smtp(request): smtp = smtplib.SMTP(request.param) - yield smtp - print ("finalizing %s" % smtp) - smtp.close() + def fin(): + print ("finalizing %s" % smtp) + smtp.close() + request.addfinalizer(fin) + return smtp -The main change is the declaration of ``params`` with +The main change is the declaration of ``params`` with :py:func:`@pytest.fixture <_pytest.python.fixture>`, a list of values for each of which the fixture function will execute and can access -a value via ``request.param``. No test function code needs to change. +a value via ``request.param``. No test function code needs to change. So let's just do another run:: $ py.test -q test_module.py FFFF ================================= FAILURES ================================= __________________________ test_ehlo[merlinux.eu] __________________________ - - smtp = - + + smtp = + def test_ehlo(smtp): response = smtp.ehlo() assert response[0] == 250 assert "merlinux" in response[1] > assert 0 # for demo purposes E assert 0 - + test_module.py:6: AssertionError __________________________ test_noop[merlinux.eu] __________________________ - - smtp = - + + smtp = + def test_noop(smtp): response = smtp.noop() assert response[0] == 250 > assert 0 # for demo purposes E assert 0 - + test_module.py:11: AssertionError ________________________ test_ehlo[mail.python.org] ________________________ - - smtp = - + + smtp = + def test_ehlo(smtp): response = smtp.ehlo() assert response[0] == 250 > assert "merlinux" in response[1] E assert 'merlinux' in 'mail.python.org\nSIZE 25600000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' - + test_module.py:5: AssertionError + ----------------------------- Captured stdout ------------------------------ + finalizing ________________________ test_noop[mail.python.org] ________________________ - - smtp = - + + smtp = + def test_noop(smtp): response = smtp.noop() assert response[0] == 250 > assert 0 # for demo purposes E assert 0 - + test_module.py:11: AssertionError + 4 failed in 6.58 seconds We see that our two test functions each ran twice, against the different -``smtp`` instances. Note also, that with the ``mail.python.org`` -connection the second test fails in ``test_ehlo`` because a +``smtp`` instances. Note also, that with the ``mail.python.org`` +connection the second test fails in ``test_ehlo`` because a different server string is expected than what arrived. @@ -470,7 +445,7 @@ and instantiate an object ``app`` where we stick the already defined ``smtp`` resource into it:: # content of test_appsetup.py - + import pytest class App: @@ -489,22 +464,22 @@ Here we declare an ``app`` fixture which receives the previously defined $ py.test -v test_appsetup.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 -- /home/hpk/p/pytest/.tox/regen/bin/python collecting ... collected 2 items - + test_appsetup.py:12: test_smtp_exists[merlinux.eu] PASSED test_appsetup.py:12: test_smtp_exists[mail.python.org] PASSED - - ========================= 2 passed in 5.38 seconds ========================= + + ========================= 2 passed in 5.95 seconds ========================= Due to the parametrization of ``smtp`` the test will run twice with two different ``App`` instances and respective smtp servers. There is no -need for the ``app`` fixture to be aware of the ``smtp`` parametrization -as pytest will fully analyse the fixture dependency graph. +need for the ``app`` fixture to be aware of the ``smtp`` parametrization +as pytest will fully analyse the fixture dependency graph. Note, that the ``app`` fixture has a scope of ``module`` and uses a module-scoped ``smtp`` fixture. The example would still work if ``smtp`` -was cached on a ``session`` scope: it is fine for fixtures to use +was cached on a ``session`` scope: it is fine for fixtures to use "broader" scoped fixtures but not the other way round: A session-scoped fixture could not use a module-scoped one in a meaningful way. @@ -519,11 +494,11 @@ Automatic grouping of tests by fixture instances pytest minimizes the number of active fixtures during test runs. If you have a parametrized fixture, then all the tests using it will -first execute with one instance and then finalizers are called +first execute with one instance and then finalizers are called before the next fixture instance is created. Among other things, this eases testing of applications which create and use global state. -The following example uses two parametrized funcargs, one of which is +The following example uses two parametrized funcargs, one of which is scoped on a per-module basis, and all the functions perform ``print`` calls to show the setup/teardown flow:: @@ -534,8 +509,9 @@ to show the setup/teardown flow:: def modarg(request): param = request.param print "create", param - yield param - print ("fin %s" % param) + def fin(): + print ("fin %s" % param) + return param @pytest.fixture(scope="function", params=[1,2]) def otherarg(request): @@ -552,34 +528,32 @@ Let's run the tests in verbose mode and with looking at the print-output:: $ py.test -v -s test_module.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 -- /home/hpk/p/pytest/.tox/regen/bin/python collecting ... collected 8 items - - test_module.py:16: test_0[1] PASSED - test_module.py:16: test_0[2] PASSED - test_module.py:18: test_1[mod1] PASSED - test_module.py:20: test_2[1-mod1] PASSED - test_module.py:20: test_2[2-mod1] PASSED - test_module.py:18: test_1[mod2] PASSED - test_module.py:20: test_2[1-mod2] PASSED - test_module.py:20: test_2[2-mod2] PASSED - - ========================= 8 passed in 0.01 seconds ========================= - test0 1 - test0 2 - create mod1 + + test_module.py:15: test_0[1] test0 1 + PASSED + test_module.py:15: test_0[2] test0 2 + PASSED + test_module.py:17: test_1[mod1] create mod1 test1 mod1 - test2 1 mod1 - test2 2 mod1 - fin mod1 - create mod2 + PASSED + test_module.py:19: test_2[1-mod1] test2 1 mod1 + PASSED + test_module.py:19: test_2[2-mod1] test2 2 mod1 + PASSED + test_module.py:17: test_1[mod2] create mod2 test1 mod2 - test2 1 mod2 - test2 2 mod2 - fin mod2 + PASSED + test_module.py:19: test_2[1-mod2] test2 1 mod2 + PASSED + test_module.py:19: test_2[2-mod2] test2 2 mod2 + PASSED + + ========================= 8 passed in 0.01 seconds ========================= You can see that the parametrized module-scoped ``modarg`` resource caused -an ordering of test execution that lead to the fewest possible "active" resources. The finalizer for the ``mod1`` parametrized resource was executed +an ordering of test execution that lead to the fewest possible "active" resources. The finalizer for the ``mod1`` parametrized resource was executed before the ``mod2`` resource was setup. @@ -599,7 +573,7 @@ achieve it. We separate the creation of the fixture into a conftest.py file:: # content of conftest.py - + import pytest import tempfile import os @@ -632,17 +606,18 @@ to verify our fixture is activated and the tests pass:: $ py.test -q .. + 2 passed in 0.01 seconds You can specify multiple fixtures like this:: @pytest.mark.usefixtures("cleandir", "anotherfixture") -and you may specify fixture usage at the test module level, using +and you may specify fixture usage at the test module level, using a generic feature of the mark mechanism:: pytestmark = pytest.mark.usefixtures("cleandir") -Lastly you can put fixtures required by all tests in your project +Lastly you can put fixtures required by all tests in your project into an ini-file:: # content of pytest.ini @@ -651,6 +626,7 @@ into an ini-file:: usefixtures = cleandir +.. _`autouse`: .. _`autouse fixtures`: autouse fixtures (xUnit setup on steroids) @@ -659,14 +635,14 @@ autouse fixtures (xUnit setup on steroids) .. regendoc:wipe Occasionally, you may want to have fixtures get invoked automatically -without a `usefixtures`_ or `funcargs`_ reference. As a practical +without a `usefixtures`_ or `funcargs`_ reference. As a practical example, suppose we have a database fixture which has a begin/rollback/commit architecture and we want to automatically surround each test method by a transaction and a rollback. Here is a dummy self-contained implementation of this idea:: # content of test_db_transact.py - + import pytest class DB: @@ -702,14 +678,15 @@ If we run it, we get two passing tests:: $ py.test -q .. + 2 passed in 0.01 seconds Here is how autouse fixtures work in other scopes: -- if an autouse fixture is defined in a test module, all its test - functions automatically use it. +- if an autouse fixture is defined in a test module, all its test + functions automatically use it. -- if an autouse fixture is defined in a conftest.py file then all tests in - all test modules belows its directory will invoke the fixture. +- if an autouse fixture is defined in a conftest.py file then all tests in + all test modules belows its directory will invoke the fixture. - lastly, and **please use that with care**: if you define an autouse fixture in a plugin, it will be invoked for all tests in all projects @@ -720,7 +697,7 @@ Here is how autouse fixtures work in other scopes: Note that the above ``transact`` fixture may very well be a fixture that you want to make available in your project without having it generally -active. The canonical way to do that is to put the transact definition +active. The canonical way to do that is to put the transact definition into a conftest.py file **without** using ``autouse``:: # content of conftest.py @@ -737,7 +714,7 @@ and then e.g. have a TestClass using it by declaring the need:: ... All test methods in this TestClass will use the transaction fixture while -other test classes or functions in the module will not use it unless +other test classes or functions in the module will not use it unless they also add a ``transact`` reference. Shifting (visibility of) fixture functions @@ -750,3 +727,4 @@ to a :ref:`conftest.py ` file or even separately installable fixtures functions starts at test classes, then test modules, then ``conftest.py`` files and finally builtin and third party plugins. + diff --git a/doc/en/getting-started.txt b/doc/en/getting-started.txt index 83ce562bd..8a4c0d961 100644 --- a/doc/en/getting-started.txt +++ b/doc/en/getting-started.txt @@ -1,7 +1,7 @@ Installation and Getting Started =================================== -**Pythons**: Python 2.4-3.3, Jython, PyPy +**Pythons**: Python 2.5-3.3, Jython, PyPy **Platforms**: Unix/Posix and Windows @@ -23,7 +23,7 @@ Installation options:: To check your installation has installed the correct version:: $ py.test --version - This is py.test version 2.3.5, imported from /home/hpk/p/pytest/.tox/regen/local/lib/python2.7/site-packages/pytest.py + This is pytest version 2.5.1, imported from /home/hpk/p/pytest/.tox/regen/local/lib/python2.7/site-packages/pytest.pyc If you get an error checkout :ref:`installation issues`. @@ -45,23 +45,23 @@ That's it. You can execute the test function now:: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 1 items - + test_sample.py F - + ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - + def test_answer(): > assert func(3) == 5 E assert 4 == 5 E + where 4 = func(3) - + test_sample.py:5: AssertionError ========================= 1 failed in 0.01 seconds ========================= -py.test found the ``test_answer`` function by following :ref:`standard test discovery rules `, basically detecting the ``test_`` prefixes. We got a failure report because our little ``func(3)`` call did not return ``5``. +``pytest`` found the ``test_answer`` function by following :ref:`standard test discovery rules `, basically detecting the ``test_`` prefixes. We got a failure report because our little ``func(3)`` call did not return ``5``. .. note:: @@ -93,6 +93,7 @@ Running it with, this time in "quiet" reporting mode:: $ py.test -q test_sysexit.py . + 1 passed in 0.00 seconds .. todo:: For further ways to assert exceptions see the `raises` @@ -121,15 +122,16 @@ run the module by passing its filename:: .F ================================= FAILURES ================================= ____________________________ TestClass.test_two ____________________________ - - self = - + + self = + def test_two(self): x = "hello" > assert hasattr(x, 'check') E assert hasattr('hello', 'check') - + test_class.py:8: AssertionError + 1 failed, 1 passed in 0.01 seconds The first test passed, the second failed. Again we can easily see the intermediate values used in the assertion, helping us to @@ -140,7 +142,7 @@ Going functional: requesting a unique temporary directory For functional tests one often needs to create some files and pass them to application objects. pytest provides -:ref:`builtinfixtures` which allow to request arbitrary +:ref:`builtinfixtures` which allow to request arbitrary resources, for example a unique temporary directory:: # content of test_tmpdir.py @@ -149,24 +151,25 @@ resources, for example a unique temporary directory:: assert 0 We list the name ``tmpdir`` in the test function signature and -py.test will lookup and call a fixture factory to create the resource +``pytest`` will lookup and call a fixture factory to create the resource before performing the test function call. Let's just run it:: $ py.test -q test_tmpdir.py F ================================= FAILURES ================================= _____________________________ test_needsfiles ______________________________ - - tmpdir = local('/tmp/pytest-322/test_needsfiles0') - + + tmpdir = local('/tmp/pytest-38/test_needsfiles0') + def test_needsfiles(tmpdir): print tmpdir > assert 0 E assert 0 - + test_tmpdir.py:3: AssertionError ----------------------------- Captured stdout ------------------------------ - /tmp/pytest-322/test_needsfiles0 + /tmp/pytest-38/test_needsfiles0 + 1 failed in 0.04 seconds Before the test runs, a unique-per-test-invocation temporary directory was created. More info at :ref:`tmpdir handling`. @@ -183,7 +186,7 @@ Here are a few suggestions where to go next: * :ref:`cmdline` for command line invocation examples * :ref:`good practises ` for virtualenv, test layout, genscript support * :ref:`fixtures` for providing a functional baseline to your tests -* :ref:`apiref` for documentation and examples on using py.test +* :ref:`apiref` for documentation and examples on using ``pytest`` * :ref:`plugins` managing and writing plugins .. _`installation issues`: @@ -218,7 +221,7 @@ py.test not found on Windows despite installation? - **Jython2.5.1 on Windows XP**: `Jython does not create command line launchers`_ so ``py.test`` will not work correctly. You may install py.test on CPython and type ``py.test --genscript=mytest`` and then use - ``jython mytest`` to run py.test for your tests to run with Jython. + ``jython mytest`` to run your tests with Jython using ``pytest``. :ref:`examples` for more complex examples diff --git a/doc/en/goodpractises.txt b/doc/en/goodpractises.txt index 8b2ccec56..962148705 100644 --- a/doc/en/goodpractises.txt +++ b/doc/en/goodpractises.txt @@ -8,52 +8,175 @@ Good Integration Practises Work with virtual environments ----------------------------------------------------------- -We recommend to use virtualenv_ environments and use easy_install_ -(or pip_) for installing your application dependencies as well as -the ``pytest`` package itself. This way you will get a much more reproducible -environment. A good tool to help you automate test runs against multiple -dependency configurations or Python interpreters is `tox`_. +We recommend to use virtualenv_ environments and use pip_ +(or easy_install_) for installing your application and any dependencies +as well as the ``pytest`` package itself. This way you will get an isolated +and reproducible environment. Given you have installed virtualenv_ +and execute it from the command line, here is an example session for unix +or windows:: + + virtualenv . # create a virtualenv directory in the current directory + + source bin/activate # on unix + + scripts/activate # on Windows + +We can now install pytest:: + + pip install pytest + +Due to the ``activate`` step above the ``pip`` will come from +the virtualenv directory and install any package into the isolated +virtual environment. + +Choosing a test layout / import rules +------------------------------------------ + +``pytest`` supports two common test layouts: + +* putting tests into an extra directory outside your actual application + code, useful if you have many functional tests or for other reasons + want to keep tests separate from actual application code (often a good + idea):: + + setup.py # your distutils/setuptools Python package metadata + mypkg/ + __init__.py + appmodule.py + tests/ + test_app.py + ... + + +* inlining test directories into your application package, useful if you + have direct relation between (unit-)test and application modules and + want to distribute your tests along with your application:: + + setup.py # your distutils/setuptools Python package metadata + mypkg/ + __init__.py + appmodule.py + ... + test/ + test_app.py + ... + +Important notes relating to both schemes: + +- **make sure that "mypkg" is importable**, for example by typing once:: + + pip install -e . # install package using setup.py in editable mode + +- **avoid "__init__.py" files in your test directories**. + This way your tests can run easily against an installed version + of ``mypkg``, independently from if the installed package contains + the tests or not. + +- With inlined tests you might put ``__init__.py`` into test + directories and make them installable as part of your application. + Using the ``py.test --pyargs mypkg`` invocation pytest will + discover where mypkg is installed and collect tests from there. + With the "external" test you can still distribute tests but they + will not be installed or become importable. + +Typically you can run tests by pointing to test directories or modules:: + + py.test tests/test_app.py # for external test dirs + py.test mypkg/test/test_app.py # for inlined test dirs + py.test mypkg # run tests in all below test directories + py.test # run all tests below current dir + ... + +Because of the above ``editable install`` mode you can change your +source code (both tests and the app) and rerun tests at will. +Once you are done with your work, you can `use tox`_ to make sure +that the package is really correct and tests pass in all +required configurations. + +.. note:: + + You can use Python3 namespace packages (PEP420) for your application + but pytest will still perform `test package name`_ discovery based on the + presence of ``__init__.py`` files. If you use one of the + two recommended file system layouts above but leave away the ``__init__.py`` + files from your directories it should just work on Python3.3 and above. From + "inlined tests", however, you will need to use absolute imports for + getting at your application code. + +.. _`test package name`: + +.. note:: + + If ``pytest`` finds a "a/b/test_module.py" test file while + recursing into the filesystem it determines the import name + as follows: + + * determine ``basedir``: this is the first "upward" (towards the root) + directory not containing an ``__init__.py``. If e.g. both ``a`` + and ``b`` contain an ``__init__.py`` file then the parent directory + of ``a`` will become the ``basedir``. + + * perform ``sys.path.insert(0, basedir)`` to make the test module + importable under the fully qualified import name. + + * ``import a.b.test_module`` where the path is determined + by converting path separators ``/`` into "." characters. This means + you must follow the convention of having directory and file + names map directly to the import names. + + The reason for this somewhat evolved importing technique is + that in larger projects multiple test modules might import + from each other and thus deriving a canonical import name helps + to avoid surprises such as a test modules getting imported twice. + .. _`virtualenv`: http://pypi.python.org/pypi/virtualenv .. _`buildout`: http://www.buildout.org/ .. _pip: http://pypi.python.org/pypi/pip +.. _`use tox`: + Use tox and Continuous Integration servers ------------------------------------------------- -If you frequently release code to the public you -may want to look into `tox`_, the virtualenv test automation -tool and its `pytest support `_. -The basic idea is to generate a JUnitXML file through the ``--junitxml=PATH`` option and have a continuous integration server like Jenkins_ pick it up -and generate reports. +If you frequently release code and want to make sure that your actual +package passes all tests you may want to look into `tox`_, the +virtualenv test automation tool and its `pytest support +`_. +Tox helps you to setup virtualenv environments with pre-defined +dependencies and then executing a pre-configured test command with +options. It will run tests against the installed package and not +against your source code checkout, helping to detect packaging +glitches. + +If you want to use Jenkins_ you can use the ``--junitxml=PATH`` option +to create a JUnitXML file that Jenkins_ can pick up and generate reports. .. _standalone: .. _`genscript method`: -Create a py.test standalone script +Create a pytest standalone script ------------------------------------------- -If you are a maintainer or application developer and want others -to easily run tests you can generate a completely standalone "py.test" -script:: +If you are a maintainer or application developer and want people +who don't deal with python much to easily run tests you may generate +a standalone ``pytest`` script:: py.test --genscript=runtests.py -generates a ``runtests.py`` script which is a fully functional basic -``py.test`` script, running unchanged under Python2 and Python3. +This generates a ``runtests.py`` script which is a fully functional basic +``pytest`` script, running unchanged under Python2 and Python3. You can tell people to download the script and then e.g. run it like this:: python runtests.py - - Integrating with distutils / ``python setup.py test`` -------------------------------------------------------- You can integrate test runs into your distutils or setuptools based project. Use the `genscript method`_ -to generate a standalone py.test script:: +to generate a standalone ``pytest`` script:: py.test --genscript=runtests.py @@ -84,7 +207,7 @@ If you now type:: python setup.py test this will execute your tests using ``runtests.py``. As this is a -standalone version of ``py.test`` no prior installation whatsoever is +standalone version of ``pytest`` no prior installation whatsoever is required for calling the test command. You can also pass additional arguments to the subprocess-calls such as your test directory or other options. @@ -93,8 +216,9 @@ options. Integration with setuptools test commands ---------------------------------------------------- -Setuptools supports writing our own Test command for invoking -pytest:: +Setuptools supports writing our own Test command for invoking pytest. +Most often it is better to use tox_ instead, but here is how you can +get started with setuptools integration:: from setuptools.command.test import test as TestCommand import sys @@ -120,7 +244,7 @@ Now if you run:: python setup.py test -this will download py.test if needed and then run py.test +this will download ``pytest`` if needed and then run your tests as you would expect it to. .. _`test discovery`: @@ -129,7 +253,7 @@ as you would expect it to. Conventions for Python test discovery ------------------------------------------------- -``py.test`` implements the following standard test discovery: +``pytest`` implements the following standard test discovery: * collection starts from the initial command line arguments which may be directories, filenames or test ids. @@ -140,72 +264,7 @@ Conventions for Python test discovery For examples of how to customize your test discovery :doc:`example/pythoncollection`. -Within Python modules, py.test also discovers tests using the standard +Within Python modules, ``pytest`` also discovers tests using the standard :ref:`unittest.TestCase ` subclassing technique. -Choosing a test layout / import rules ------------------------------------------- - -py.test supports common test layouts: - -* inlining test directories into your application package, useful if you want to - keep (unit) tests and actually tested code close together:: - - mypkg/ - __init__.py - appmodule.py - ... - test/ - test_app.py - ... - -* putting tests into an extra directory outside your actual application - code, useful if you have many functional tests or want to keep - tests separate from actual application code:: - - mypkg/ - __init__.py - appmodule.py - tests/ - test_app.py - ... - -In both cases you usually need to make sure that ``mypkg`` is importable, -for example by using the setuptools ``python setup.py develop`` method. - -You can run your tests by pointing to it:: - - py.test tests/test_app.py # for external test dirs - py.test mypkg/test/test_app.py # for inlined test dirs - py.test mypkg # run tests in all below test directories - py.test # run all tests below current dir - ... - -.. _`package name`: - -.. note:: - - If py.test finds a "a/b/test_module.py" test file while - recursing into the filesystem it determines the import name - as follows: - - * find ``basedir`` -- this is the first "upward" (towards the root) - directory not containing an ``__init__.py``. If both the ``a`` - and ``b`` directories contain an ``__init__.py`` the basedir will - be the parent dir of ``a``. - - * perform ``sys.path.insert(0, basedir)`` to make the test module - importable under the fully qualified import name. - - * ``import a.b.test_module`` where the path is determined - by converting path separators ``/`` into "." characters. This means - you must follow the convention of having directory and file - names map directly to the import names. - - The reason for this somewhat evolved importing technique is - that in larger projects multiple test modules might import - from each other and thus deriving a canonical import name helps - to avoid surprises such as a test modules getting imported twice. - - .. include:: links.inc diff --git a/doc/en/img/pullrequest.png b/doc/en/img/pullrequest.png new file mode 100644 index 000000000..e0724d764 Binary files /dev/null and b/doc/en/img/pullrequest.png differ diff --git a/doc/en/index.txt b/doc/en/index.txt index 39451ac47..1f1134f2b 100644 --- a/doc/en/index.txt +++ b/doc/en/index.txt @@ -1,52 +1,51 @@ .. _features: -.. note:: second training: `professional testing with Python `_ , 25-27th November 2013, Leipzig. +.. second training: `professional testing with Python `_ , 25-27th November 2013, Leipzig. pytest: helps you write better programs ============================================= **a mature full-featured Python testing tool** - - runs on Posix/Windows, Python 2.4-3.3, PyPy and Jython-2.5.1 + - runs on Posix/Windows, Python 2.5-3.3, PyPy and Jython-2.5.1 + - **zero-reported-bugs** policy with >1000 tests against itself + - **strict backward compatibility policy** for safe pytest upgrades - :ref:`comprehensive online ` and `PDF documentation `_ - - many :ref:`third party plugins ` and - :ref:`builtin helpers ` - - used in :ref:`many projects and organisations `, in test - suites with up to twenty thousand tests - - strict policy of remaining backward compatible across releases + - many :ref:`third party plugins ` and :ref:`builtin helpers `, + - used in :ref:`many small and large projects and organisations ` - comes with many :ref:`tested examples ` **provides easy no-boilerplate testing** - makes it :ref:`easy to get started `, - many :ref:`usage options ` + has many :ref:`usage options ` - :ref:`assert with the assert statement` - helpful :ref:`traceback and failing assertion reporting ` - - allows :ref:`print debugging ` and :ref:`the + - :ref:`print debugging ` and :ref:`the capturing of standard output during test execution ` **scales from simple unit to complex functional testing** - :ref:`modular parametrizeable fixtures ` (new in 2.3, - improved in 2.4) + continously improved) - :ref:`parametrized test functions ` - :ref:`mark` - :ref:`skipping` (improved in 2.4) - - can :ref:`distribute tests to multiple CPUs ` through :ref:`xdist plugin ` - - can :ref:`continuously re-run failing tests ` + - :ref:`distribute tests to multiple CPUs ` through :ref:`xdist plugin ` + - :ref:`continuously re-run failing tests ` - flexible :ref:`Python test discovery` -**integrates many common testing methods**: +**integrates with other testing methods and tools**: - - multi-paradigm: pytest can run many ``nose``, ``unittest.py`` and - ``doctest.py`` style test suites, including running testcases made for + - multi-paradigm: pytest can run ``nose``, ``unittest`` and + ``doctest`` style test suites, including running testcases made for Django and trial - supports :ref:`good integration practises ` - supports extended :ref:`xUnit style setup ` - supports domain-specific :ref:`non-python tests` - - supports the generation of testing coverage reports - - `Javascript unit- and functional testing`_ + - supports generating `test coverage reports + `_ - supports :pep:`8` compliant coding styles in tests **extensive plugin and customization system**: @@ -56,8 +55,6 @@ pytest: helps you write better programs - it is easy to add command line options or customize existing behaviour -.. _`Javascript unit- and functional testing`: http://pypi.python.org/pypi/oejskit - .. _`easy`: http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html diff --git a/doc/en/monkeypatch.txt b/doc/en/monkeypatch.txt index 40af9cca8..4d56e860f 100644 --- a/doc/en/monkeypatch.txt +++ b/doc/en/monkeypatch.txt @@ -48,11 +48,28 @@ requests in all your tests, you can do:: import pytest @pytest.fixture(autouse=True) def no_requests(monkeypatch): - monkeypatch.replace("requests.session.Session.request", None) + monkeypatch.delattr("requests.session.Session.request") + +This autouse fixture will be executed for each test function and it +will delete the method ``request.session.Session.request`` +so that any attempts within tests to create http requests will fail. + +example: setting an attribute on some class +------------------------------------------------------ + +If you need to patch out ``os.getcwd()`` to return an artifical +value:: + + def test_some_interaction(monkeypatch): + monkeypatch.setattr("os.getcwd", lambda: "/") + +which is equivalent to the long form:: + + def test_some_interaction(monkeypatch): + import os + monkeypatch.setattr(os, "getcwd", lambda: "/") + -This autouse fixture will be executed for all test functions and it -will replace the method ``request.session.Session.request`` with the -value None so that any attempts to create http requests will fail. Method reference of the monkeypatch function argument ----------------------------------------------------- diff --git a/doc/en/nose.txt b/doc/en/nose.txt index 0c1931a4d..61d101723 100644 --- a/doc/en/nose.txt +++ b/doc/en/nose.txt @@ -3,7 +3,7 @@ Running tests written for nose .. include:: links.inc -py.test has basic support for running tests written for nose_. +``pytest`` has basic support for running tests written for nose_. .. _nosestyle: @@ -16,7 +16,7 @@ After :ref:`installation` type:: py.test # instead of 'nosetests' and you should be able to run your nose style tests and -make use of py.test's capabilities. +make use of pytest's capabilities. Supported nose Idioms ---------------------- @@ -30,9 +30,25 @@ Supported nose Idioms Unsupported idioms / known issues ---------------------------------- +- unittest-style ``setUp, tearDown, setUpClass, tearDownClass`` + are recognized only on ``unittest.TestCase`` classes but not + on plain classes. ``nose`` supports these methods also on plain + classes but pytest deliberately does not. As nose and pytest already + both support ``setup_class, teardown_class, setup_method, teardown_method`` + it doesn't seem useful to duplicate the unittest-API like nose does. + If you however rather think pytest should support the unittest-spelling on + plain classes please post `to this issue + `_. + +- nose imports test modules with the same import path (e.g. + ``tests.test_mod``) but different file system paths + (e.g. ``tests/test_mode.py`` and ``other/tests/test_mode.py``) + by extending sys.path/import semantics. pytest does not do that + but there is discussion in `issue268 `_ for adding some support. Note that + `nose2 choose to avoid this sys.path/import hackery `_. + - nose-style doctests are not collected and executed correctly, also doctest fixtures don't work. - no nose-configuration is recognized - diff --git a/doc/en/parametrize.txt b/doc/en/parametrize.txt index 5c326d0b2..b4ea40985 100644 --- a/doc/en/parametrize.txt +++ b/doc/en/parametrize.txt @@ -15,10 +15,10 @@ pytest supports test parametrization in several well-integrated ways: at the level of fixture functions `. * `@pytest.mark.parametrize`_ allows to define parametrization at the - function or class level, provides multiple argument/fixture sets + function or class level, provides multiple argument/fixture sets for a particular test function or class. -* `pytest_generate_tests`_ enables implementing your own custom +* `pytest_generate_tests`_ enables implementing your own custom dynamic parametrization scheme or extensions. .. _parametrizemark: @@ -47,23 +47,22 @@ to an expected output:: def test_eval(input, expected): assert eval(input) == expected -Here, the ``@parametrize`` decorator defines three different ``(input,output)`` -tuples so that that the ``test_eval`` function will run three times using +Here, the ``@parametrize`` decorator defines three different ``(input,expected)`` +tuples so that the ``test_eval`` function will run three times using them in turn:: - $ py.test - ============================= test session starts ============================== - platform linux2 -- Python 2.7.3 -- pytest-2.4.0.dev3 - plugins: xdist, cache, cli, pep8, xprocess, cov, capturelog, bdd-splinter, rerunfailures, instafail, localserver + $ py.test + =========================== test session starts ============================ + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 3 items - + test_expectation.py ..F - - =================================== FAILURES =================================== - ______________________________ test_eval[6*9-42] _______________________________ - + + ================================= FAILURES ================================= + ____________________________ test_eval[6*9-42] _____________________________ + input = '6*9', expected = 42 - + @pytest.mark.parametrize("input,expected", [ ("3+5", 8), ("2+4", 6), @@ -73,9 +72,9 @@ them in turn:: > assert eval(input) == expected E assert 54 == 42 E + where 54 = eval('6*9') - + test_expectation.py:8: AssertionError - ====================== 1 failed, 2 passed in 0.02 seconds ====================== + ==================== 1 failed, 2 passed in 0.01 seconds ==================== As designed in this example, only one pair of input/output values fails the simple test function. And as usual with test function arguments, @@ -100,14 +99,13 @@ for example with the builtin ``mark.xfail``:: Let's run this:: $ py.test - ============================= test session starts ============================== - platform linux2 -- Python 2.7.3 -- pytest-2.4.0.dev3 - plugins: xdist, cache, cli, pep8, xprocess, cov, capturelog, bdd-splinter, rerunfailures, instafail, localserver + =========================== test session starts ============================ + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 3 items - + test_expectation.py ..x - - ===================== 2 passed, 1 xfailed in 0.02 seconds ====================== + + =================== 2 passed, 1 xfailed in 0.01 seconds ==================== The one parameter set which caused a failure previously now shows up as an "xfailed (expected to fail)" test. @@ -116,8 +114,8 @@ shows up as an "xfailed (expected to fail)" test. In versions prior to 2.4 one needed to specify the argument names as a tuple. This remains valid but the simpler ``"name1,name2,..."`` - comma-separated-string syntax is now advertised fist because - it's easier to write, produces less line noise. + comma-separated-string syntax is now advertised first because + it's easier to write and produces less line noise. .. _`pytest_generate_tests`: @@ -126,14 +124,14 @@ Basic ``pytest_generate_tests`` example Sometimes you may want to implement your own parametrization scheme or implement some dynamism for determining the parameters or scope -of a fixture. For this, you can use the ``pytest_generate_tests`` hook +of a fixture. For this, you can use the ``pytest_generate_tests`` hook which is called when collecting a test function. Through the passed in `metafunc` object you can inspect the requesting test context and, most importantly, you can call ``metafunc.parametrize()`` to cause -parametrization. +parametrization. For example, let's say we want to run a test taking string inputs which -we want to set via a new py.test command line option. Let's first write +we want to set via a new ``pytest`` command line option. Let's first write a simple test accepting a ``stringinput`` fixture function argument:: # content of test_strings.py @@ -141,7 +139,7 @@ a simple test accepting a ``stringinput`` fixture function argument:: def test_valid_string(stringinput): assert stringinput.isalpha() -Now we add a ``conftest.py`` file containing the addition of a +Now we add a ``conftest.py`` file containing the addition of a command line option and the parametrization of our test function:: # content of conftest.py @@ -152,40 +150,43 @@ command line option and the parametrization of our test function:: def pytest_generate_tests(metafunc): if 'stringinput' in metafunc.fixturenames: - metafunc.parametrize("stringinput", + metafunc.parametrize("stringinput", metafunc.config.option.stringinput) If we now pass two stringinput values, our test will run twice:: $ py.test -q --stringinput="hello" --stringinput="world" test_strings.py .. + 2 passed in 0.01 seconds Let's also run with a stringinput that will lead to a failing test:: $ py.test -q --stringinput="!" test_strings.py F - =================================== FAILURES =================================== - _____________________________ test_valid_string[!] _____________________________ - + ================================= FAILURES ================================= + ___________________________ test_valid_string[!] ___________________________ + stringinput = '!' - + def test_valid_string(stringinput): > assert stringinput.isalpha() - E assert () - E + where = '!'.isalpha - - test_strings.py:3: AssertionError + E assert () + E + where = '!'.isalpha -As expected our test function fails. + test_strings.py:3: AssertionError + 1 failed in 0.01 seconds + +As expected our test function fails. If you don't specify a stringinput it will be skipped because ``metafunc.parametrize()`` will be called with an empty parameter listlist:: - $ py.test -q -rs test_strings.py + $ py.test -q -rs test_strings.py s - =========================== short test summary info ============================ - SKIP [1] /home/hpk/p/pytest/_pytest/python.py:999: got empty parameter set, function test_valid_string at /tmp/doc-exec-2/test_strings.py:1 + ========================= short test summary info ========================== + SKIP [1] /home/hpk/p/pytest/.tox/regen/local/lib/python2.7/site-packages/_pytest/python.py:1094: got empty parameter set, function test_valid_string at /tmp/doc-exec-24/test_strings.py:1 + 1 skipped in 0.01 seconds For further examples, you might want to look at :ref:`more parametrization examples `. diff --git a/doc/en/plugins.txt b/doc/en/plugins.txt index d6c169ef5..0f612cceb 100644 --- a/doc/en/plugins.txt +++ b/doc/en/plugins.txt @@ -3,9 +3,9 @@ Working with plugins and conftest files ============================================= -py.test implements all aspects of configuration, collection, running and reporting by calling `well specified hooks`_. Virtually any Python module can be registered as a plugin. It can implement any number of hook functions (usually two or three) which all have a ``pytest_`` prefix, making hook functions easy to distinguish and find. There are three basic location types: +``pytest`` implements all aspects of configuration, collection, running and reporting by calling `well specified hooks`_. Virtually any Python module can be registered as a plugin. It can implement any number of hook functions (usually two or three) which all have a ``pytest_`` prefix, making hook functions easy to distinguish and find. There are three basic location types: -* `builtin plugins`_: loaded from py.test's internal ``_pytest`` directory. +* `builtin plugins`_: loaded from pytest's internal ``_pytest`` directory. * `external plugins`_: modules discovered through `setuptools entry points`_ * `conftest.py plugins`_: modules auto-discovered in test directories @@ -63,7 +63,7 @@ tool, for example:: pip install pytest-NAME pip uninstall pytest-NAME -If a plugin is installed, py.test automatically finds and integrates it, +If a plugin is installed, ``pytest`` automatically finds and integrates it, there is no need to activate it. Here is a initial list of known plugins: .. _`django`: https://www.djangoproject.com/ @@ -84,14 +84,14 @@ there is no need to activate it. Here is a initial list of known plugins: * `pytest-xdist `_: to distribute tests to CPUs and remote hosts, to run in boxed mode which allows to survive segmentation faults, to run in - looponfailing mode, automatically re-running failing tests + looponfailing mode, automatically re-running failing tests on file changes, see also :ref:`xdist` * `pytest-instafail `_: to report failures while the test run is happening. * `pytest-bdd `_ and - `pytest-konira `_ + `pytest-konira `_ to write tests using behaviour-driven testing. * `pytest-timeout `_: @@ -122,7 +122,7 @@ If you want to write a plugin, there are many real-life examples you can copy from: * a custom collection example plugin: :ref:`yaml plugin` -* around 20 `builtin plugins`_ which provide py.test's own functionality +* around 20 `builtin plugins`_ which provide pytest's own functionality * many `external plugins`_ providing additional features All of these plugins implement the documented `well specified hooks`_ @@ -135,10 +135,10 @@ Making your plugin installable by others If you want to make your plugin externally available, you may define a so-called entry point for your distribution so -that ``py.test`` finds your plugin module. Entry points are +that ``pytest`` finds your plugin module. Entry points are a feature that is provided by `setuptools`_ or `Distribute`_. -py.test looks up the ``pytest11`` entrypoint to discover its -plugins and you can thus make your plugin available by definig +pytest looks up the ``pytest11`` entrypoint to discover its +plugins and you can thus make your plugin available by defining it in your setuptools/distribute-based setup-invocation: .. sourcecode:: python @@ -150,7 +150,7 @@ it in your setuptools/distribute-based setup-invocation: name="myproject", packages = ['myproject'] - # the following makes a plugin available to py.test + # the following makes a plugin available to pytest entry_points = { 'pytest11': [ 'name_of_plugin = myproject.pluginmodule', @@ -158,7 +158,7 @@ it in your setuptools/distribute-based setup-invocation: }, ) -If a package is installed this way, py.test will load +If a package is installed this way, ``pytest`` will load ``myproject.pluginmodule`` as a plugin which can define `well specified hooks`_. @@ -167,7 +167,7 @@ If a package is installed this way, py.test will load Plugin discovery order at tool startup -------------------------------------------- -py.test loads plugin modules at tool startup in the following way: +``pytest`` loads plugin modules at tool startup in the following way: * by loading all builtin plugins @@ -177,9 +177,15 @@ py.test loads plugin modules at tool startup in the following way: and loading the specified plugin before actual command line parsing. * by loading all :file:`conftest.py` files as inferred by the command line - invocation (test files and all of its *parent* directories). - Note that ``conftest.py`` files from *sub* directories are by default - not loaded at tool startup. + invocation: + + - if no test paths are specified use current dir as a test path + - if exists, load ``conftest.py`` and ``test*/conftest.py`` relative + to the directory part of the first test path. + + Note that pytest does not find ``conftest.py`` files in deeper nested + sub directories at tool startup. It is usually a good idea to keep + your conftest.py file in the top level test or project root directory. * by recursively loading all plugins specified by the ``pytest_plugins`` variable in ``conftest.py`` files @@ -197,7 +203,7 @@ will be loaded as well. You can also use dotted path like this:: pytest_plugins = "myapp.testsupport.myplugin" -which will import the specified module as a py.test plugin. +which will import the specified module as a ``pytest`` plugin. Accessing another plugin by name @@ -243,7 +249,7 @@ how to obtain the name of a plugin. .. _`builtin plugins`: -py.test default plugin reference +pytest default plugin reference ==================================== @@ -277,14 +283,14 @@ in the `pytest repository `_. .. _`well specified hooks`: -py.test hook reference +pytest hook reference ==================================== Hook specification and validation ----------------------------------------- -py.test calls hook functions to implement initialization, running, -test execution and reporting. When py.test loads a plugin it validates +``pytest`` calls hook functions to implement initialization, running, +test execution and reporting. When ``pytest`` loads a plugin it validates that each hook function conforms to its respective hook specification. Each hook function name and its argument names need to match a hook specification. However, a hook function may accept *fewer* parameters @@ -327,7 +333,7 @@ the reporting hook to print information about a test run. Collection hooks ------------------------------ -py.test calls the following hooks for collecting files and directories: +``pytest`` calls the following hooks for collecting files and directories: .. autofunction:: pytest_ignore_collect .. autofunction:: pytest_collect_directory diff --git a/doc/en/plugins_index/plugins_index.py b/doc/en/plugins_index/plugins_index.py new file mode 100644 index 000000000..74a6be32a --- /dev/null +++ b/doc/en/plugins_index/plugins_index.py @@ -0,0 +1,237 @@ +""" +Script to generate the file `plugins_index.txt` with information about +pytest plugins taken directly from a live PyPI server. + +Also includes plugin compatibility between different python and pytest versions, +obtained from http://pytest-plugs.herokuapp.com. +""" +from __future__ import print_function +from collections import namedtuple +import datetime +from distutils.version import LooseVersion +import itertools +from optparse import OptionParser +import os +import sys +import pytest + + +def get_proxy(url): + """ + wrapper function to obtain a xmlrpc proxy, taking in account import + differences between python 2.X and 3.X + + :param url: url to bind the proxy to + :return: a ServerProxy instance + """ + if sys.version_info < (3, 0): + from xmlrpclib import ServerProxy + else: + from xmlrpc.client import ServerProxy + return ServerProxy(url) + + +def iter_plugins(client, search='pytest-'): + """ + Returns an iterator of (name, version) from PyPI. + + :param client: ServerProxy + :param search: package names to search for + """ + for plug_data in client.search({'name': search}): + yield plug_data['name'], plug_data['version'] + + +def get_latest_versions(plugins): + """ + Returns an iterator of (name, version) from the given list of (name, + version), but returning only the latest version of the package. Uses + distutils.LooseVersion to ensure compatibility with PEP386. + """ + plugins = [(name, LooseVersion(version)) for (name, version) in plugins] + for name, grouped_plugins in itertools.groupby(plugins, key=lambda x: x[0]): + name, loose_version = list(grouped_plugins)[-1] + yield name, str(loose_version) + + +def obtain_plugins_table(plugins, client): + """ + Returns information to populate a table of plugins, their versions, + authors, etc. + + The returned information is a list of columns of `ColumnData` + namedtuples(text, link). Link can be None if the text for that column + should not be linked to anything. + + :param plugins: list of (name, version) + :param client: ServerProxy + """ + rows = [] + ColumnData = namedtuple('ColumnData', 'text link') + headers = ['Name', 'Py27', 'Py33', 'Repository', 'Summary'] + pytest_version = pytest.__version__ + repositories = obtain_override_repositories() + print('*** pytest-{0} ***'.format(pytest_version)) + plugins = list(plugins) + for index, (package_name, version) in enumerate(plugins): + print(package_name, version, '...', end='') + + release_data = client.release_data(package_name, version) + + common_params = dict( + site='http://pytest-plugs.herokuapp.com', + name=package_name, + version=version) + + # first row: name, images and simple links + url = '.. image:: {site}/status/{name}-{version}' + image_url = url.format(**common_params) + image_url += '?py={py}&pytest={pytest}' + row = ( + ColumnData(package_name + '-' + version, + release_data['release_url']), + ColumnData(image_url.format(py='py27', pytest=pytest_version), + None), + ColumnData(image_url.format(py='py33', pytest=pytest_version), + None), + ColumnData( + repositories.get(package_name, release_data['home_page']), + None), + ColumnData(release_data['summary'], None), + ) + assert len(row) == len(headers) + rows.append(row) + + # second row: links for images (they should be in their own line) + url = ' :target: {site}/output/{name}-{version}' + output_url = url.format(**common_params) + output_url += '?py={py}&pytest={pytest}' + + row = ( + ColumnData('', None), + ColumnData(output_url.format(py='py27', pytest=pytest_version), + None), + ColumnData(output_url.format(py='py33', pytest=pytest_version), + None), + ColumnData('', None), + ColumnData('', None), + ) + assert len(row) == len(headers) + rows.append(row) + + print('OK (%d%%)' % ((index + 1) * 100 / len(plugins))) + + return headers, rows + + +def obtain_override_repositories(): + """ + Used to override the "home_page" obtained from pypi to known + package repositories. Used when the author didn't fill the "home_page" + field in setup.py. + + :return: dict of {package_name: repository_url} + """ + return { + 'pytest-blockage': 'https://github.com/rob-b/pytest-blockage', + 'pytest-konira': 'http://github.com/alfredodeza/pytest-konira', + } + + +def generate_plugins_index_from_table(filename, headers, rows): + """ + Generates a RST file with the table data given. + + :param filename: output filename + :param headers: see `obtain_plugins_table` + :param rows: see `obtain_plugins_table` + """ + # creates a list of rows, each being a str containing appropriate column + # text and link + table_texts = [] + for row in rows: + column_texts = [] + for i, col_data in enumerate(row): + text = '`%s <%s>`_' % ( + col_data.text, + col_data.link) if col_data.link else col_data.text + column_texts.append(text) + table_texts.append(column_texts) + + # compute max length of each column so we can build the rst table + column_lengths = [len(x) for x in headers] + for column_texts in table_texts: + for i, row_text in enumerate(column_texts): + column_lengths[i] = max(column_lengths[i], len(row_text) + 2) + + def get_row_limiter(char): + return ' '.join(char * length for length in column_lengths) + + with open(filename, 'w') as f: + # write welcome + print('.. _plugins_index:', file=f) + print(file=f) + print('List of Third-Party Plugins', file=f) + print('===========================', file=f) + print(file=f) + + # table + print(get_row_limiter('='), file=f) + formatted_headers = [ + '{0:^{fill}}'.format(header, fill=column_lengths[i]) + for i, header in enumerate(headers)] + print(*formatted_headers, file=f) + print(get_row_limiter('='), file=f) + + for column_texts in table_texts: + formatted_rows = [ + '{0:^{fill}}'.format(row_text, fill=column_lengths[i]) + for i, row_text in enumerate(column_texts) + ] + print(*formatted_rows, file=f) + print(file=f) + print(get_row_limiter('='), file=f) + print(file=f) + today = datetime.date.today().strftime('%Y-%m-%d') + print('*(Updated on %s)*' % today, file=f) + + +def generate_plugins_index(client, filename): + """ + Generates an RST file with a table of the latest pytest plugins found in + PyPI. + + :param client: ServerProxy + :param filename: output filename + """ + plugins = get_latest_versions(iter_plugins(client)) + headers, rows = obtain_plugins_table(plugins, client) + generate_plugins_index_from_table(filename, headers, rows) + + +def main(argv): + """ + Script entry point. Configures an option parser and calls the appropriate + internal function. + """ + filename = os.path.join(os.path.dirname(__file__), 'plugins_index.txt') + url = 'http://pypi.python.org/pypi' + + parser = OptionParser( + description='Generates a restructured document of pytest plugins from PyPI') + parser.add_option('-f', '--filename', default=filename, + help='output filename [default: %default]') + parser.add_option('-u', '--url', default=url, + help='url of PyPI server to obtain data from [default: %default]') + (options, _) = parser.parse_args(argv[1:]) + + client = get_proxy(options.url) + generate_plugins_index(client, options.filename) + + print() + print('%s Updated.' % options.filename) + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/doc/en/plugins_index/plugins_index.txt b/doc/en/plugins_index/plugins_index.txt new file mode 100644 index 000000000..a968f9c43 --- /dev/null +++ b/doc/en/plugins_index/plugins_index.txt @@ -0,0 +1,112 @@ +.. _plugins_index: + +List of Third-Party Plugins +=========================== + +========================================================================================== ======================================================================================================== ======================================================================================================== ============================================================= ============================================================================================================================================= + Name Py27 Py33 Repository Summary +========================================================================================== ======================================================================================================== ======================================================================================================== ============================================================= ============================================================================================================================================= + `pytest-bdd-0.6.8 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-bdd-0.6.8?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-bdd-0.6.8?py=py33&pytest=2.5.1 https://github.com/olegpidsadnyi/pytest-bdd BDD for pytest + :target: http://pytest-plugs.herokuapp.com/output/pytest-bdd-0.6.8?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-bdd-0.6.8?py=py33&pytest=2.5.1 + `pytest-bdd-splinter-0.5.98 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-bdd-splinter-0.5.98?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-bdd-splinter-0.5.98?py=py33&pytest=2.5.1 https://github.com/olegpidsadnyi/pytest-bdd-splinter Splinter subplugin for Pytest BDD plugin + :target: http://pytest-plugs.herokuapp.com/output/pytest-bdd-splinter-0.5.98?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-bdd-splinter-0.5.98?py=py33&pytest=2.5.1 + `pytest-bench-0.2.5 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-bench-0.2.5?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-bench-0.2.5?py=py33&pytest=2.5.1 http://github.com/concordusapps/pytest-bench Benchmark utility that plugs into pytest. + :target: http://pytest-plugs.herokuapp.com/output/pytest-bench-0.2.5?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-bench-0.2.5?py=py33&pytest=2.5.1 + `pytest-blockage-0.1 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-blockage-0.1?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-blockage-0.1?py=py33&pytest=2.5.1 https://github.com/rob-b/pytest-blockage Disable network requests during a test run. + :target: http://pytest-plugs.herokuapp.com/output/pytest-blockage-0.1?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-blockage-0.1?py=py33&pytest=2.5.1 + `pytest-browsermob-proxy-0.1 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-browsermob-proxy-0.1?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-browsermob-proxy-0.1?py=py33&pytest=2.5.1 https://github.com/davehunt/pytest-browsermob-proxy BrowserMob proxy plugin for py.test. + :target: http://pytest-plugs.herokuapp.com/output/pytest-browsermob-proxy-0.1?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-browsermob-proxy-0.1?py=py33&pytest=2.5.1 + `pytest-bugzilla-0.2 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-bugzilla-0.2?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-bugzilla-0.2?py=py33&pytest=2.5.1 http://github.com/nibrahim/pytest_bugzilla py.test bugzilla integration plugin + :target: http://pytest-plugs.herokuapp.com/output/pytest-bugzilla-0.2?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-bugzilla-0.2?py=py33&pytest=2.5.1 + `pytest-cache-1.0 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-cache-1.0?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-cache-1.0?py=py33&pytest=2.5.1 http://bitbucket.org/hpk42/pytest-cache/ pytest plugin with mechanisms for caching across test runs + :target: http://pytest-plugs.herokuapp.com/output/pytest-cache-1.0?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-cache-1.0?py=py33&pytest=2.5.1 + `pytest-capturelog-0.7 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-capturelog-0.7?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-capturelog-0.7?py=py33&pytest=2.5.1 http://bitbucket.org/memedough/pytest-capturelog/overview py.test plugin to capture log messages + :target: http://pytest-plugs.herokuapp.com/output/pytest-capturelog-0.7?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-capturelog-0.7?py=py33&pytest=2.5.1 + `pytest-codecheckers-0.2 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-codecheckers-0.2?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-codecheckers-0.2?py=py33&pytest=2.5.1 http://bitbucket.org/RonnyPfannschmidt/pytest-codecheckers/ pytest plugin to add source code sanity checks (pep8 and friends) + :target: http://pytest-plugs.herokuapp.com/output/pytest-codecheckers-0.2?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-codecheckers-0.2?py=py33&pytest=2.5.1 + `pytest-contextfixture-0.1.1 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-contextfixture-0.1.1?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-contextfixture-0.1.1?py=py33&pytest=2.5.1 http://github.com/pelme/pytest-contextfixture/ Define pytest fixtures as context managers. + :target: http://pytest-plugs.herokuapp.com/output/pytest-contextfixture-0.1.1?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-contextfixture-0.1.1?py=py33&pytest=2.5.1 + `pytest-couchdbkit-0.5.1 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-couchdbkit-0.5.1?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-couchdbkit-0.5.1?py=py33&pytest=2.5.1 http://bitbucket.org/RonnyPfannschmidt/pytest-couchdbkit py.test extension for per-test couchdb databases using couchdbkit + :target: http://pytest-plugs.herokuapp.com/output/pytest-couchdbkit-0.5.1?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-couchdbkit-0.5.1?py=py33&pytest=2.5.1 + `pytest-cov-1.6 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-cov-1.6?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-cov-1.6?py=py33&pytest=2.5.1 http://bitbucket.org/memedough/pytest-cov/overview py.test plugin for coverage reporting with support for both centralised and distributed testing, including subprocesses and multiprocessing + :target: http://pytest-plugs.herokuapp.com/output/pytest-cov-1.6?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-cov-1.6?py=py33&pytest=2.5.1 + `pytest-dbfixtures-0.4.3 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-dbfixtures-0.4.3?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-dbfixtures-0.4.3?py=py33&pytest=2.5.1 https://github.com/clearcode/pytest-dbfixtures dbfixtures plugin for py.test. + :target: http://pytest-plugs.herokuapp.com/output/pytest-dbfixtures-0.4.3?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-dbfixtures-0.4.3?py=py33&pytest=2.5.1 + `pytest-django-2.5 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-django-2.5?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-django-2.5?py=py33&pytest=2.5.1 http://pytest-django.readthedocs.org/ A Django plugin for py.test. + :target: http://pytest-plugs.herokuapp.com/output/pytest-django-2.5?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-django-2.5?py=py33&pytest=2.5.1 + `pytest-django-lite-0.1.0 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-django-lite-0.1.0?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-django-lite-0.1.0?py=py33&pytest=2.5.1 UNKNOWN The bare minimum to integrate py.test with Django. + :target: http://pytest-plugs.herokuapp.com/output/pytest-django-lite-0.1.0?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-django-lite-0.1.0?py=py33&pytest=2.5.1 + `pytest-figleaf-1.0 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-figleaf-1.0?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-figleaf-1.0?py=py33&pytest=2.5.1 http://bitbucket.org/hpk42/pytest-figleaf py.test figleaf coverage plugin + :target: http://pytest-plugs.herokuapp.com/output/pytest-figleaf-1.0?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-figleaf-1.0?py=py33&pytest=2.5.1 + `pytest-flakes-0.2 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-flakes-0.2?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-flakes-0.2?py=py33&pytest=2.5.1 https://github.com/fschulze/pytest-flakes pytest plugin to check source code with pyflakes + :target: http://pytest-plugs.herokuapp.com/output/pytest-flakes-0.2?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-flakes-0.2?py=py33&pytest=2.5.1 + `pytest-greendots-0.2 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-greendots-0.2?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-greendots-0.2?py=py33&pytest=2.5.1 UNKNOWN Green progress dots + :target: http://pytest-plugs.herokuapp.com/output/pytest-greendots-0.2?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-greendots-0.2?py=py33&pytest=2.5.1 + `pytest-growl-0.2 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-growl-0.2?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-growl-0.2?py=py33&pytest=2.5.1 UNKNOWN Growl notifications for pytest results. + :target: http://pytest-plugs.herokuapp.com/output/pytest-growl-0.2?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-growl-0.2?py=py33&pytest=2.5.1 + `pytest-incremental-0.3.0 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-incremental-0.3.0?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-incremental-0.3.0?py=py33&pytest=2.5.1 https://bitbucket.org/schettino72/pytest-incremental an incremental test runner (pytest plugin) + :target: http://pytest-plugs.herokuapp.com/output/pytest-incremental-0.3.0?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-incremental-0.3.0?py=py33&pytest=2.5.1 + `pytest-instafail-0.1.1 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-instafail-0.1.1?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-instafail-0.1.1?py=py33&pytest=2.5.1 https://github.com/jpvanhal/pytest-instafail py.test plugin to show failures instantly + :target: http://pytest-plugs.herokuapp.com/output/pytest-instafail-0.1.1?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-instafail-0.1.1?py=py33&pytest=2.5.1 + `pytest-ipdb-0.1-prerelease `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-ipdb-0.1-prerelease?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-ipdb-0.1-prerelease?py=py33&pytest=2.5.1 https://github.com/mverteuil/pytest-ipdb A py.test plug-in to enable drop to ipdb debugger on test failure. + :target: http://pytest-plugs.herokuapp.com/output/pytest-ipdb-0.1-prerelease?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-ipdb-0.1-prerelease?py=py33&pytest=2.5.1 + `pytest-jira-0.01 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-jira-0.01?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-jira-0.01?py=py33&pytest=2.5.1 http://github.com/jlaska/pytest_jira py.test JIRA integration plugin, using markers + :target: http://pytest-plugs.herokuapp.com/output/pytest-jira-0.01?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-jira-0.01?py=py33&pytest=2.5.1 + `pytest-konira-0.2 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-konira-0.2?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-konira-0.2?py=py33&pytest=2.5.1 http://github.com/alfredodeza/pytest-konira Run Konira DSL tests with py.test + :target: http://pytest-plugs.herokuapp.com/output/pytest-konira-0.2?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-konira-0.2?py=py33&pytest=2.5.1 + `pytest-localserver-0.3.2 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-localserver-0.3.2?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-localserver-0.3.2?py=py33&pytest=2.5.1 http://bitbucket.org/basti/pytest-localserver/ py.test plugin to test server connections locally. + :target: http://pytest-plugs.herokuapp.com/output/pytest-localserver-0.3.2?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-localserver-0.3.2?py=py33&pytest=2.5.1 + `pytest-marker-bugzilla-0.06 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-marker-bugzilla-0.06?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-marker-bugzilla-0.06?py=py33&pytest=2.5.1 http://github.com/eanxgeek/pytest_marker_bugzilla py.test bugzilla integration plugin, using markers + :target: http://pytest-plugs.herokuapp.com/output/pytest-marker-bugzilla-0.06?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-marker-bugzilla-0.06?py=py33&pytest=2.5.1 + `pytest-markfiltration-0.8 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-markfiltration-0.8?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-markfiltration-0.8?py=py33&pytest=2.5.1 https://github.com/adamgoucher/pytest-markfiltration UNKNOWN + :target: http://pytest-plugs.herokuapp.com/output/pytest-markfiltration-0.8?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-markfiltration-0.8?py=py33&pytest=2.5.1 + `pytest-marks-0.4 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-marks-0.4?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-marks-0.4?py=py33&pytest=2.5.1 https://github.com/adamgoucher/pytest-marks UNKNOWN + :target: http://pytest-plugs.herokuapp.com/output/pytest-marks-0.4?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-marks-0.4?py=py33&pytest=2.5.1 + `pytest-monkeyplus-1.1.0 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-monkeyplus-1.1.0?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-monkeyplus-1.1.0?py=py33&pytest=2.5.1 http://bitbucket.org/hsoft/pytest-monkeyplus/ pytest's monkeypatch subclass with extra functionalities + :target: http://pytest-plugs.herokuapp.com/output/pytest-monkeyplus-1.1.0?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-monkeyplus-1.1.0?py=py33&pytest=2.5.1 + `pytest-mozwebqa-1.1.1 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-mozwebqa-1.1.1?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-mozwebqa-1.1.1?py=py33&pytest=2.5.1 https://github.com/davehunt/pytest-mozwebqa Mozilla WebQA plugin for py.test. + :target: http://pytest-plugs.herokuapp.com/output/pytest-mozwebqa-1.1.1?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-mozwebqa-1.1.1?py=py33&pytest=2.5.1 + `pytest-oerp-0.2.0 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-oerp-0.2.0?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-oerp-0.2.0?py=py33&pytest=2.5.1 http://github.com/santagada/pytest-oerp/ pytest plugin to test OpenERP modules + :target: http://pytest-plugs.herokuapp.com/output/pytest-oerp-0.2.0?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-oerp-0.2.0?py=py33&pytest=2.5.1 + `pytest-osxnotify-0.1.4 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-osxnotify-0.1.4?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-osxnotify-0.1.4?py=py33&pytest=2.5.1 https://github.com/dbader/pytest-osxnotify OS X notifications for py.test results. + :target: http://pytest-plugs.herokuapp.com/output/pytest-osxnotify-0.1.4?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-osxnotify-0.1.4?py=py33&pytest=2.5.1 + `pytest-paste-config-0.1 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-paste-config-0.1?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-paste-config-0.1?py=py33&pytest=2.5.1 UNKNOWN Allow setting the path to a paste config file + :target: http://pytest-plugs.herokuapp.com/output/pytest-paste-config-0.1?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-paste-config-0.1?py=py33&pytest=2.5.1 + `pytest-pep8-1.0.5 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-pep8-1.0.5?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-pep8-1.0.5?py=py33&pytest=2.5.1 http://bitbucket.org/hpk42/pytest-pep8/ pytest plugin to check PEP8 requirements + :target: http://pytest-plugs.herokuapp.com/output/pytest-pep8-1.0.5?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-pep8-1.0.5?py=py33&pytest=2.5.1 + `pytest-poo-0.2 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-poo-0.2?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-poo-0.2?py=py33&pytest=2.5.1 http://github.com/pelme/pytest-poo Visualize your crappy tests + :target: http://pytest-plugs.herokuapp.com/output/pytest-poo-0.2?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-poo-0.2?py=py33&pytest=2.5.1 + `pytest-pydev-0.1 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-pydev-0.1?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-pydev-0.1?py=py33&pytest=2.5.1 http://bitbucket.org/basti/pytest-pydev/ py.test plugin to connect to a remote debug server with PyDev or PyCharm. + :target: http://pytest-plugs.herokuapp.com/output/pytest-pydev-0.1?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-pydev-0.1?py=py33&pytest=2.5.1 + `pytest-qt-1.0.2 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-qt-1.0.2?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-qt-1.0.2?py=py33&pytest=2.5.1 http://github.com/nicoddemus/pytest-qt pytest plugin that adds fixtures for testing Qt (PyQt and PySide) applications. + :target: http://pytest-plugs.herokuapp.com/output/pytest-qt-1.0.2?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-qt-1.0.2?py=py33&pytest=2.5.1 + `pytest-quickcheck-0.8 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-quickcheck-0.8?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-quickcheck-0.8?py=py33&pytest=2.5.1 http://bitbucket.org/t2y/pytest-quickcheck/ pytest plugin to generate random data inspired by QuickCheck + :target: http://pytest-plugs.herokuapp.com/output/pytest-quickcheck-0.8?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-quickcheck-0.8?py=py33&pytest=2.5.1 + `pytest-rage-0.1 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-rage-0.1?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-rage-0.1?py=py33&pytest=2.5.1 http://github.com/santagada/pytest-rage/ pytest plugin to implement PEP712 + :target: http://pytest-plugs.herokuapp.com/output/pytest-rage-0.1?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-rage-0.1?py=py33&pytest=2.5.1 + `pytest-random-0.02 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-random-0.02?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-random-0.02?py=py33&pytest=2.5.1 https://github.com/klrmn/pytest-random py.test plugin to randomize tests + :target: http://pytest-plugs.herokuapp.com/output/pytest-random-0.02?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-random-0.02?py=py33&pytest=2.5.1 + `pytest-rerunfailures-0.03 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-rerunfailures-0.03?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-rerunfailures-0.03?py=py33&pytest=2.5.1 https://github.com/klrmn/pytest-rerunfailures py.test plugin to re-run tests to eliminate flakey failures + :target: http://pytest-plugs.herokuapp.com/output/pytest-rerunfailures-0.03?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-rerunfailures-0.03?py=py33&pytest=2.5.1 + `pytest-runfailed-0.3 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-runfailed-0.3?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-runfailed-0.3?py=py33&pytest=2.5.1 http://github.com/dmerejkowsky/pytest-runfailed implement a --failed option for pytest + :target: http://pytest-plugs.herokuapp.com/output/pytest-runfailed-0.3?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-runfailed-0.3?py=py33&pytest=2.5.1 + `pytest-runner-2.0 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-runner-2.0?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-runner-2.0?py=py33&pytest=2.5.1 https://bitbucket.org/jaraco/pytest-runner UNKNOWN + :target: http://pytest-plugs.herokuapp.com/output/pytest-runner-2.0?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-runner-2.0?py=py33&pytest=2.5.1 + `pytest-sugar-0.2.2 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-sugar-0.2.2?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-sugar-0.2.2?py=py33&pytest=2.5.1 http://pivotfinland.com/pytest-sugar/ py.test plugin that adds instafail, ETA and neat graphics + :target: http://pytest-plugs.herokuapp.com/output/pytest-sugar-0.2.2?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-sugar-0.2.2?py=py33&pytest=2.5.1 + `pytest-timeout-0.3 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-timeout-0.3?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-timeout-0.3?py=py33&pytest=2.5.1 http://bitbucket.org/flub/pytest-timeout/ pytest plugin to abort tests after a timeout + :target: http://pytest-plugs.herokuapp.com/output/pytest-timeout-0.3?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-timeout-0.3?py=py33&pytest=2.5.1 + `pytest-twisted-1.4 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-twisted-1.4?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-twisted-1.4?py=py33&pytest=2.5.1 https://github.com/schmir/pytest-twisted A twisted plugin for py.test. + :target: http://pytest-plugs.herokuapp.com/output/pytest-twisted-1.4?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-twisted-1.4?py=py33&pytest=2.5.1 + `pytest-xdist-1.9 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-xdist-1.9?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-xdist-1.9?py=py33&pytest=2.5.1 http://bitbucket.org/hpk42/pytest-xdist py.test xdist plugin for distributed testing and loop-on-failing modes + :target: http://pytest-plugs.herokuapp.com/output/pytest-xdist-1.9?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-xdist-1.9?py=py33&pytest=2.5.1 + `pytest-xprocess-0.8 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-xprocess-0.8?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-xprocess-0.8?py=py33&pytest=2.5.1 http://bitbucket.org/hpk42/pytest-xprocess/ pytest plugin to manage external processes across test runs + :target: http://pytest-plugs.herokuapp.com/output/pytest-xprocess-0.8?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-xprocess-0.8?py=py33&pytest=2.5.1 + `pytest-yamlwsgi-0.6 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-yamlwsgi-0.6?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-yamlwsgi-0.6?py=py33&pytest=2.5.1 UNKNOWN Run tests against wsgi apps defined in yaml + :target: http://pytest-plugs.herokuapp.com/output/pytest-yamlwsgi-0.6?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-yamlwsgi-0.6?py=py33&pytest=2.5.1 + `pytest-zap-0.1 `_ .. image:: http://pytest-plugs.herokuapp.com/status/pytest-zap-0.1?py=py27&pytest=2.5.1 .. image:: http://pytest-plugs.herokuapp.com/status/pytest-zap-0.1?py=py33&pytest=2.5.1 https://github.com/davehunt/pytest-zap OWASP ZAP plugin for py.test. + :target: http://pytest-plugs.herokuapp.com/output/pytest-zap-0.1?py=py27&pytest=2.5.1 :target: http://pytest-plugs.herokuapp.com/output/pytest-zap-0.1?py=py33&pytest=2.5.1 + +========================================================================================== ======================================================================================================== ======================================================================================================== ============================================================= ============================================================================================================================================= + +*(Updated on 2014-01-15)* diff --git a/doc/en/projects.txt b/doc/en/projects.txt index 1544b015c..0883db36c 100644 --- a/doc/en/projects.txt +++ b/doc/en/projects.txt @@ -20,10 +20,10 @@ Project examples ========================== -Here are some examples of projects using py.test (please send notes via :ref:`contact`): +Here are some examples of projects using ``pytest`` (please send notes via :ref:`contact`): -* `PyPy `_, Python with a JIT compiler, running over - `16000 tests `_ +* `PyPy `_, Python with a JIT compiler, running over + `21000 tests `_ * the `MoinMoin `_ Wiki Engine * `sentry `_, realtime app-maintenance and exception tracking * `tox `_, virtualenv/Hudson integration tool @@ -60,7 +60,7 @@ Here are some examples of projects using py.test (please send notes via :ref:`co * `pytest-localserver `_ a plugin for pytest that provides a httpserver and smtpserver * `pytest-monkeyplus `_ a plugin that extends monkeypatch -These projects help integrate py.test into other Python frameworks: +These projects help integrate ``pytest`` into other Python frameworks: * `pytest-django `_ for Django * `zope.pytest `_ for Zope and Grok @@ -68,7 +68,7 @@ These projects help integrate py.test into other Python frameworks: * There is `some work `_ underway for Kotti, a CMS built in Pyramid/Pylons -Some organisations using py.test +Some organisations using pytest ----------------------------------- * `Square Kilometre Array, Cape Town `_ diff --git a/doc/en/skipping.txt b/doc/en/skipping.txt index 4be597478..fd2f2d952 100644 --- a/doc/en/skipping.txt +++ b/doc/en/skipping.txt @@ -14,7 +14,7 @@ A *skip* means that you expect your test to pass unless the environment And *xfail* means that your test can run but you expect it to fail because there is an implementation problem. -py.test counts and lists *skip* and *xfail* tests separately. Detailed +``pytest`` counts and lists *skip* and *xfail* tests separately. Detailed information about skipped/xfailed tests is not shown by default to avoid cluttering the output. You can use the ``-r`` option to see details corresponding to the "short" letters shown in the test progress:: @@ -29,7 +29,7 @@ corresponding to the "short" letters shown in the test progress:: Marking a test function to be skipped ------------------------------------------- -.. versionadded:: 2.4 +.. versionadded:: 2.0, 2.4 Here is an example of marking a test function to be skipped when run on a Python3.3 interpreter:: @@ -70,7 +70,8 @@ For larger test suites it's usually a good idea to have one file where you define the markers which you then consistently apply throughout your test suite. -Alternatively, the pre pytest-2.4 way to specify `condition strings `_ instead of booleans will remain fully supported in future +Alternatively, the pre pytest-2.4 way to specify :ref:`condition strings +` instead of booleans will remain fully supported in future versions of pytest. It couldn't be easily used for importing markers between test modules so it's no longer advertised as the primary method. @@ -103,7 +104,7 @@ you can set the ``pytestmark`` attribute of a class:: "will not be setup or run under 'win32' platform" As with the class-decorator, the ``pytestmark`` special name tells -py.test to apply it to each test function in the class. +``pytest`` to apply it to each test function in the class. If you want to skip all test functions of a module, you must use the ``pytestmark`` name on the global level:: @@ -158,7 +159,7 @@ Running it with the report-on-xfail option gives this output:: example $ py.test -rx xfail_demo.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 6 items xfail_demo.py xxxxxx @@ -175,7 +176,7 @@ Running it with the report-on-xfail option gives this output:: XFAIL xfail_demo.py::test_hello6 reason: reason - ======================== 6 xfailed in 0.05 seconds ========================= + ======================== 6 xfailed in 0.04 seconds ========================= .. _`skip/xfail with parametrize`: @@ -183,7 +184,7 @@ Skip/xfail with parametrize --------------------------- It is possible to apply markers like skip and xfail to individual -test instances when using parametrize: +test instances when using parametrize:: import pytest @@ -191,7 +192,7 @@ test instances when using parametrize: (1, 2), pytest.mark.xfail((1, 0)), pytest.mark.xfail(reason="some bug")((1, 3)), - (2, 3), + (2, 3), (3, 4), (4, 5), pytest.mark.skipif("sys.version_info >= (3,0)")((10, 11)), @@ -211,7 +212,7 @@ imperatively, in test or setup code:: if not valid_config(): pytest.xfail("failing configuration (but should work)") # or - pytest.skipif("unsupported configuration") + pytest.skip("unsupported configuration") Skipping on a missing import dependency @@ -232,7 +233,7 @@ The version will be read from the specified module's ``__version__`` attribute. -.. _`string conditions`: +.. _string conditions: specifying conditions as strings versus booleans ---------------------------------------------------------- @@ -254,7 +255,7 @@ because markers can then be freely imported between test modules. With strings you need to import not only the marker but all variables everything used by the marker, which violates encapsulation. -The reason for specifying the condition as a string was that py.test can +The reason for specifying the condition as a string was that ``pytest`` can report a summary of skip conditions based purely on the condition string. With conditions as booleans you are required to specify a ``reason`` string. diff --git a/doc/en/status.txt b/doc/en/status.txt new file mode 100644 index 000000000..da9da7a52 --- /dev/null +++ b/doc/en/status.txt @@ -0,0 +1,5 @@ +pytest development status +================================ + +https://drone.io/bitbucket.org/hpk42/pytest + diff --git a/doc/en/talks.txt b/doc/en/talks.txt index 4d562c30f..75b6e4cfa 100644 --- a/doc/en/talks.txt +++ b/doc/en/talks.txt @@ -10,14 +10,20 @@ Tutorial examples and blog postings .. _`tutorial1 repository`: http://bitbucket.org/hpk42/pytest-tutorial1/ .. _`pycon 2010 tutorial PDF`: http://bitbucket.org/hpk42/pytest-tutorial1/raw/tip/pytest-basic.pdf -Basic usage and funcargs: +Basic usage and fixtures: + +- `pytest feature and release highlights (GERMAN, October 2013) + `_ - `pytest introduction from Brian Okken (January 2013) `_ +- `3-part blog series about pytest from Daniel Greenfeld (January + 2014) `_ + - `pycon australia 2012 pytest talk from Brianna Laugher `_ (`video `_, `slides `_, `code `_) -- `pycon 2012 US talk video from Holger Krekel `_ +- `pycon 2012 US talk video from Holger Krekel `_ - `pycon 2010 tutorial PDF`_ and `tutorial1 repository`_ @@ -36,8 +42,8 @@ Test parametrization: Assertion introspection: -- `(07/2011) Behind the scenes of py.test's new assertion rewriting - `_ +- `(07/2011) Behind the scenes of pytest's new assertion rewriting + `_ Distributed testing: @@ -45,11 +51,11 @@ Distributed testing: Plugin specific examples: -- `skipping slow tests by default in py.test`_ (blog entry) +- `skipping slow tests by default in pytest`_ (blog entry) - `many examples in the docs for plugins`_ -.. _`skipping slow tests by default in py.test`: http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html +.. _`skipping slow tests by default in pytest`: http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html .. _`many examples in the docs for plugins`: plugin/index.html .. _`monkeypatch plugin`: plugin/monkeypatch.html .. _`application setup in test functions with funcargs`: funcargs.html#appsetup @@ -66,14 +72,14 @@ Older conference talks and tutorials - `ep2009-rapidtesting.pdf`_ tutorial slides (July 2009): - testing terminology - - basic py.test usage, file system layout + - basic pytest usage, file system layout - test function arguments (funcargs_) and test fixtures - existing plugins - distributed testing -- `ep2009-pytest.pdf`_ 60 minute py.test talk, highlighting unique features and a roadmap (July 2009) +- `ep2009-pytest.pdf`_ 60 minute pytest talk, highlighting unique features and a roadmap (July 2009) -- `pycon2009-pytest-introduction.zip`_ slides and files, extended version of py.test basic introduction, discusses more options, also introduces old-style xUnit setup, looponfailing and other features. +- `pycon2009-pytest-introduction.zip`_ slides and files, extended version of pytest basic introduction, discusses more options, also introduces old-style xUnit setup, looponfailing and other features. - `pycon2009-pytest-advanced.pdf`_ contain a slightly older version of funcargs and distributed testing, compared to the EuroPython 2009 slides. diff --git a/doc/en/test/attic.txt b/doc/en/test/attic.txt index b90ab4be9..6408c7225 100644 --- a/doc/en/test/attic.txt +++ b/doc/en/test/attic.txt @@ -13,7 +13,7 @@ writing conftest.py files You may put conftest.py files containing project-specific configuration in your project's root directory, it's usually best to put it just into the same directory level as your -topmost ``__init__.py``. In fact, ``py.test`` performs +topmost ``__init__.py``. In fact, ``pytest`` performs an "upwards" search starting from the directory that you specify to be tested and will lookup configuration values right-to-left. You may have options that reside e.g. in your home directory diff --git a/doc/en/test/index.txt b/doc/en/test/index.txt index 475870d5e..1a3b5a54d 100644 --- a/doc/en/test/index.txt +++ b/doc/en/test/index.txt @@ -1,5 +1,5 @@ ======================================= -py.test documentation index +pytest documentation index ======================================= @@ -17,7 +17,7 @@ customize_: configuration, customization, extensions changelog_: history of changes covering last releases -**Continuous Integration of py.test's own tests and plugins with Hudson**: +**Continuous Integration of pytest's own tests and plugins with Hudson**: `http://hudson.testrun.org/view/pytest`_ diff --git a/doc/en/test/mission.txt b/doc/en/test/mission.txt index 469e50597..cda8d9a72 100644 --- a/doc/en/test/mission.txt +++ b/doc/en/test/mission.txt @@ -2,10 +2,10 @@ Mission ==================================== -py.test strives to make testing a fun and no-boilerplate effort. +``pytest`` strives to make testing a fun and no-boilerplate effort. -The tool is distributed as part of the `py` package which contains supporting APIs that -are also usable independently. The project independent ``py.test`` command line tool helps you to: +The tool is distributed as a `pytest` package. Its project independent +``py.test`` command line tool helps you to: * rapidly collect and run tests * run unit- or doctests, functional or integration tests diff --git a/doc/en/test/plugin/django.txt b/doc/en/test/plugin/django.txt index 9d513af81..061497b38 100644 --- a/doc/en/test/plugin/django.txt +++ b/doc/en/test/plugin/django.txt @@ -1,7 +1,7 @@ pytest_django plugin (EXTERNAL) ========================================== -pytest_django is a plugin for py.test that provides a set of useful tools for testing Django applications, checkout Ben Firshman's `pytest_django github page`_. +pytest_django is a plugin for ``pytest`` that provides a set of useful tools for testing Django applications, checkout Ben Firshman's `pytest_django github page`_. .. _`pytest_django github page`: http://github.com/bfirsh/pytest_django/tree/master diff --git a/doc/en/test/plugin/genscript.txt b/doc/en/test/plugin/genscript.txt index f102afe39..b2e1c58af 100644 --- a/doc/en/test/plugin/genscript.txt +++ b/doc/en/test/plugin/genscript.txt @@ -13,7 +13,7 @@ command line options ``--genscript=path`` - create standalone py.test script at given target path. + create standalone ``pytest`` script at given target path. Start improving this plugin in 30 seconds ========================================= @@ -21,7 +21,7 @@ Start improving this plugin in 30 seconds 1. Download `pytest_genscript.py`_ plugin source code 2. put it somewhere as ``pytest_genscript.py`` into your import path -3. a subsequent ``py.test`` run will use your local version +3. a subsequent ``pytest`` run will use your local version Checkout customize_, other plugins_ or `get in contact`_. diff --git a/doc/en/test/plugin/helpconfig.txt b/doc/en/test/plugin/helpconfig.txt index 966bbc988..9b5b8cddd 100644 --- a/doc/en/test/plugin/helpconfig.txt +++ b/doc/en/test/plugin/helpconfig.txt @@ -19,7 +19,7 @@ command line options ``--traceconfig`` trace considerations of conftest.py files. ``--nomagic`` - don't reinterpret asserts, no traceback cutting. + don't reinterpret asserts, no traceback cutting. ``--debug`` generate and show internal debugging information. ``--help-config`` @@ -31,7 +31,7 @@ Start improving this plugin in 30 seconds 1. Download `pytest_helpconfig.py`_ plugin source code 2. put it somewhere as ``pytest_helpconfig.py`` into your import path -3. a subsequent ``py.test`` run will use your local version +3. a subsequent ``pytest`` run will use your local version Checkout customize_, other plugins_ or `get in contact`_. diff --git a/doc/en/test/plugin/links.txt b/doc/en/test/plugin/links.txt index b7d427856..aa965e730 100644 --- a/doc/en/test/plugin/links.txt +++ b/doc/en/test/plugin/links.txt @@ -22,7 +22,7 @@ .. _`capturelog`: capturelog.html .. _`junitxml`: junitxml.html .. _`pytest_skipping.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_skipping.py -.. _`checkout the py.test development version`: ../../install.html#checkout +.. _`checkout the pytest development version`: ../../install.html#checkout .. _`pytest_helpconfig.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_helpconfig.py .. _`oejskit`: oejskit.html .. _`doctest`: doctest.html diff --git a/doc/en/test/plugin/nose.txt b/doc/en/test/plugin/nose.txt index a0294207b..f3aa7d705 100644 --- a/doc/en/test/plugin/nose.txt +++ b/doc/en/test/plugin/nose.txt @@ -7,7 +7,7 @@ nose-compatibility plugin: allow to run nose test suites natively. :local: This is an experimental plugin for allowing to run tests written -in 'nosetests style with py.test. +in 'nosetests' style with ``pytest``. Usage ------------- @@ -17,7 +17,7 @@ type:: py.test # instead of 'nosetests' and you should be able to run nose style tests and at the same -time can make full use of py.test's capabilities. +time can make full use of pytest's capabilities. Supported nose Idioms ---------------------- @@ -40,7 +40,7 @@ If you find other issues or have suggestions please run:: py.test --pastebin=all -and send the resulting URL to a py.test contact channel, +and send the resulting URL to a ``pytest`` contact channel, at best to the mailing list. Start improving this plugin in 30 seconds @@ -49,7 +49,7 @@ Start improving this plugin in 30 seconds 1. Download `pytest_nose.py`_ plugin source code 2. put it somewhere as ``pytest_nose.py`` into your import path -3. a subsequent ``py.test`` run will use your local version +3. a subsequent ``pytest`` run will use your local version Checkout customize_, other plugins_ or `get in contact`_. diff --git a/doc/en/test/plugin/oejskit.txt b/doc/en/test/plugin/oejskit.txt index f664fd03f..4995aa17c 100644 --- a/doc/en/test/plugin/oejskit.txt +++ b/doc/en/test/plugin/oejskit.txt @@ -1,7 +1,7 @@ pytest_oejskit plugin (EXTERNAL) ========================================== -The `oejskit`_ offers a py.test plugin for running Javascript tests in life browsers. Running inside the browsers comes with some speed cost, on the other hand it means for example the code is tested against the real-word DOM implementations. +The `oejskit`_ offers a ``pytest`` plugin for running Javascript tests in live browsers. Running inside the browsers comes with some speed cost, on the other hand it means for example the code is tested against the real-word DOM implementations. The approach enables to write integration tests such that the JavaScript code is tested against server-side Python code mocked as necessary. Any server-side framework that can already be exposed through WSGI (or for which a subset of WSGI can be written to accommodate the jskit own needs) can play along. For more info and download please visit the `oejskit PyPI`_ page. diff --git a/doc/en/test/plugin/terminal.txt b/doc/en/test/plugin/terminal.txt index 0d8b8505f..214c24dfc 100644 --- a/doc/en/test/plugin/terminal.txt +++ b/doc/en/test/plugin/terminal.txt @@ -33,7 +33,7 @@ Start improving this plugin in 30 seconds 1. Download `pytest_terminal.py`_ plugin source code 2. put it somewhere as ``pytest_terminal.py`` into your import path -3. a subsequent ``py.test`` run will use your local version +3. a subsequent ``pytest`` run will use your local version Checkout customize_, other plugins_ or `get in contact`_. diff --git a/doc/en/test/plugin/xdist.txt b/doc/en/test/plugin/xdist.txt index 3116658c4..df5d5d199 100644 --- a/doc/en/test/plugin/xdist.txt +++ b/doc/en/test/plugin/xdist.txt @@ -6,13 +6,13 @@ loop on failing tests, distribute test runs to CPUs and hosts. .. contents:: :local: -The `pytest-xdist`_ plugin extends py.test with some unique +The `pytest-xdist`_ plugin extends ``pytest`` with some unique test execution modes: -* Looponfail: run your tests repeatedly in a subprocess. After each run py.test - waits until a file in your project changes and then re-runs the previously - failing tests. This is repeated until all tests pass after which again - a full run is performed. +* Looponfail: run your tests repeatedly in a subprocess. After each run + ``pytest`` waits until a file in your project changes and then re-runs the + previously failing tests. This is repeated until all tests pass after which + again a full run is performed. * Load-balancing: if you have multiple CPUs or hosts you can use those for a combined test run. This allows to speed up @@ -21,7 +21,7 @@ test execution modes: * Multi-Platform coverage: you can specify different Python interpreters or different platforms and run tests in parallel on all of them. -Before running tests remotely, ``py.test`` efficiently synchronizes your +Before running tests remotely, ``pytest`` efficiently synchronizes your program source code to the remote place. All test results are reported back and displayed to your local test session. You may specify different Python versions and interpreters. @@ -77,11 +77,11 @@ and send them to remote places for execution. You can specify multiple ``--rsyncdir`` directories to be sent to the remote side. -**NOTE:** For py.test to collect and send tests correctly +**NOTE:** For ``pytest`` to collect and send tests correctly you not only need to make sure all code and tests directories are rsynced, but that any test (sub) directory also has an ``__init__.py`` file because internally -py.test references tests as a fully qualified python +``pytest`` references tests using their fully qualified python module path. **You will otherwise get strange errors** during setup of the remote side. @@ -156,11 +156,11 @@ command line options box each test run in a separate process (unix) ``--dist=distmode`` set mode for distributing tests to exec environments. - + each: send each test to each available environment. - + load: send each test to available environment. - + (default) no: run tests inprocess, don't distribute. ``--tx=xspec`` add a test execution environment. some examples: --tx popen//python=python2.5 --tx socket=192.168.1.102:8888 --tx ssh=user@codespeak.net//chdir=testcache diff --git a/doc/en/tmpdir.txt b/doc/en/tmpdir.txt index 531b3433a..f7ac60fef 100644 --- a/doc/en/tmpdir.txt +++ b/doc/en/tmpdir.txt @@ -29,7 +29,7 @@ Running this would result in a passed test except for the last $ py.test test_tmpdir.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 1 items test_tmpdir.py F @@ -37,7 +37,7 @@ Running this would result in a passed test except for the last ================================= FAILURES ================================= _____________________________ test_create_file _____________________________ - tmpdir = local('/tmp/pytest-323/test_create_file0') + tmpdir = local('/tmp/pytest-39/test_create_file0') def test_create_file(tmpdir): p = tmpdir.mkdir("sub").join("hello.txt") @@ -48,7 +48,7 @@ Running this would result in a passed test except for the last E assert 0 test_tmpdir.py:7: AssertionError - ========================= 1 failed in 0.02 seconds ========================= + ========================= 1 failed in 0.01 seconds ========================= .. _`base temporary directory`: @@ -64,7 +64,7 @@ You can override the default temporary directory setting like this:: py.test --basetemp=mydir -When distributing tests on the local machine, ``py.test`` takes care to +When distributing tests on the local machine, ``pytest`` takes care to configure a basetemp directory for the sub processes such that all temporary data lands below a single per-test run basetemp directory. diff --git a/doc/en/unittest.txt b/doc/en/unittest.txt index 9184584fd..7256f3de3 100644 --- a/doc/en/unittest.txt +++ b/doc/en/unittest.txt @@ -6,13 +6,13 @@ Support for unittest.TestCase / Integration of fixtures .. _`unittest.py style`: http://docs.python.org/library/unittest.html -py.test has support for running Python `unittest.py style`_ tests. +``pytest`` has support for running Python `unittest.py style`_ tests. It's meant for leveraging existing unittest-style projects to use pytest features. Concretely, pytest will automatically collect ``unittest.TestCase`` subclasses and their ``test`` methods in test files. It will invoke typical setup/teardown methods and generally try to make test suites written to run on unittest, to also -run using ``py.test``. We assume here that you are familiar with writing +run using ``pytest``. We assume here that you are familiar with writing ``unittest.TestCase`` style tests and rather focus on integration aspects. @@ -30,12 +30,12 @@ you can make use of most :ref:`pytest features `, for example :ref:`more informative tracebacks `, stdout-capturing or distributing tests to multiple CPUs via the ``-nNUM`` option if you installed the ``pytest-xdist`` plugin. Please refer to -the general pytest documentation for many more examples. +the general ``pytest`` documentation for many more examples. Mixing pytest fixtures into unittest.TestCase style tests ----------------------------------------------------------- -Running your unittest with ``py.test`` allows you to use its +Running your unittest with ``pytest`` allows you to use its :ref:`fixture mechanism ` with ``unittest.TestCase`` style tests. Assuming you have at least skimmed the pytest fixture features, let's jump-start into an example that integrates a pytest ``db_class`` @@ -88,7 +88,7 @@ the ``self.db`` values in the traceback:: $ py.test test_unittest_db.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.5 + platform linux2 -- Python 2.7.3 -- pytest-2.5.1 collected 2 items test_unittest_db.py FF @@ -101,7 +101,7 @@ the ``self.db`` values in the traceback:: def test_method1(self): assert hasattr(self, "db") > assert 0, self.db # fail for demo purposes - E AssertionError: + E AssertionError: test_unittest_db.py:9: AssertionError ___________________________ MyTest.test_method2 ____________________________ @@ -110,10 +110,10 @@ the ``self.db`` values in the traceback:: def test_method2(self): > assert 0, self.db # fail for demo purposes - E AssertionError: + E AssertionError: test_unittest_db.py:12: AssertionError - ========================= 2 failed in 0.02 seconds ========================= + ========================= 2 failed in 0.01 seconds ========================= This default pytest traceback shows that the two test methods share the same ``self.db`` instance which was our intention @@ -160,6 +160,7 @@ Running this test module ...:: $ py.test -q test_unittest_cleandir.py . + 1 passed in 0.01 seconds ... gives us one passed test because the ``initdir`` fixture function was executed ahead of the ``test_method``. diff --git a/doc/en/usage.txt b/doc/en/usage.txt index de3a501f4..29a226dbc 100644 --- a/doc/en/usage.txt +++ b/doc/en/usage.txt @@ -44,8 +44,11 @@ Specifying tests / selecting tests Several test run options:: py.test test_mod.py # run tests in module - py.test somepath # run all tests below path - py.test -k string # only run tests whose names contain a string + py.test somepath # run all tests below somepath + py.test -k stringexpr # only run tests with names that match the + # the "string expression", e.g. "MyClass and not method" + # will select TestMyClass.test_something + # but not TestMyClass.test_method_simple Import 'pkg' and use its filesystem location to find and run tests:: @@ -69,7 +72,7 @@ Dropping to PDB (Python Debugger) on failures .. _PDB: http://docs.python.org/library/pdb.html -Python comes with a builtin Python debugger called PDB_. ``py.test`` +Python comes with a builtin Python debugger called PDB_. ``pytest`` allows one to drop into the PDB prompt via a command line option:: py.test --pdb @@ -150,6 +153,17 @@ for example ``-x`` if you only want to send one particular failure. Currently only pasting to the http://bpaste.net service is implemented. +Disabling plugins +----------------- + +To disable loading specific plugins at invocation time, use the ``-p`` option +together with the prefix ``no:``. + +Example: to disable loading the plugin ``doctest``, which is responsible for +executing doctest tests from text files, invoke py.test like this:: + + py.test -p no:doctest + .. _`pytest.main-usage`: Calling pytest from Python code @@ -157,7 +171,7 @@ Calling pytest from Python code .. versionadded:: 2.0 -You can invoke ``py.test`` from Python code directly:: +You can invoke ``pytest`` from Python code directly:: pytest.main() @@ -185,7 +199,7 @@ Running it will show that ``MyPlugin`` was added and its hook was invoked:: $ python myinvoke.py - *** test run reporting finishing + .. include:: links.inc diff --git a/doc/en/xdist.txt b/doc/en/xdist.txt index 6aa4be117..bf092d369 100644 --- a/doc/en/xdist.txt +++ b/doc/en/xdist.txt @@ -4,11 +4,11 @@ xdist: pytest distributed testing plugin =============================================================== -The `pytest-xdist`_ plugin extends py.test with some unique +The `pytest-xdist`_ plugin extends ``pytest`` with some unique test execution modes: * Looponfail: run your tests repeatedly in a subprocess. After each - run, py.test waits until a file in your project changes and then + run, ``pytest`` waits until a file in your project changes and then re-runs the previously failing tests. This is repeated until all tests pass. At this point a full run is again performed. @@ -19,7 +19,7 @@ test execution modes: * Multi-Platform coverage: you can specify different Python interpreters or different platforms and run tests in parallel on all of them. -Before running tests remotely, ``py.test`` efficiently "rsyncs" your +Before running tests remotely, ``pytest`` efficiently "rsyncs" your program source code to the remote place. All test results are reported back and displayed to your local terminal. You may specify different Python versions and interpreters. @@ -86,7 +86,7 @@ you can use the looponfailing mode. Simply add the ``--f`` option:: py.test -f -and py.test will run your tests. Assuming you have failures it will then +and ``pytest`` will run your tests. Assuming you have failures it will then wait for file changes and re-run the failing test set. File changes are detected by looking at ``looponfailingroots`` root directories and all of their contents (recursively). If the default for this value does not work for you you can change it in your project by setting a configuration option:: @@ -115,11 +115,11 @@ to be sent to the remote side. .. XXX CHECK - **NOTE:** For py.test to collect and send tests correctly + **NOTE:** For ``pytest`` to collect and send tests correctly you not only need to make sure all code and tests directories are rsynced, but that any test (sub) directory also has an ``__init__.py`` file because internally - py.test references tests as a fully qualified python + ``pytest`` references tests as a fully qualified python module path. **You will otherwise get strange errors** during setup of the remote side. diff --git a/doc/en/yieldfixture.txt b/doc/en/yieldfixture.txt new file mode 100644 index 000000000..eb84ac9f5 --- /dev/null +++ b/doc/en/yieldfixture.txt @@ -0,0 +1,124 @@ + +.. _yieldfixture: + +Fixture functions using "yield" / context manager integration +--------------------------------------------------------------- + +.. versionadded:: 2.4 + +.. regendoc:wipe + +pytest-2.4 allows fixture functions to seamlessly use a ``yield`` instead +of a ``return`` statement to provide a fixture value while otherwise +fully supporting all other fixture features. + +.. note:: + + "yielding" fixture values is an experimental feature and its exact + declaration may change later but earliest in a 2.5 release. You can thus + safely use this feature in the 2.4 series but may need to adapt later. + Test functions themselves will not need to change (as a general + feature, they are ignorant of how fixtures are setup). + +Let's look at a simple standalone-example using the new ``yield`` syntax:: + + # content of test_yield.py + + import pytest + + @pytest.yield_fixture + def passwd(): + print ("\nsetup before yield") + f = open("/etc/passwd") + yield f.readlines() + print ("teardown after yield") + f.close() + + def test_has_lines(passwd): + print ("test called") + assert passwd + +In contrast to :ref:`finalization through registering callbacks +`, our fixture function used a ``yield`` +statement to provide the lines of the ``/etc/passwd`` file. +The code after the ``yield`` statement serves as the teardown code, +avoiding the indirection of registering a teardown callback function. + +Let's run it with output capturing disabled:: + + $ py.test -q -s test_yield.py + + setup before yield + test called + .teardown after yield + + 1 passed in 0.00 seconds + +We can also seemlessly use the new syntax with ``with`` statements. +Let's simplify the above ``passwd`` fixture:: + + # content of test_yield2.py + + import pytest + + @pytest.yield_fixture + def passwd(): + with open("/etc/passwd") as f: + yield f.readlines() + + def test_has_lines(passwd): + assert len(passwd) >= 1 + +The file ``f`` will be closed after the test finished execution +because the Python ``file`` object supports finalization when +the ``with`` statement ends. + +Note that the new syntax is fully integrated with using ``scope``, +``params`` and other fixture features. Changing existing +fixture functions to use ``yield`` is thus straight forward. + +Discussion and future considerations / feedback +++++++++++++++++++++++++++++++++++++++++++++++++++++ + +The yield-syntax has been discussed by pytest users extensively. +In general, the advantages of the using a ``yield`` fixture syntax are: + +- easy provision of fixtures in conjunction with context managers. + +- no need to register a callback, providing for more synchronous + control flow in the fixture function. Also there is no need to accept + the ``request`` object into the fixture function just for providing + finalization code. + +However, there are also limitations or foreseeable irritations: + +- usually ``yield`` is used for producing multiple values. + But fixture functions can only yield exactly one value. + Yielding a second fixture value will get you an error. + It's possible we can evolve pytest to allow for producing + multiple values as an alternative to current parametrization. + For now, you can just use the normal + :ref:`fixture parametrization ` + mechanisms together with ``yield``-style fixtures. + +- the ``yield`` syntax is similar to what + :py:func:`contextlib.contextmanager` decorated functions + provide. With pytest fixture functions, the "after yield" part will + always be invoked, independently from the exception status + of the test function which uses the fixture. The pytest + behaviour makes sense if you consider that many different + test functions might use a module or session scoped fixture. + Some test functions might raise exceptions and others not, + so how could pytest re-raise a single exception at the + ``yield`` point in the fixture function? + +- lastly ``yield`` introduces more than one way to write + fixture functions, so what's the obvious way to a newcomer? + Newcomers reading the docs will see feature examples using the + ``return`` style so should use that, if in doubt. + Others can start experimenting with writing yield-style fixtures + and possibly help evolving them further. + +If you want to feedback or participate in the ongoing +discussion, please join our :ref:`contact channels`. +you are most welcome. diff --git a/extra/get_issues.py b/extra/get_issues.py new file mode 100644 index 000000000..6045d49c7 --- /dev/null +++ b/extra/get_issues.py @@ -0,0 +1,74 @@ +import json +import py +import textwrap + +issues_url = "http://bitbucket.org/api/1.0/repositories/hpk42/pytest/issues" + +import requests + +def get_issues(): + chunksize = 50 + start = 0 + issues = [] + while 1: + post_data = {"accountname": "hpk42", + "repo_slug": "pytest", + "start": start, + "limit": chunksize} + print ("getting from", start) + r = requests.get(issues_url, params=post_data) + data = r.json() + issues.extend(data["issues"]) + if start + chunksize >= data["count"]: + return issues + start += chunksize + +kind2num = "bug enhancement task proposal".split() + +status2num = "new open resolved duplicate invalid wontfix".split() + +def main(args): + cachefile = py.path.local(args.cache) + if not cachefile.exists() or args.refresh: + issues = get_issues() + cachefile.write(json.dumps(issues)) + else: + issues = json.loads(cachefile.read()) + + open_issues = [x for x in issues + if x["status"] in ("new", "open")] + + def kind_and_id(x): + kind = x["metadata"]["kind"] + return kind2num.index(kind), len(issues)-int(x["local_id"]) + open_issues.sort(key=kind_and_id) + report(open_issues) + +def report(issues): + for issue in issues: + metadata = issue["metadata"] + priority = issue["priority"] + title = issue["title"] + content = issue["content"] + kind = metadata["kind"] + status = issue["status"] + id = issue["local_id"] + link = "https://bitbucket.org/hpk42/pytest/issue/%s/" % id + print("----") + print(status, kind, link) + print(title) + #print() + #lines = content.split("\n") + #print ("\n".join(lines[:3])) + #if len(lines) > 3 or len(content) > 240: + # print ("...") + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser("process bitbucket issues") + parser.add_argument("--refresh", action="store_true", + help="invalidate cache, refresh issues") + parser.add_argument("--cache", action="store", default="issues.json", + help="cache file") + args = parser.parse_args() + main(args) diff --git a/plugin-test.sh b/plugin-test.sh new file mode 100644 index 000000000..9c61b5053 --- /dev/null +++ b/plugin-test.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# this assumes plugins are installed as sister directories + +set -e +cd ../pytest-pep8 +py.test +cd ../pytest-instafail +py.test +cd ../pytest-cache +py.test +cd ../pytest-xprocess +py.test +#cd ../pytest-cov +#py.test +cd ../pytest-capturelog +py.test +cd ../pytest-xdist +py.test + diff --git a/pytest.py b/pytest.py index 9897780b2..6c25c6195 100644 --- a/pytest.py +++ b/pytest.py @@ -8,9 +8,11 @@ if __name__ == '__main__': # if run as a script or by 'python -m pytest' # we trigger the below "else" condition by the following import import pytest raise SystemExit(pytest.main()) -else: - # we are simply imported - from _pytest.core import main, UsageError, _preloadplugins - from _pytest import core as cmdline - from _pytest import __version__ - _preloadplugins() # to populate pytest.* namespace so help(pytest) works + +# else we are imported + +from _pytest.config import main, UsageError, _preloadplugins, cmdline +from _pytest import __version__ + +_preloadplugins() # to populate pytest.* namespace so help(pytest) works + diff --git a/runtox.py b/runtox.py new file mode 100644 index 000000000..94e80ba4f --- /dev/null +++ b/runtox.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python + +import subprocess +import sys + +if __name__ == "__main__": + subprocess.call(["tox", + "-i", "ALL=https://devpi.net/hpk/dev/", + "--develop",] + sys.argv[1:]) + diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..6ebbc65a5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,8 @@ +[build_sphinx] +source-dir = doc/en/ +build-dir = doc/build +all_files = 1 + +[upload_sphinx] +upload-dir = doc/en/build/html + diff --git a/setup.py b/setup.py index 33ef4c60e..fd579e06d 100644 --- a/setup.py +++ b/setup.py @@ -1,39 +1,43 @@ import os, sys from setuptools import setup, Command +classifiers=['Development Status :: 6 - Mature', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: POSIX', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: MacOS :: MacOS X', + 'Topic :: Software Development :: Testing', + 'Topic :: Software Development :: Libraries', + 'Topic :: Utilities', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3'] + [ + ("Programming Language :: Python :: %s" % x) for x in + "2.6 2.7 3.0 3.1 3.2 3.3".split()] + long_description = open("README.rst").read() def main(): - install_requires = ["py>=1.4.15"] + install_requires = ["py>=1.4.20.dev2"] if sys.version_info < (2,7): install_requires.append("argparse") + if sys.platform == "win32": + install_requires.append("colorama") setup( name='pytest', - description='py.test: simple powerful testing with Python', + description='pytest: simple powerful testing with Python', long_description = long_description, - version='2.4.0.dev12', + version='2.5.2.dev1', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], author='Holger Krekel, Benjamin Peterson, Ronny Pfannschmidt, Floris Bruynooghe and others', author_email='holger at merlinux.eu', entry_points= make_entry_points(), + classifiers=classifiers, cmdclass = {'test': PyTest}, # the following should be enabled for release install_requires=install_requires, - classifiers=['Development Status :: 6 - Mature', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: POSIX', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: MacOS :: MacOS X', - 'Topic :: Software Development :: Testing', - 'Topic :: Software Development :: Libraries', - 'Topic :: Utilities', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 3'] + [ - ("Programming Language :: Python :: %s" % x) for x in - "2.4 2.5 2.6 2.7 3.0 3.1 3.2 3.3".split()], packages=['_pytest', '_pytest.assertion'], py_modules=['pytest'], zip_safe=False, diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 67e4a6ba0..0cfe38e47 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,4 +1,4 @@ -import sys, py, pytest +import py, pytest class TestGeneralUsage: def test_config_error(self, testdir): @@ -14,7 +14,7 @@ class TestGeneralUsage: ]) def test_root_conftest_syntax_error(self, testdir): - p = testdir.makepyfile(conftest="raise SyntaxError\n") + testdir.makepyfile(conftest="raise SyntaxError\n") result = testdir.runpytest() result.stderr.fnmatch_lines(["*raise SyntaxError*"]) assert result.ret != 0 @@ -67,7 +67,7 @@ class TestGeneralUsage: result = testdir.runpytest("-s", "asd") assert result.ret == 4 # EXIT_USAGEERROR result.stderr.fnmatch_lines(["ERROR: file not found*asd"]) - s = result.stdout.fnmatch_lines([ + result.stdout.fnmatch_lines([ "*---configure", "*---unconfigure", ]) @@ -316,6 +316,24 @@ class TestGeneralUsage: "*ERROR*test_b.py::b*", ]) + def test_namespace_import_doesnt_confuse_import_hook(self, testdir): + # Ref #383. Python 3.3's namespace package messed with our import hooks + # Importing a module that didn't exist, even if the ImportError was + # gracefully handled, would make our test crash. + testdir.mkdir('not_a_package') + p = testdir.makepyfile(""" + try: + from not_a_package import doesnt_exist + except ImportError: + # We handle the import error gracefully here + pass + + def test_whatever(): + pass + """) + res = testdir.runpytest(p.basename) + assert res.ret == 0 + class TestInvocationVariants: def test_earlyinit(self, testdir): @@ -399,15 +417,15 @@ class TestInvocationVariants: def test_equivalence_pytest_pytest(self): assert pytest.main == py.test.cmdline.main - def test_invoke_with_string(self, testdir, capsys): - retcode = testdir.pytestmain("-h") + def test_invoke_with_string(self, capsys): + retcode = pytest.main("-h") assert not retcode out, err = capsys.readouterr() assert "--help" in out pytest.raises(ValueError, lambda: pytest.main(0)) - def test_invoke_with_path(self, testdir, capsys): - retcode = testdir.pytestmain(testdir.tmpdir) + def test_invoke_with_path(self, tmpdir, capsys): + retcode = pytest.main(tmpdir) assert not retcode out, err = capsys.readouterr() @@ -416,7 +434,7 @@ class TestInvocationVariants: def pytest_addoption(self, parser): parser.addoption("--myopt") - testdir.pytestmain(["-h"], plugins=[MyPlugin()]) + pytest.main(["-h"], plugins=[MyPlugin()]) out, err = capsys.readouterr() assert "--myopt" in out @@ -512,7 +530,7 @@ class TestInvocationVariants: class TestDurations: source = """ import time - frag = 0.02 + frag = 0.002 def test_something(): pass def test_2(): @@ -527,7 +545,7 @@ class TestDurations: testdir.makepyfile(self.source) result = testdir.runpytest("--durations=10") assert result.ret == 0 - result.stdout.fnmatch_lines([ + result.stdout.fnmatch_lines_random([ "*durations*", "*call*test_3*", "*call*test_2*", @@ -538,12 +556,8 @@ class TestDurations: testdir.makepyfile(self.source) result = testdir.runpytest("--durations=2") assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*durations*", - "*call*test_3*", - "*call*test_2*", - ]) - assert "test_1" not in result.stdout.str() + lines = result.stdout.get_lines_after("*slowest*durations*") + assert "4 passed" in lines[2] def test_calls_showall(self, testdir): testdir.makepyfile(self.source) @@ -551,7 +565,6 @@ class TestDurations: assert result.ret == 0 for x in "123": for y in 'call',: #'setup', 'call', 'teardown': - l = [] for line in result.stdout.lines: if ("test_%s" % x) in line and y in line: break @@ -581,7 +594,7 @@ class TestDurations: class TestDurationWithFixture: source = """ import time - frag = 0.01 + frag = 0.001 def setup_function(func): time.sleep(frag * 3) def test_1(): @@ -594,9 +607,9 @@ class TestDurationWithFixture: result = testdir.runpytest("--durations=10") assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*durations*", - "* setup *test_1*", - "* call *test_1*", - ]) + result.stdout.fnmatch_lines_random(""" + *durations* + * setup *test_1* + * call *test_1* + """) diff --git a/testing/conftest.py b/testing/conftest.py index b4c06e5ee..204c4490e 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -12,10 +12,7 @@ def pytest_addoption(parser): help=("run FD checks if lsof is available")) def pytest_configure(config): - config.addinivalue_line("markers", - "multi(arg=[value1,value2, ...]): call the test function " - "multiple times with arg=value1, then with arg=value2, ... " - ) + config._basedir = py.path.local() if config.getvalue("lsof"): try: out = py.process.cmdexec("lsof -p %d" % pid) @@ -46,30 +43,13 @@ def check_open_files(config): config._numfiles = len(lines2) raise AssertionError("\n".join(error)) -@pytest.mark.tryfirst # XXX rather do item.addfinalizer -def pytest_runtest_setup(item): - item._oldir = py.path.local() - def pytest_runtest_teardown(item, __multicall__): - item._oldir.chdir() + item.config._basedir.chdir() if hasattr(item.config, '_numfiles'): x = __multicall__.execute() check_open_files(item.config) return x -def pytest_generate_tests(metafunc): - multi = getattr(metafunc.function, 'multi', None) - if multi is not None: - assert len(multi.kwargs) == 1 - for name, l in multi.kwargs.items(): - for val in l: - metafunc.addcall(funcargs={name: val}) - elif 'anypython' in metafunc.fixturenames: - for name in ('python2.5', 'python2.6', - 'python2.7', 'python3.2', "python3.3", - 'pypy', 'jython'): - metafunc.addcall(id=name, param=name) - # XXX copied from execnet's conftest.py - needs to be merged winpymap = { 'python2.7': r'C:\Python27\python.exe', @@ -100,7 +80,10 @@ def getexecutable(name, cache={}): cache[name] = executable return executable -def pytest_funcarg__anypython(request): +@pytest.fixture(params=['python2.5', 'python2.6', + 'python2.7', 'python3.2', "python3.3", + 'pypy', 'jython']) +def anypython(request): name = request.param executable = getexecutable(name) if executable is None: diff --git a/testing/python/collect.py b/testing/python/collect.py index 894eb6a91..381f68797 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1,6 +1,4 @@ -import pytest, py, sys -from _pytest import python as funcargs -from _pytest.python import FixtureLookupError +import pytest, py class TestModule: def test_failing_import(self, testdir): @@ -32,7 +30,7 @@ class TestModule: def test_module_considers_pluginmanager_at_import(self, testdir): modcol = testdir.getmodulecol("pytest_plugins='xasdlkj',") - pytest.raises(ImportError, "modcol.obj") + pytest.raises(ImportError, lambda: modcol.obj) class TestClass: def test_class_with_init_skip_collect(self, testdir): @@ -289,22 +287,10 @@ class TestFunction: pass f1 = pytest.Function(name="name", parent=session, config=config, args=(1,), callobj=func1) + assert f1 == f1 f2 = pytest.Function(name="name",config=config, - args=(1,), callobj=func2, parent=session) - assert not f1 == f2 + callobj=func2, parent=session) assert f1 != f2 - f3 = pytest.Function(name="name", parent=session, config=config, - args=(1,2), callobj=func2) - assert not f3 == f2 - assert f3 != f2 - - assert not f3 == f1 - assert f3 != f1 - - f1_b = pytest.Function(name="name", parent=session, config=config, - args=(1,), callobj=func1) - assert f1 == f1_b - assert not f1 != f1_b def test_issue197_parametrize_emptyset(self, testdir): testdir.makepyfile(""" @@ -316,6 +302,16 @@ class TestFunction: reprec = testdir.inline_run() reprec.assertoutcome(skipped=1) + def test_single_tuple_unwraps_values(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.parametrize(('arg',), [(1,)]) + def test_function(arg): + assert arg == 1 + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + def test_issue213_parametrize_value_no_equal(self, testdir): testdir.makepyfile(""" import pytest @@ -326,7 +322,7 @@ class TestFunction: def test_function(arg): assert arg.__class__.__name__ == "A" """) - reprec = testdir.inline_run() + reprec = testdir.inline_run("--fulltrace") reprec.assertoutcome(passed=1) def test_parametrize_with_non_hashable_values(self, testdir): @@ -347,6 +343,68 @@ class TestFunction: rec = testdir.inline_run() rec.assertoutcome(passed=2) + + def test_parametrize_with_non_hashable_values_indirect(self, testdir): + """Test parametrization with non-hashable values with indirect parametrization.""" + testdir.makepyfile(""" + archival_mapping = { + '1.0': {'tag': '1.0'}, + '1.2.2a1': {'tag': 'release-1.2.2a1'}, + } + + import pytest + + @pytest.fixture + def key(request): + return request.param + + @pytest.fixture + def value(request): + return request.param + + @pytest.mark.parametrize('key value'.split(), + archival_mapping.items(), indirect=True) + def test_archival_to_version(key, value): + assert key in archival_mapping + assert value == archival_mapping[key] + """) + rec = testdir.inline_run() + rec.assertoutcome(passed=2) + + + def test_parametrize_overrides_fixture(self, testdir): + """Test parametrization when parameter overrides existing fixture with same name.""" + testdir.makepyfile(""" + import pytest + + @pytest.fixture + def value(): + return 'value' + + @pytest.mark.parametrize('value', + ['overrided']) + def test_overrided_via_param(value): + assert value == 'overrided' + """) + rec = testdir.inline_run() + rec.assertoutcome(passed=1) + + + def test_parametrize_with_mark(selfself, testdir): + items = testdir.getitems(""" + import pytest + @pytest.mark.foo + @pytest.mark.parametrize('arg', [ + 1, + pytest.mark.bar(pytest.mark.baz(2)) + ]) + def test_function(arg): + pass + """) + keywords = [item.keywords for item in items] + assert 'foo' in keywords[0] and 'bar' not in keywords[0] and 'baz' not in keywords[0] + assert 'foo' in keywords[1] and 'bar' in keywords[1] and 'baz' in keywords[1] + def test_function_equality_with_callspec(self, testdir, tmpdir): items = testdir.getitems(""" import pytest @@ -596,7 +654,7 @@ class TestReportInfo: return MyFunction(name, parent=collector) """) item = testdir.getitem("def test_func(): pass") - runner = item.config.pluginmanager.getplugin("runner") + item.config.pluginmanager.getplugin("runner") assert item.location == ("ABCDE", 42, "custom") def test_func_reportinfo(self, testdir): @@ -686,7 +744,7 @@ def test_customized_python_discovery_functions(testdir): [pytest] python_functions=_test """) - p = testdir.makepyfile(""" + testdir.makepyfile(""" def _test_underscore(): pass """) diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 1218bb7da..79ae87cad 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -2,7 +2,7 @@ import pytest, py, sys from _pytest import python as funcargs from _pytest.python import FixtureLookupError from _pytest.pytester import get_public_names - +from textwrap import dedent def test_getfuncargnames(): def f(): pass @@ -247,7 +247,7 @@ class TestFillFixtures: assert result.ret == 0 def test_funcarg_lookup_error(self, testdir): - p = testdir.makepyfile(""" + testdir.makepyfile(""" def test_lookup_error(unknown): pass """) @@ -307,7 +307,7 @@ class TestRequestBasic: def pytest_funcarg__something(request): return 1 """) - item = testdir.makepyfile(""" + testdir.makepyfile(""" def pytest_funcarg__something(request): return request.getfuncargvalue("something") + 1 def test_func(something): @@ -473,7 +473,7 @@ class TestRequestBasic: assert l == ["module", "function", "class", "function", "method", "function"] """) - reprec = testdir.inline_run() + reprec = testdir.inline_run("-v") reprec.assertoutcome(passed=3) def test_fixtures_sub_subdir_normalize_sep(self, testdir): @@ -634,13 +634,13 @@ class TestRequestCachedSetup: l.append("setup") def teardown(val): l.append("teardown") - ret1 = req1.cached_setup(setup, teardown, scope="function") + req1.cached_setup(setup, teardown, scope="function") assert l == ['setup'] # artificial call of finalizer setupstate = req1._pyfuncitem.session._setupstate setupstate._callfinalizers(item1) assert l == ["setup", "teardown"] - ret2 = req1.cached_setup(setup, teardown, scope="function") + req1.cached_setup(setup, teardown, scope="function") assert l == ["setup", "teardown", "setup"] setupstate._callfinalizers(item1) assert l == ["setup", "teardown", "setup", "teardown"] @@ -914,6 +914,34 @@ class TestFixtureUsages: reprec = testdir.inline_run() reprec.assertoutcome(passed=1) + def test_fixture_parametrized_with_iterator(self, testdir): + testdir.makepyfile(""" + import pytest + + l = [] + def f(): + yield 1 + yield 2 + dec = pytest.fixture(scope="module", params=f()) + + @dec + def arg(request): + return request.param + @dec + def arg2(request): + return request.param + + def test_1(arg): + l.append(arg) + def test_2(arg2): + l.append(arg2*10) + """) + reprec = testdir.inline_run("-v") + reprec.assertoutcome(passed=4) + l = reprec.getcalls("pytest_runtest_call")[0].item.module.l + assert l == [1,2, 10,20] + + class TestFixtureManagerParseFactories: def pytest_funcarg__testdir(self, request): testdir = request.getfuncargvalue("testdir") @@ -1315,6 +1343,7 @@ class TestAutouseManagement: l.append("step2-%d" % item) def test_finish(): + print (l) assert l == ["setup-1", "step1-1", "step2-1", "teardown-1", "setup-2", "step1-2", "step2-2", "teardown-2",] """) @@ -1461,7 +1490,7 @@ class TestFixtureMarker: 'request.getfuncargvalue("arg")', 'request.cached_setup(lambda: None, scope="function")', ], ids=["getfuncargvalue", "cached_setup"]) - def test_scope_mismatch(self, testdir, method): + def test_scope_mismatch_various(self, testdir, method): testdir.makeconftest(""" import pytest finalized = [] @@ -1609,7 +1638,7 @@ class TestFixtureMarker: """) def test_class_ordering(self, testdir): - p = testdir.makeconftest(""" + testdir.makeconftest(""" import pytest l = [] @@ -1683,23 +1712,22 @@ class TestFixtureMarker: l.append("test3") def test_4(modarg, arg): l.append("test4") - def test_5(): - assert len(l) == 12 * 3 - expected = [ - 'create:1', 'test1', 'fin:1', 'create:2', 'test1', - 'fin:2', 'create:mod1', 'test2', 'create:1', 'test3', - 'fin:1', 'create:2', 'test3', 'fin:2', 'create:1', - 'test4', 'fin:1', 'create:2', 'test4', 'fin:2', - 'fin:mod1', 'create:mod2', 'test2', 'create:1', 'test3', - 'fin:1', 'create:2', 'test3', 'fin:2', 'create:1', - 'test4', 'fin:1', 'create:2', 'test4', 'fin:2', - 'fin:mod2'] - import pprint - pprint.pprint(list(zip(l, expected))) - assert l == expected """) reprec = testdir.inline_run("-v") - reprec.assertoutcome(passed=12+1) + reprec.assertoutcome(passed=12) + l = reprec.getcalls("pytest_runtest_call")[0].item.module.l + expected = [ + 'create:1', 'test1', 'fin:1', 'create:2', 'test1', + 'fin:2', 'create:mod1', 'test2', 'create:1', 'test3', + 'fin:1', 'create:2', 'test3', 'fin:2', 'create:1', + 'test4', 'fin:1', 'create:2', 'test4', 'fin:2', + 'fin:mod1', 'create:mod2', 'test2', 'create:1', 'test3', + 'fin:1', 'create:2', 'test3', 'fin:2', 'create:1', + 'test4', 'fin:1', 'create:2', 'test4', 'fin:2', + 'fin:mod2'] + import pprint + pprint.pprint(list(zip(l, expected))) + assert l == expected def test_parametrized_fixture_teardown_order(self, testdir): testdir.makepyfile(""" @@ -1738,35 +1766,100 @@ class TestFixtureMarker: """) assert "error" not in result.stdout.str() + def test_fixture_finalizer(self, testdir): + testdir.makeconftest(""" + import pytest + import sys + + @pytest.fixture + def browser(request): + + def finalize(): + sys.stdout.write('Finalized') + request.addfinalizer(finalize) + return {} + """) + b = testdir.mkdir("subdir") + b.join("test_overriden_fixture_finalizer.py").write(dedent(""" + import pytest + @pytest.fixture + def browser(browser): + browser['visited'] = True + return browser + + def test_browser(browser): + assert browser['visited'] is True + """)) + reprec = testdir.runpytest("-s") + for test in ['test_browser']: + reprec.stdout.fnmatch_lines('*Finalized*') + + def test_class_scope_with_normal_tests(self, testdir): + testpath = testdir.makepyfile(""" + import pytest + + class Box: + value = 0 + + @pytest.fixture(scope='class') + def a(request): + Box.value += 1 + return Box.value + + def test_a(a): + assert a == 1 + + class Test1: + def test_b(self, a): + assert a == 2 + + class Test2: + def test_c(self, a): + assert a == 3""") + reprec = testdir.inline_run(testpath) + for test in ['test_a', 'test_b', 'test_c']: + assert reprec.matchreport(test).passed + + def test_request_is_clean(self, testdir): + testdir.makepyfile(""" + import pytest + l = [] + @pytest.fixture(params=[1, 2]) + def fix(request): + request.addfinalizer(lambda: l.append(request.param)) + def test_fix(fix): + pass + """) + reprec = testdir.inline_run("-s") + l = reprec.getcalls("pytest_runtest_call")[0].item.module.l + assert l == [1,2] + def test_parametrize_separated_lifecycle(self, testdir): testdir.makepyfile(""" import pytest + l = [] @pytest.fixture(scope="module", params=[1, 2]) def arg(request): - request.config.l = l # to access from outer x = request.param request.addfinalizer(lambda: l.append("fin%s" % x)) return request.param - - l = [] def test_1(arg): l.append(arg) def test_2(arg): l.append(arg) """) - reprec = testdir.inline_run("-v") + reprec = testdir.inline_run("-vs") reprec.assertoutcome(passed=4) - l = reprec.getcalls("pytest_configure")[0].config.l + l = reprec.getcalls("pytest_runtest_call")[0].item.module.l import pprint pprint.pprint(l) - assert len(l) == 6 + #assert len(l) == 6 assert l[0] == l[1] == 1 assert l[2] == "fin1" assert l[3] == l[4] == 2 assert l[5] == "fin2" - def test_parametrize_function_scoped_finalizers_called(self, testdir): testdir.makepyfile(""" import pytest @@ -1789,6 +1882,69 @@ class TestFixtureMarker: reprec = testdir.inline_run("-v") reprec.assertoutcome(passed=5) + + @pytest.mark.issue246 + @pytest.mark.parametrize("scope", ["session", "function", "module"]) + def test_finalizer_order_on_parametrization(self, scope, testdir): + testdir.makepyfile(""" + import pytest + l = [] + + @pytest.fixture(scope=%(scope)r, params=["1"]) + def fix1(request): + return request.param + + @pytest.fixture(scope=%(scope)r) + def fix2(request, base): + def cleanup_fix2(): + assert not l, "base should not have been finalized" + request.addfinalizer(cleanup_fix2) + + @pytest.fixture(scope=%(scope)r) + def base(request, fix1): + def cleanup_base(): + l.append("fin_base") + print ("finalizing base") + request.addfinalizer(cleanup_base) + + def test_begin(): + pass + def test_baz(base, fix2): + pass + def test_other(): + pass + """ % {"scope": scope}) + reprec = testdir.inline_run("-lvs") + reprec.assertoutcome(passed=3) + + @pytest.mark.issue396 + def test_class_scope_parametrization_ordering(self, testdir): + testdir.makepyfile(""" + import pytest + l = [] + @pytest.fixture(params=["John", "Doe"], scope="class") + def human(request): + request.addfinalizer(lambda: l.append("fin %s" % request.param)) + return request.param + + class TestGreetings: + def test_hello(self, human): + l.append("test_hello") + + class TestMetrics: + def test_name(self, human): + l.append("test_name") + + def test_population(self, human): + l.append("test_population") + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=6) + l = reprec.getcalls("pytest_runtest_call")[0].item.module.l + assert l == ["test_hello", "fin John", "test_hello", "fin Doe", + "test_name", "test_population", "fin John", + "test_name", "test_population", "fin Doe"] + def test_parametrize_setup_function(self, testdir): testdir.makepyfile(""" import pytest @@ -1833,6 +1989,40 @@ class TestFixtureMarker: reprec = testdir.inline_run() reprec.assertoutcome(passed=1) + def test_params_and_ids(self, testdir): + testdir.makepyfile(""" + import pytest + + @pytest.fixture(params=[object(), object()], + ids=['alpha', 'beta']) + def fix(request): + return request.param + + def test_foo(fix): + assert 1 + """) + res = testdir.runpytest('-v') + res.stdout.fnmatch_lines([ + '*test_foo*alpha*', + '*test_foo*beta*']) + + def test_params_and_ids_yieldfixture(self, testdir): + testdir.makepyfile(""" + import pytest + + @pytest.yield_fixture(params=[object(), object()], + ids=['alpha', 'beta']) + def fix(request): + yield request.param + + def test_foo(fix): + assert 1 + """) + res = testdir.runpytest('-v') + res.stdout.fnmatch_lines([ + '*test_foo*alpha*', + '*test_foo*beta*']) + class TestRequestScopeAccess: pytestmark = pytest.mark.parametrize(("scope", "ok", "error"),[ @@ -1980,7 +2170,7 @@ class TestContextManagerFixtureFuncs: def test_simple(self, testdir): testdir.makepyfile(""" import pytest - @pytest.fixture + @pytest.yield_fixture def arg1(): print ("setup") yield 1 @@ -1993,18 +2183,18 @@ class TestContextManagerFixtureFuncs: """) result = testdir.runpytest("-s") result.stdout.fnmatch_lines(""" - setup - test1 1 - teardown - setup - test2 1 - teardown + *setup* + *test1 1* + *teardown* + *setup* + *test2 1* + *teardown* """) def test_scoped(self, testdir): testdir.makepyfile(""" import pytest - @pytest.fixture(scope="module") + @pytest.yield_fixture(scope="module") def arg1(): print ("setup") yield 1 @@ -2016,16 +2206,16 @@ class TestContextManagerFixtureFuncs: """) result = testdir.runpytest("-s") result.stdout.fnmatch_lines(""" - setup - test1 1 - test2 1 - teardown + *setup* + *test1 1* + *test2 1* + *teardown* """) def test_setup_exception(self, testdir): testdir.makepyfile(""" import pytest - @pytest.fixture(scope="module") + @pytest.yield_fixture(scope="module") def arg1(): pytest.fail("setup") yield 1 @@ -2041,7 +2231,7 @@ class TestContextManagerFixtureFuncs: def test_teardown_exception(self, testdir): testdir.makepyfile(""" import pytest - @pytest.fixture(scope="module") + @pytest.yield_fixture(scope="module") def arg1(): yield 1 pytest.fail("teardown") @@ -2054,11 +2244,10 @@ class TestContextManagerFixtureFuncs: *1 passed*1 error* """) - def test_yields_more_than_one(self, testdir): testdir.makepyfile(""" import pytest - @pytest.fixture(scope="module") + @pytest.yield_fixture(scope="module") def arg1(): yield 1 yield 2 @@ -2072,3 +2261,34 @@ class TestContextManagerFixtureFuncs: """) + def test_no_yield(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.yield_fixture(scope="module") + def arg1(): + return 1 + def test_1(arg1): + pass + """) + result = testdir.runpytest("-s") + result.stdout.fnmatch_lines(""" + *yield_fixture*requires*yield* + *yield_fixture* + *def arg1* + """) + + def test_yield_not_allowed_in_non_yield(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.fixture(scope="module") + def arg1(): + yield 1 + def test_1(arg1): + pass + """) + result = testdir.runpytest("-s") + result.stdout.fnmatch_lines(""" + *fixture*cannot use*yield* + *def arg1* + """) + diff --git a/testing/python/integration.py b/testing/python/integration.py index 1915c763d..6fbf44572 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -1,5 +1,6 @@ -import pytest, py, sys +import pytest from _pytest import runner +from _pytest import python class TestOEJSKITSpecials: def test_funcarg_non_pycollectobj(self, testdir): # rough jstests usage @@ -55,6 +56,20 @@ class TestOEJSKITSpecials: assert not clscol.funcargs +def test_wrapped_getfslineno(): + def func(): + pass + def wrap(f): + func.__wrapped__ = f + func.patchings = ["qwe"] + return func + @wrap + def wrapped_func(x, y, z): + pass + fs, lineno = python.getfslineno(wrapped_func) + fs2, lineno2 = python.getfslineno(wrap) + assert lineno > lineno2, "getfslineno does not unwrap correctly" + class TestMockDecoration: def test_wrapped_getfuncargnames(self): from _pytest.python import getfuncargnames @@ -118,6 +133,32 @@ class TestMockDecoration: """) reprec = testdir.inline_run() reprec.assertoutcome(passed=2) + calls = reprec.getcalls("pytest_runtest_logreport") + funcnames = [call.report.location[2] for call in calls + if call.report.when == "call"] + assert funcnames == ["T.test_hello", "test_someting"] + + def test_mock_sorting(self, testdir): + pytest.importorskip("mock", "1.0.1") + testdir.makepyfile(""" + import os + import mock + + @mock.patch("os.path.abspath") + def test_one(abspath): + pass + @mock.patch("os.path.abspath") + def test_two(abspath): + pass + @mock.patch("os.path.abspath") + def test_three(abspath): + pass + """) + reprec = testdir.inline_run() + calls = reprec.getreports("pytest_runtest_logreport") + calls = [x for x in calls if x.when == "call"] + names = [x.nodeid.split("::")[-1] for x in calls] + assert names == ["test_one", "test_two", "test_three"] class TestReRunTests: @@ -150,3 +191,7 @@ class TestReRunTests: result.stdout.fnmatch_lines(""" *2 passed* """) + +def test_pytestconfig_is_session_scoped(): + from _pytest.python import pytestconfig + assert pytestconfig._pytestfixturefunction.scope == "session" diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 464423e39..464c9e937 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1,7 +1,6 @@ -import pytest, py, sys +import pytest, py from _pytest import python as funcargs -from _pytest.python import FixtureLookupError class TestMetafunc: def Metafunc(self, func): @@ -194,8 +193,8 @@ class TestMetafunc: metafunc.parametrize('y', [2]) def pytest_funcarg__x(request): return request.param * 10 - def pytest_funcarg__y(request): - return request.param + #def pytest_funcarg__y(request): + # return request.param def test_simple(x,y): assert x in (10,20) @@ -593,6 +592,8 @@ class TestMetafuncFunctional: def test_it(foo): pass + def test_it2(foo): + pass """) reprec = testdir.inline_run("--collect-only") assert not reprec.getcalls("pytest_internalerror") @@ -629,6 +630,22 @@ class TestMetafuncFunctional: "*3 passed*" ]) + def test_generate_same_function_names_issue403(self, testdir): + testdir.makepyfile(""" + import pytest + + def make_tests(): + @pytest.mark.parametrize("x", range(2)) + def test_foo(x): + pass + return test_foo + + test_x = make_tests() + test_y = make_tests() + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=4) + class TestMarkersWithParametrization: pytestmark = pytest.mark.issue308 @@ -815,7 +832,7 @@ class TestMarkersWithParametrization: reprec.assertoutcome(passed=2, skipped=2) - @pytest.mark.xfail(reason="issue 290") + @pytest.mark.issue290 def test_parametrize_ID_generation_string_int_works(self, testdir): testdir.makepyfile(""" import pytest diff --git a/testing/python/raises.py b/testing/python/raises.py index ab4ee19eb..5101fecf0 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -29,7 +29,7 @@ class TestRaises: def test_raises_flip_builtin_AssertionError(self): # we replace AssertionError on python level # however c code might still raise the builtin one - from _pytest.assertion.util import BuiltinAssertionError + from _pytest.assertion.util import BuiltinAssertionError # noqa pytest.raises(AssertionError,""" raise BuiltinAssertionError """) diff --git a/testing/test_argcomplete.py b/testing/test_argcomplete.py index 12b96993b..9a218a401 100644 --- a/testing/test_argcomplete.py +++ b/testing/test_argcomplete.py @@ -72,6 +72,7 @@ class FilesCompleter(object): # the following barfs with a syntax error on py2.5 # @pytest.mark.skipif("sys.version_info < (2,6)") class TestArgComplete: + @pytest.mark.skipif("sys.platform in ('win32', 'darwin')") @pytest.mark.skipif("sys.version_info < (2,6)") def test_compare_with_compgen(self): from _pytest._argcomplete import FastFilesCompleter @@ -80,6 +81,7 @@ class TestArgComplete: for x in '/ /d /data qqq'.split(): assert equal_with_bash(x, ffc, fc, out=py.std.sys.stdout) + @pytest.mark.skipif("sys.platform in ('win32', 'darwin')") @pytest.mark.skipif("sys.version_info < (2,6)") def test_remove_dir_prefix(self): """this is not compatible with compgen but it is with bash itself: diff --git a/testing/test_assertinterpret.py b/testing/test_assertinterpret.py index bc7c55616..eb9cc57dc 100644 --- a/testing/test_assertinterpret.py +++ b/testing/test_assertinterpret.py @@ -28,7 +28,7 @@ def test_assert_with_explicit_message(): assert e.msg == 'hello' def test_assert_within_finally(): - excinfo = py.test.raises(ZeroDivisionError, """ + excinfo = pytest.raises(ZeroDivisionError, """ try: 1/0 finally: @@ -79,7 +79,7 @@ def test_is(): assert s.startswith("assert 1 is 2") -@py.test.mark.skipif("sys.version_info < (2,6)") +@pytest.mark.skipif("sys.version_info < (2,6)") def test_attrib(): class Foo(object): b = 1 @@ -91,7 +91,7 @@ def test_attrib(): s = str(e) assert s.startswith("assert 1 == 2") -@py.test.mark.skipif("sys.version_info < (2,6)") +@pytest.mark.skipif("sys.version_info < (2,6)") def test_attrib_inst(): class Foo(object): b = 1 @@ -168,7 +168,7 @@ def test_assert_with_brokenrepr_arg(): def __repr__(self): 0 / 0 e = AssertionError(BrokenRepr()) if e.msg.find("broken __repr__") == -1: - py.test.fail("broken __repr__ not handle correctly") + pytest.fail("broken __repr__ not handle correctly") def test_multiple_statements_per_line(): try: @@ -244,7 +244,7 @@ class TestView: assert codelines == ["4 + 5", "getitem('', 'join')", "setattr('x', 'y', 3)", "12 - 1"] -@py.test.mark.skipif("sys.version_info < (2,6)") +@pytest.mark.skipif("sys.version_info < (2,6)") def test_assert_customizable_reprcompare(monkeypatch): monkeypatch.setattr(util, '_reprcompare', lambda *args: 'hello') try: @@ -323,7 +323,7 @@ def test_assert_raises_in_nonzero_of_object_pytest_issue10(): s = str(e) assert " < 0" in s -@py.test.mark.skipif("sys.version_info >= (2,6)") +@pytest.mark.skipif("sys.version_info >= (2,6)") def test_oldinterpret_importation(): # we had a cyclic import there # requires pytest on sys.path diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 9a6ba127f..fbe44eb73 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1,8 +1,9 @@ +# -*- coding: utf-8 -*- import sys import py, pytest import _pytest.assertion as plugin -from _pytest.assertion import reinterpret, util +from _pytest.assertion import reinterpret needsnewassert = pytest.mark.skipif("sys.version_info < (2,6)") @@ -176,6 +177,15 @@ class TestAssert_reprcompare: expl = ' '.join(callequal('foo', 'bar')) assert 'raised in repr()' not in expl + def test_unicode(self): + left = py.builtin._totext('£€', 'utf-8') + right = py.builtin._totext('£', 'utf-8') + expl = callequal(left, right) + assert expl[0] == py.builtin._totext("'£€' == '£'", 'utf-8') + assert expl[1] == py.builtin._totext('- £€', 'utf-8') + assert expl[2] == py.builtin._totext('+ £', 'utf-8') + + def test_python25_compile_issue257(testdir): testdir.makepyfile(""" def test_rewritten(): @@ -353,7 +363,7 @@ def test_traceback_failure(testdir): @pytest.mark.skipif("sys.version_info < (2,5) or '__pypy__' in sys.builtin_module_names or sys.platform.startswith('java')" ) def test_warn_missing(testdir): - p1 = testdir.makepyfile("") + testdir.makepyfile("") result = testdir.run(sys.executable, "-OO", "-m", "pytest", "-h") result.stderr.fnmatch_lines([ "*WARNING*assert statements are not executed*", @@ -376,3 +386,16 @@ def test_recursion_source_decode(testdir): result.stdout.fnmatch_lines(""" """) + +def test_AssertionError_message(testdir): + testdir.makepyfile(""" + def test_hello(): + x,y = 1,2 + assert 0, (x,y) + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(""" + *def test_hello* + *assert 0, (x,y)* + *AssertionError: (1, 2)* + """) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 82fe9a366..d19ebd1d4 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -107,13 +107,13 @@ class TestAssertionRewrite: assert f assert getmsg(f) == "assert False" def f(): - assert a_global + assert a_global # noqa assert getmsg(f, {"a_global" : False}) == "assert False" def f(): assert sys == 42 assert getmsg(f, {"sys" : sys}) == "assert sys == 42" def f(): - assert cls == 42 + assert cls == 42 # noqa class X(object): pass assert getmsg(f, {"cls" : X}) == "assert cls == 42" @@ -174,7 +174,7 @@ class TestAssertionRewrite: def test_short_circut_evaluation(self): def f(): - assert True or explode + assert True or explode # noqa getmsg(f, must_pass=True) def f(): x = 1 @@ -206,7 +206,6 @@ class TestAssertionRewrite: assert x + y assert getmsg(f) == "assert (1 + -1)" def f(): - x = range(10) assert not 5 % 4 assert getmsg(f) == "assert not (5 % 4)" @@ -243,12 +242,12 @@ class TestAssertionRewrite: g = 3 ns = {"x" : X} def f(): - assert not x.g + assert not x.g # noqa assert getmsg(f, ns) == """assert not 3 + where 3 = x.g""" def f(): - x.a = False - assert x.a + x.a = False # noqa + assert x.a # noqa assert getmsg(f, ns) == """assert x.a""" def test_comparisons(self): @@ -412,6 +411,13 @@ def test_rewritten(): testdir.tmpdir.join("test_newlines.py").write(b, "wb") assert testdir.runpytest().ret == 0 + @pytest.mark.skipif(sys.version_info[0] == 2, + reason='packages without __init__.py not supported on python 2') + def test_package_without__init__py(self, testdir): + pkg = testdir.mkdir('a_package_without_init_py') + mod = pkg.join('module.py').ensure() + testdir.makepyfile("import a_package_without_init_py.module") + assert testdir.runpytest().ret == 0 class TestAssertionRewriteHookDetails(object): def test_loader_is_package_false_for_module(self, testdir): @@ -435,21 +441,46 @@ class TestAssertionRewriteHookDetails(object): def test_missing(): assert not __loader__.is_package('pytest_not_there') """) - pkg = testdir.mkpydir('fun') + testdir.mkpydir('fun') result = testdir.runpytest() result.stdout.fnmatch_lines([ '* 3 passed*', ]) - @pytest.mark.skipif("sys.version_info[0] >= 3") + @pytest.mark.xfail("hasattr(sys, 'pypy_translation_info')") def test_assume_ascii(self, testdir): - content = "u'\xe2\x99\xa5'" + content = "u'\xe2\x99\xa5\x01\xfe'" testdir.tmpdir.join("test_encoding.py").write(content, "wb") res = testdir.runpytest() assert res.ret != 0 assert "SyntaxError: Non-ASCII character" in res.stdout.str() + @pytest.mark.skipif("sys.version_info[0] >= 3") + def test_detect_coding_cookie(self, testdir): + testdir.tmpdir.join("test_cookie.py").write("""# -*- coding: utf-8 -*- +u"St\xc3\xa4d" +def test_rewritten(): + assert "@py_builtins" in globals()""", "wb") + assert testdir.runpytest().ret == 0 + + @pytest.mark.skipif("sys.version_info[0] >= 3") + def test_detect_coding_cookie_second_line(self, testdir): + testdir.tmpdir.join("test_cookie.py").write("""#!/usr/bin/env python +# -*- coding: utf-8 -*- +u"St\xc3\xa4d" +def test_rewritten(): + assert "@py_builtins" in globals()""", "wb") + assert testdir.runpytest().ret == 0 + + @pytest.mark.skipif("sys.version_info[0] >= 3") + def test_detect_coding_cookie_crlf(self, testdir): + testdir.tmpdir.join("test_cookie.py").write("""#!/usr/bin/env python +# -*- coding: utf-8 -*- +u"St\xc3\xa4d" +def test_rewritten(): + assert "@py_builtins" in globals()""".replace("\n", "\r\n"), "wb") + assert testdir.runpytest().ret == 0 def test_write_pyc(self, testdir, tmpdir, monkeypatch): from _pytest.assertion.rewrite import _write_pyc @@ -469,3 +500,35 @@ class TestAssertionRewriteHookDetails(object): raise e monkeypatch.setattr(b, "open", open) assert not _write_pyc(state, [1], source_path, pycpath) + + def test_resources_provider_for_loader(self, testdir): + """ + Attempts to load resources from a package should succeed normally, + even when the AssertionRewriteHook is used to load the modules. + + See #366 for details. + """ + pytest.importorskip("pkg_resources") + + testdir.mkpydir('testpkg') + contents = { + 'testpkg/test_pkg': """ + import pkg_resources + + import pytest + from _pytest.assertion.rewrite import AssertionRewritingHook + + def test_load_resource(): + assert isinstance(__loader__, AssertionRewritingHook) + res = pkg_resources.resource_string(__name__, 'resource.txt') + res = res.decode('ascii') + assert res == 'Load me please.' + """, + } + testdir.makepyfile(**contents) + testdir.maketxtfile(**{'testpkg/resource': "Load me please."}) + + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + '* 1 passed*', + ]) diff --git a/testing/test_capture.py b/testing/test_capture.py index 8f40d1332..cdc8f5295 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1,8 +1,48 @@ -import pytest, py, os, sys +# note: py.io capture tests where copied from +# pylib 1.4.20.dev2 (rev 13d9af95547e) +from __future__ import with_statement +import os +import sys +import py +import pytest + +from _pytest import capture from _pytest.capture import CaptureManager +from py.builtin import print_ needsosdup = pytest.mark.xfail("not hasattr(os, 'dup')") +if sys.version_info >= (3, 0): + def tobytes(obj): + if isinstance(obj, str): + obj = obj.encode('UTF-8') + assert isinstance(obj, bytes) + return obj + + def totext(obj): + if isinstance(obj, bytes): + obj = str(obj, 'UTF-8') + assert isinstance(obj, str) + return obj +else: + def tobytes(obj): + if isinstance(obj, unicode): + obj = obj.encode('UTF-8') + assert isinstance(obj, str) + return obj + + def totext(obj): + if isinstance(obj, str): + obj = unicode(obj, 'UTF-8') + assert isinstance(obj, unicode) + return obj + + +def oswritebytes(fd, obj): + os.write(fd, tobytes(obj)) + + + class TestCaptureManager: def test_getmethod_default_no_fd(self, testdir, monkeypatch): config = testdir.parseconfig(testdir.tmpdir) @@ -32,9 +72,9 @@ class TestCaptureManager: assert capman._getmethod(config, sub.join("test_hello.py")) == mode @needsosdup - @pytest.mark.multi(method=['no', 'fd', 'sys']) + @pytest.mark.parametrize("method", ['no', 'fd', 'sys']) def test_capturing_basic_api(self, method): - capouter = py.io.StdCaptureFD() + capouter = capture.StdCaptureFD() old = sys.stdout, sys.stderr, sys.stdin try: capman = CaptureManager() @@ -58,7 +98,7 @@ class TestCaptureManager: @needsosdup def test_juggle_capturings(self, testdir): - capouter = py.io.StdCaptureFD() + capouter = capture.StdCaptureFD() try: #config = testdir.parseconfig(testdir.tmpdir) capman = CaptureManager() @@ -80,10 +120,11 @@ class TestCaptureManager: finally: capouter.reset() + @pytest.mark.xfail("hasattr(sys, 'pypy_version_info')") -@pytest.mark.multi(method=['fd', 'sys']) +@pytest.mark.parametrize("method", ['fd', 'sys']) def test_capturing_unicode(testdir, method): - if sys.version_info >= (3,0): + if sys.version_info >= (3, 0): obj = "'b\u00f6y'" else: obj = "u'\u00f6y'" @@ -100,7 +141,8 @@ def test_capturing_unicode(testdir, method): "*1 passed*" ]) -@pytest.mark.multi(method=['fd', 'sys']) + +@pytest.mark.parametrize("method", ['fd', 'sys']) def test_capturing_bytes_in_utf8_encoding(testdir, method): testdir.makepyfile(""" def test_unicode(): @@ -111,6 +153,7 @@ def test_capturing_bytes_in_utf8_encoding(testdir, method): "*1 passed*" ]) + def test_collect_capturing(testdir): p = testdir.makepyfile(""" print ("collect %s failure" % 13) @@ -122,6 +165,7 @@ def test_collect_capturing(testdir): "*collect 13 failure*", ]) + class TestPerTestCapturing: def test_capture_and_fixtures(self, testdir): p = testdir.makepyfile(""" @@ -169,7 +213,6 @@ class TestPerTestCapturing: "in teardown*", ]) - def test_no_carry_over(self, testdir): p = testdir.makepyfile(""" def test_func1(): @@ -183,7 +226,6 @@ class TestPerTestCapturing: assert "in func1" not in s assert "in func2" in s - def test_teardown_capturing(self, testdir): p = testdir.makepyfile(""" def setup_function(function): @@ -244,13 +286,14 @@ class TestPerTestCapturing: "2", ]) + class TestLoggingInteraction: def test_logging_stream_ownership(self, testdir): p = testdir.makepyfile(""" def test_logging(): import logging import pytest - stream = py.io.TextIO() + stream = capture.TextIO() logging.basicConfig(stream=stream) stream.close() # to free memory/release resources """) @@ -320,7 +363,8 @@ class TestLoggingInteraction: logging.warn("hello432") assert 0 """) - result = testdir.runpytest(p, "--traceconfig", + result = testdir.runpytest( + p, "--traceconfig", "-p", "no:capturelog") assert result.ret != 0 result.stdout.fnmatch_lines([ @@ -437,7 +481,6 @@ class TestCaptureFixture: ]) assert result.ret == 2 - @pytest.mark.xfail("sys.version_info < (2,7)") @pytest.mark.issue14 def test_capture_and_logging(self, testdir): p = testdir.makepyfile(""" @@ -462,6 +505,7 @@ def test_setup_failure_does_not_kill_capturing(testdir): "*1 error*" ]) + def test_fdfuncarg_skips_on_no_osdup(testdir): testdir.makepyfile(""" import os @@ -475,6 +519,7 @@ def test_fdfuncarg_skips_on_no_osdup(testdir): "*1 skipped*" ]) + def test_capture_conftest_runtest_setup(testdir): testdir.makeconftest(""" def pytest_runtest_setup(): @@ -484,3 +529,534 @@ def test_capture_conftest_runtest_setup(testdir): result = testdir.runpytest() assert result.ret == 0 assert 'hello19' not in result.stdout.str() + + +def test_capture_early_option_parsing(testdir): + testdir.makeconftest(""" + def pytest_runtest_setup(): + print ("hello19") + """) + testdir.makepyfile("def test_func(): pass") + result = testdir.runpytest("-vs") + assert result.ret == 0 + assert 'hello19' in result.stdout.str() + + +@pytest.mark.xfail(sys.version_info >= (3, 0), reason='encoding issues') +def test_capture_binary_output(testdir): + testdir.makepyfile(r""" + import pytest + + def test_a(): + import sys + import subprocess + subprocess.call([sys.executable, __file__]) + + @pytest.mark.skip + def test_foo(): + import os;os.write(1, b'\xc3') + + if __name__ == '__main__': + test_foo() + """) + result = testdir.runpytest('--assert=plain') + result.stdout.fnmatch_lines([ + '*2 passed*', + ]) + + +class TestTextIO: + def test_text(self): + f = capture.TextIO() + f.write("hello") + s = f.getvalue() + assert s == "hello" + f.close() + + def test_unicode_and_str_mixture(self): + f = capture.TextIO() + if sys.version_info >= (3, 0): + f.write("\u00f6") + pytest.raises(TypeError, "f.write(bytes('hello', 'UTF-8'))") + else: + f.write(unicode("\u00f6", 'UTF-8')) + f.write("hello") # bytes + s = f.getvalue() + f.close() + assert isinstance(s, unicode) + + +def test_bytes_io(): + f = capture.BytesIO() + f.write(tobytes("hello")) + pytest.raises(TypeError, "f.write(totext('hello'))") + s = f.getvalue() + assert s == tobytes("hello") + + +def test_dontreadfrominput(): + from _pytest.capture import DontReadFromInput + f = DontReadFromInput() + assert not f.isatty() + pytest.raises(IOError, f.read) + pytest.raises(IOError, f.readlines) + pytest.raises(IOError, iter, f) + pytest.raises(ValueError, f.fileno) + f.close() # just for completeness + + +def pytest_funcarg__tmpfile(request): + testdir = request.getfuncargvalue("testdir") + f = testdir.makepyfile("").open('wb+') + request.addfinalizer(f.close) + return f + + +@needsosdup +def test_dupfile(tmpfile): + flist = [] + for i in range(5): + nf = capture.dupfile(tmpfile, encoding="utf-8") + assert nf != tmpfile + assert nf.fileno() != tmpfile.fileno() + assert nf not in flist + print_(i, end="", file=nf) + flist.append(nf) + for i in range(5): + f = flist[i] + f.close() + tmpfile.seek(0) + s = tmpfile.read() + assert "01234" in repr(s) + tmpfile.close() + + +def test_dupfile_no_mode(): + """ + dupfile should trap an AttributeError and return f if no mode is supplied. + """ + class SomeFileWrapper(object): + "An object with a fileno method but no mode attribute" + def fileno(self): + return 1 + tmpfile = SomeFileWrapper() + assert capture.dupfile(tmpfile) is tmpfile + with pytest.raises(AttributeError): + capture.dupfile(tmpfile, raising=True) + + +def lsof_check(func): + pid = os.getpid() + try: + out = py.process.cmdexec("lsof -p %d" % pid) + except py.process.cmdexec.Error: + pytest.skip("could not run 'lsof'") + func() + out2 = py.process.cmdexec("lsof -p %d" % pid) + len1 = len([x for x in out.split("\n") if "REG" in x]) + len2 = len([x for x in out2.split("\n") if "REG" in x]) + assert len2 < len1 + 3, out2 + + +class TestFDCapture: + pytestmark = needsosdup + + def test_not_now(self, tmpfile): + fd = tmpfile.fileno() + cap = capture.FDCapture(fd, now=False) + data = tobytes("hello") + os.write(fd, data) + f = cap.done() + s = f.read() + assert not s + cap = capture.FDCapture(fd, now=False) + cap.start() + os.write(fd, data) + f = cap.done() + s = f.read() + assert s == "hello" + + def test_simple(self, tmpfile): + fd = tmpfile.fileno() + cap = capture.FDCapture(fd) + data = tobytes("hello") + os.write(fd, data) + f = cap.done() + s = f.read() + assert s == "hello" + f.close() + + def test_simple_many(self, tmpfile): + for i in range(10): + self.test_simple(tmpfile) + + def test_simple_many_check_open_files(self, tmpfile): + lsof_check(lambda: self.test_simple_many(tmpfile)) + + def test_simple_fail_second_start(self, tmpfile): + fd = tmpfile.fileno() + cap = capture.FDCapture(fd) + f = cap.done() + pytest.raises(ValueError, cap.start) + f.close() + + def test_stderr(self): + cap = capture.FDCapture(2, patchsys=True) + print_("hello", file=sys.stderr) + f = cap.done() + s = f.read() + assert s == "hello\n" + + def test_stdin(self, tmpfile): + tmpfile.write(tobytes("3")) + tmpfile.seek(0) + cap = capture.FDCapture(0, tmpfile=tmpfile) + # check with os.read() directly instead of raw_input(), because + # sys.stdin itself may be redirected (as pytest now does by default) + x = os.read(0, 100).strip() + cap.done() + assert x == tobytes("3") + + def test_writeorg(self, tmpfile): + data1, data2 = tobytes("foo"), tobytes("bar") + try: + cap = capture.FDCapture(tmpfile.fileno()) + tmpfile.write(data1) + cap.writeorg(data2) + finally: + tmpfile.close() + f = cap.done() + scap = f.read() + assert scap == totext(data1) + stmp = open(tmpfile.name, 'rb').read() + assert stmp == data2 + + +class TestStdCapture: + def getcapture(self, **kw): + return capture.StdCapture(**kw) + + def test_capturing_done_simple(self): + cap = self.getcapture() + sys.stdout.write("hello") + sys.stderr.write("world") + outfile, errfile = cap.done() + s = outfile.read() + assert s == "hello" + s = errfile.read() + assert s == "world" + + def test_capturing_reset_simple(self): + cap = self.getcapture() + print("hello world") + sys.stderr.write("hello error\n") + out, err = cap.reset() + assert out == "hello world\n" + assert err == "hello error\n" + + def test_capturing_readouterr(self): + cap = self.getcapture() + try: + print ("hello world") + sys.stderr.write("hello error\n") + out, err = cap.readouterr() + assert out == "hello world\n" + assert err == "hello error\n" + sys.stderr.write("error2") + finally: + out, err = cap.reset() + assert err == "error2" + + def test_capturing_readouterr_unicode(self): + cap = self.getcapture() + try: + print ("hx\xc4\x85\xc4\x87") + out, err = cap.readouterr() + finally: + cap.reset() + assert out == py.builtin._totext("hx\xc4\x85\xc4\x87\n", "utf8") + + @pytest.mark.skipif('sys.version_info >= (3,)', + reason='text output different for bytes on python3') + def test_capturing_readouterr_decode_error_handling(self): + cap = self.getcapture() + # triggered a internal error in pytest + print('\xa6') + out, err = cap.readouterr() + assert out == py.builtin._totext('\ufffd\n', 'unicode-escape') + + def test_capturing_mixed(self): + cap = self.getcapture(mixed=True) + sys.stdout.write("hello ") + sys.stderr.write("world") + sys.stdout.write(".") + out, err = cap.reset() + assert out.strip() == "hello world." + assert not err + + def test_reset_twice_error(self): + cap = self.getcapture() + print ("hello") + out, err = cap.reset() + pytest.raises(ValueError, cap.reset) + assert out == "hello\n" + assert not err + + def test_capturing_modify_sysouterr_in_between(self): + oldout = sys.stdout + olderr = sys.stderr + cap = self.getcapture() + sys.stdout.write("hello") + sys.stderr.write("world") + sys.stdout = capture.TextIO() + sys.stderr = capture.TextIO() + print ("not seen") + sys.stderr.write("not seen\n") + out, err = cap.reset() + assert out == "hello" + assert err == "world" + assert sys.stdout == oldout + assert sys.stderr == olderr + + def test_capturing_error_recursive(self): + cap1 = self.getcapture() + print ("cap1") + cap2 = self.getcapture() + print ("cap2") + out2, err2 = cap2.reset() + out1, err1 = cap1.reset() + assert out1 == "cap1\n" + assert out2 == "cap2\n" + + def test_just_out_capture(self): + cap = self.getcapture(out=True, err=False) + sys.stdout.write("hello") + sys.stderr.write("world") + out, err = cap.reset() + assert out == "hello" + assert not err + + def test_just_err_capture(self): + cap = self.getcapture(out=False, err=True) + sys.stdout.write("hello") + sys.stderr.write("world") + out, err = cap.reset() + assert err == "world" + assert not out + + def test_stdin_restored(self): + old = sys.stdin + cap = self.getcapture(in_=True) + newstdin = sys.stdin + out, err = cap.reset() + assert newstdin != sys.stdin + assert sys.stdin is old + + def test_stdin_nulled_by_default(self): + print ("XXX this test may well hang instead of crashing") + print ("XXX which indicates an error in the underlying capturing") + print ("XXX mechanisms") + cap = self.getcapture() + pytest.raises(IOError, "sys.stdin.read()") + out, err = cap.reset() + + def test_suspend_resume(self): + cap = self.getcapture(out=True, err=False, in_=False) + try: + print ("hello") + sys.stderr.write("error\n") + out, err = cap.suspend() + assert out == "hello\n" + assert not err + print ("in between") + sys.stderr.write("in between\n") + cap.resume() + print ("after") + sys.stderr.write("error_after\n") + finally: + out, err = cap.reset() + assert out == "after\n" + assert not err + + +class TestStdCaptureNotNow(TestStdCapture): + def getcapture(self, **kw): + kw['now'] = False + cap = capture.StdCapture(**kw) + cap.startall() + return cap + + +class TestStdCaptureFD(TestStdCapture): + pytestmark = needsosdup + + def getcapture(self, **kw): + return capture.StdCaptureFD(**kw) + + def test_intermingling(self): + cap = self.getcapture() + oswritebytes(1, "1") + sys.stdout.write(str(2)) + sys.stdout.flush() + oswritebytes(1, "3") + oswritebytes(2, "a") + sys.stderr.write("b") + sys.stderr.flush() + oswritebytes(2, "c") + out, err = cap.reset() + assert out == "123" + assert err == "abc" + + def test_callcapture(self): + def func(x, y): + print (x) + sys.stderr.write(str(y)) + return 42 + + res, out, err = capture.StdCaptureFD.call(func, 3, y=4) + assert res == 42 + assert out.startswith("3") + assert err.startswith("4") + + def test_many(self, capfd): + def f(): + for i in range(10): + cap = capture.StdCaptureFD() + cap.reset() + lsof_check(f) + + +class TestStdCaptureFDNotNow(TestStdCaptureFD): + pytestmark = needsosdup + + def getcapture(self, **kw): + kw['now'] = False + cap = capture.StdCaptureFD(**kw) + cap.startall() + return cap + + +@needsosdup +def test_stdcapture_fd_tmpfile(tmpfile): + capfd = capture.StdCaptureFD(out=tmpfile) + try: + os.write(1, "hello".encode("ascii")) + os.write(2, "world".encode("ascii")) + outf, errf = capfd.done() + finally: + capfd.reset() + assert outf == tmpfile + + +class TestStdCaptureFDinvalidFD: + pytestmark = needsosdup + + def test_stdcapture_fd_invalid_fd(self, testdir): + testdir.makepyfile(""" + import os + from _pytest.capture import StdCaptureFD + def test_stdout(): + os.close(1) + cap = StdCaptureFD(out=True, err=False, in_=False) + cap.done() + def test_stderr(): + os.close(2) + cap = StdCaptureFD(out=False, err=True, in_=False) + cap.done() + def test_stdin(): + os.close(0) + cap = StdCaptureFD(out=False, err=False, in_=True) + cap.done() + """) + result = testdir.runpytest("--capture=fd") + assert result.ret == 0 + assert result.parseoutcomes()['passed'] == 3 + + +def test_capture_not_started_but_reset(): + capsys = capture.StdCapture(now=False) + capsys.done() + capsys.done() + capsys.reset() + + +@needsosdup +def test_capture_no_sys(): + capsys = capture.StdCapture() + try: + cap = capture.StdCaptureFD(patchsys=False) + sys.stdout.write("hello") + sys.stderr.write("world") + oswritebytes(1, "1") + oswritebytes(2, "2") + out, err = cap.reset() + assert out == "1" + assert err == "2" + finally: + capsys.reset() + + +@needsosdup +def test_callcapture_nofd(): + def func(x, y): + oswritebytes(1, "hello") + oswritebytes(2, "hello") + print (x) + sys.stderr.write(str(y)) + return 42 + + capfd = capture.StdCaptureFD(patchsys=False) + try: + res, out, err = capture.StdCapture.call(func, 3, y=4) + finally: + capfd.reset() + assert res == 42 + assert out.startswith("3") + assert err.startswith("4") + + +@needsosdup +@pytest.mark.parametrize('use', [True, False]) +def test_fdcapture_tmpfile_remains_the_same(tmpfile, use): + if not use: + tmpfile = True + cap = capture.StdCaptureFD(out=False, err=tmpfile, now=False) + try: + cap.startall() + capfile = cap.err.tmpfile + cap.suspend() + cap.resume() + finally: + cap.reset() + capfile2 = cap.err.tmpfile + assert capfile2 == capfile + + +@pytest.mark.parametrize('method', ['StdCapture', 'StdCaptureFD']) +def test_capturing_and_logging_fundamentals(testdir, method): + if method == "StdCaptureFD" and not hasattr(os, 'dup'): + pytest.skip("need os.dup") + # here we check a fundamental feature + p = testdir.makepyfile(""" + import sys, os + import py, logging + from _pytest import capture + cap = capture.%s(out=False, in_=False) + + logging.warn("hello1") + outerr = cap.suspend() + print ("suspend, captured %%s" %%(outerr,)) + logging.warn("hello2") + + cap.resume() + logging.warn("hello3") + + outerr = cap.suspend() + print ("suspend2, captured %%s" %% (outerr,)) + """ % (method,)) + result = testdir.runpython(p) + result.stdout.fnmatch_lines([ + "suspend, captured*hello1*", + "suspend2, captured*hello2*WARNING:root:hello3*", + ]) + assert "atexit" not in result.stderr.str() diff --git a/testing/test_collection.py b/testing/test_collection.py index f330002e9..4adf46886 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -123,7 +123,7 @@ class TestCollectPluginHookRelay: def pytest_collect_file(self, path, parent): wascalled.append(path) testdir.makefile(".abc", "xyz") - testdir.pytestmain([testdir.tmpdir], plugins=[Plugin()]) + pytest.main([testdir.tmpdir], plugins=[Plugin()]) assert len(wascalled) == 1 assert wascalled[0].ext == '.abc' @@ -134,7 +134,7 @@ class TestCollectPluginHookRelay: wascalled.append(path.basename) testdir.mkdir("hello") testdir.mkdir("world") - testdir.pytestmain(testdir.tmpdir, plugins=[Plugin()]) + pytest.main(testdir.tmpdir, plugins=[Plugin()]) assert "hello" in wascalled assert "world" in wascalled @@ -242,7 +242,7 @@ class TestCustomConftests: assert "passed" in result.stdout.str() def test_pytest_fs_collect_hooks_are_seen(self, testdir): - conf = testdir.makeconftest(""" + testdir.makeconftest(""" import pytest class MyModule(pytest.Module): pass @@ -250,8 +250,8 @@ class TestCustomConftests: if path.ext == ".py": return MyModule(path, parent) """) - sub = testdir.mkdir("sub") - p = testdir.makepyfile("def test_x(): pass") + testdir.mkdir("sub") + testdir.makepyfile("def test_x(): pass") result = testdir.runpytest("--collect-only") result.stdout.fnmatch_lines([ "*MyModule*", @@ -318,7 +318,7 @@ class TestSession: topdir = testdir.tmpdir rcol = Session(config) assert topdir == rcol.fspath - rootid = rcol.nodeid + #rootid = rcol.nodeid #root2 = rcol.perform_collect([rcol.nodeid], genitems=False)[0] #assert root2 == rcol, rootid colitems = rcol.perform_collect([rcol.nodeid], genitems=False) @@ -329,13 +329,13 @@ class TestSession: def test_collect_protocol_single_function(self, testdir): p = testdir.makepyfile("def test_func(): pass") id = "::".join([p.basename, "test_func"]) - topdir = testdir.tmpdir items, hookrec = testdir.inline_genitems(id) item, = items assert item.name == "test_func" newid = item.nodeid assert newid == id py.std.pprint.pprint(hookrec.hookrecorder.calls) + topdir = testdir.tmpdir # noqa hookrec.hookrecorder.contains([ ("pytest_collectstart", "collector.fspath == topdir"), ("pytest_make_collect_report", "collector.fspath == topdir"), @@ -436,7 +436,7 @@ class TestSession: ]) def test_serialization_byid(self, testdir): - p = testdir.makepyfile("def test_func(): pass") + testdir.makepyfile("def test_func(): pass") items, hookrec = testdir.inline_genitems() assert len(items) == 1 item, = items @@ -566,4 +566,25 @@ def test_matchnodes_two_collections_same_file(testdir): ]) +class TestNodekeywords: + def test_no_under(self, testdir): + modcol = testdir.getmodulecol(""" + def test_pass(): pass + def test_fail(): assert 0 + """) + l = list(modcol.keywords) + assert modcol.name in l + for x in l: + assert not x.startswith("_") + assert modcol.name in repr(modcol.keywords) + def test_issue345(self, testdir): + testdir.makepyfile(""" + def test_should_not_be_selected(): + assert False, 'I should not have been selected to run' + + def test___repr__(): + pass + """) + reprec = testdir.inline_run("-k repr") + reprec.assertoutcome(passed=1, failed=0) diff --git a/testing/test_config.py b/testing/test_config.py index 1e3eb6d70..f0ee1a670 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -16,7 +16,7 @@ class TestParseIni: assert config.inicfg['name'] == 'value' def test_getcfg_empty_path(self, tmpdir): - cfg = getcfg([''], ['setup.cfg']) #happens on py.test "" + getcfg([''], ['setup.cfg']) #happens on py.test "" def test_append_parse_args(self, testdir, tmpdir): tmpdir.join("setup.cfg").write(py.code.Source(""" @@ -31,7 +31,7 @@ class TestParseIni: #assert len(args) == 1 def test_tox_ini_wrong_version(self, testdir): - p = testdir.makefile('.ini', tox=""" + testdir.makefile('.ini', tox=""" [pytest] minversion=9.0 """) @@ -41,7 +41,7 @@ class TestParseIni: "*tox.ini:2*requires*9.0*actual*" ]) - @pytest.mark.multi(name="setup.cfg tox.ini pytest.ini".split()) + @pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split()) def test_ini_names(self, testdir, name): testdir.tmpdir.join(name).write(py.std.textwrap.dedent(""" [pytest] @@ -77,12 +77,12 @@ class TestParseIni: class TestConfigCmdlineParsing: def test_parsing_again_fails(self, testdir): config = testdir.parseconfig() - pytest.raises(AssertionError, "config.parse([])") + pytest.raises(AssertionError, lambda: config.parse([])) class TestConfigAPI: def test_config_trace(self, testdir): - config = testdir.Config() + config = testdir.parseconfig() l = [] config.trace.root.setwriter(l.append) config.trace("hello") @@ -113,6 +113,17 @@ class TestConfigAPI: assert config.getoption(x) == "this" pytest.raises(ValueError, "config.getoption('qweqwe')") + @pytest.mark.skipif('sys.version_info[:2] not in [(2, 6), (2, 7)]') + def test_config_getoption_unicode(self, testdir): + testdir.makeconftest(""" + from __future__ import unicode_literals + + def pytest_addoption(parser): + parser.addoption('--hello', type='string') + """) + config = testdir.parseconfig('--hello=this') + assert config.getoption('hello') == 'this' + def test_config_getvalueorskip(self, testdir): config = testdir.parseconfig() pytest.raises(pytest.skip.Exception, @@ -189,7 +200,7 @@ class TestConfigAPI: parser.addini("args", "new args", type="args") parser.addini("a2", "", "args", default="1 2 3".split()) """) - p = testdir.makeini(""" + testdir.makeini(""" [pytest] args=123 "123 hello" "this" """) @@ -206,7 +217,7 @@ class TestConfigAPI: parser.addini("xy", "", type="linelist") parser.addini("a2", "", "linelist") """) - p = testdir.makeini(""" + testdir.makeini(""" [pytest] xy= 123 345 second line @@ -223,7 +234,7 @@ class TestConfigAPI: def pytest_addoption(parser): parser.addini("xy", "", type="linelist") """) - p = testdir.makeini(""" + testdir.makeini(""" [pytest] xy= 123 """) @@ -269,7 +280,7 @@ def test_options_on_small_file_do_not_blow_up(testdir): runfiletest(opts + [path]) def test_preparse_ordering_with_setuptools(testdir, monkeypatch): - pkg_resources = py.test.importorskip("pkg_resources") + pkg_resources = pytest.importorskip("pkg_resources") def my_iter(name): assert name == "pytest11" class EntryPoint: @@ -291,7 +302,7 @@ def test_preparse_ordering_with_setuptools(testdir, monkeypatch): assert plugin.x == 42 def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch): - pkg_resources = py.test.importorskip("pkg_resources") + pkg_resources = pytest.importorskip("pkg_resources") def my_iter(name): assert name == "pytest11" class EntryPoint: @@ -320,3 +331,33 @@ def test_cmdline_processargs_simple(testdir): def test_toolongargs_issue224(testdir): result = testdir.runpytest("-m", "hello" * 500) assert result.ret == 0 + +def test_notify_exception(testdir, capfd): + config = testdir.parseconfig() + excinfo = pytest.raises(ValueError, "raise ValueError(1)") + config.notify_exception(excinfo) + out, err = capfd.readouterr() + assert "ValueError" in err + class A: + def pytest_internalerror(self, excrepr): + return True + config.pluginmanager.register(A()) + config.notify_exception(excinfo) + out, err = capfd.readouterr() + assert not err + + +def test_load_initial_conftest_last_ordering(testdir): + from _pytest.config import get_plugin_manager + pm = get_plugin_manager() + class My: + def pytest_load_initial_conftests(self): + pass + m = My() + pm.register(m) + l = pm.listattr("pytest_load_initial_conftests") + assert l[-1].__module__ == "_pytest.capture" + assert l[-2] == m.pytest_load_initial_conftests + assert l[-3].__module__ == "_pytest.config" + + diff --git a/testing/test_conftest.py b/testing/test_conftest.py index d753f5672..9b5fa94e9 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -8,7 +8,7 @@ def pytest_generate_tests(metafunc): def pytest_funcarg__basedir(request): def basedirmaker(request): - basedir = d = request.getfuncargvalue("tmpdir") + d = request.getfuncargvalue("tmpdir") d.ensure("adir/conftest.py").write("a=1 ; Directory = 3") d.ensure("adir/b/conftest.py").write("b=2 ; a = 1.5") if request.param == "inpackage": @@ -41,7 +41,7 @@ class TestConftestValueAccessGlobal: def test_immediate_initialiation_and_incremental_are_the_same(self, basedir): conftest = Conftest() - snap0 = len(conftest._path2confmods) + len(conftest._path2confmods) conftest.getconftestmodules(basedir) snap1 = len(conftest._path2confmods) #assert len(conftest._path2confmods) == snap1 + 1 @@ -57,7 +57,7 @@ class TestConftestValueAccessGlobal: def test_value_access_not_existing(self, basedir): conftest = ConftestWithSetinitial(basedir) - pytest.raises(KeyError, "conftest.rget('a')") + pytest.raises(KeyError, lambda: conftest.rget('a')) #pytest.raises(KeyError, "conftest.lget('a')") def test_value_access_by_path(self, basedir): @@ -97,7 +97,7 @@ def test_conftest_in_nonpkg_with_init(tmpdir): tmpdir.ensure("adir-1.0/b/conftest.py").write("b=2 ; a = 1.5") tmpdir.ensure("adir-1.0/b/__init__.py") tmpdir.ensure("adir-1.0/__init__.py") - conftest = ConftestWithSetinitial(tmpdir.join("adir-1.0", "b")) + ConftestWithSetinitial(tmpdir.join("adir-1.0", "b")) def test_doubledash_not_considered(testdir): conf = testdir.mkdir("--option") @@ -182,7 +182,7 @@ def test_setinitial_confcut(testdir): assert conftest.getconftestmodules(sub) == [] assert conftest.getconftestmodules(conf.dirpath()) == [] -@pytest.mark.multi(name='test tests whatever .dotdir'.split()) +@pytest.mark.parametrize("name", 'test tests whatever .dotdir'.split()) def test_setinitial_conftest_subdirs(testdir, name): sub = testdir.mkdir(name) subconftest = sub.ensure("conftest.py") @@ -215,3 +215,40 @@ def test_conftest_import_order(testdir, monkeypatch): conftest = Conftest() monkeypatch.setattr(conftest, 'importconftest', impct) assert conftest.getconftestmodules(sub) == [ct1, ct2] + + +def test_fixture_dependency(testdir, monkeypatch): + ct1 = testdir.makeconftest("") + ct1 = testdir.makepyfile("__init__.py") + ct1.write("") + sub = testdir.mkdir("sub") + sub.join("__init__.py").write("") + sub.join("conftest.py").write(py.std.textwrap.dedent(""" + import pytest + + @pytest.fixture + def not_needed(): + assert False, "Should not be called!" + + @pytest.fixture + def foo(): + assert False, "Should not be called!" + + @pytest.fixture + def bar(foo): + return 'bar' + """)) + subsub = sub.mkdir("subsub") + subsub.join("__init__.py").write("") + subsub.join("test_bar.py").write(py.std.textwrap.dedent(""" + import pytest + + @pytest.fixture + def bar(): + return 'sub bar' + + def test_event_fixture(bar): + assert bar == 'sub bar' + """)) + result = testdir.runpytest("sub") + result.stdout.fnmatch_lines(["*1 passed*"]) diff --git a/testing/test_core.py b/testing/test_core.py index 0a18a8efc..071a6c264 100644 --- a/testing/test_core.py +++ b/testing/test_core.py @@ -1,19 +1,18 @@ import pytest, py, os -from _pytest.core import PluginManager -from _pytest.core import MultiCall, HookRelay, varnames +from _pytest.core import * # noqa +from _pytest.config import get_plugin_manager class TestBootstrapping: def test_consider_env_fails_to_import(self, monkeypatch): pluginmanager = PluginManager() monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") - pytest.raises(ImportError, "pluginmanager.consider_env()") + pytest.raises(ImportError, lambda: pluginmanager.consider_env()) def test_preparse_args(self): pluginmanager = PluginManager() - pytest.raises(ImportError, """ - pluginmanager.consider_preparse(["xyz", "-p", "hello123"]) - """) + pytest.raises(ImportError, lambda: + pluginmanager.consider_preparse(["xyz", "-p", "hello123"])) def test_plugin_prevent_register(self): pluginmanager = PluginManager() @@ -66,7 +65,7 @@ class TestBootstrapping: assert l2 == l3 def test_consider_setuptools_instantiation(self, monkeypatch): - pkg_resources = py.test.importorskip("pkg_resources") + pkg_resources = pytest.importorskip("pkg_resources") def my_iter(name): assert name == "pytest11" class EntryPoint: @@ -92,7 +91,7 @@ class TestBootstrapping: # ok, we did not explode def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): - x500 = testdir.makepyfile(pytest_x500="#") + testdir.makepyfile(pytest_x500="#") p = testdir.makepyfile(""" import pytest def test_hello(pytestconfig): @@ -109,7 +108,7 @@ class TestBootstrapping: pytest.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') pytest.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwx.y")') - reset = testdir.syspathinsert() + testdir.syspathinsert() pluginname = "pytest_hello" testdir.makepyfile(**{pluginname: ""}) pluginmanager.import_plugin("pytest_hello") @@ -127,7 +126,7 @@ class TestBootstrapping: pytest.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') pytest.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwex.y")') - reset = testdir.syspathinsert() + testdir.syspathinsert() testdir.mkpydir("pkg").join("plug.py").write("x=3") pluginname = "pkg.plug" pluginmanager.import_plugin(pluginname) @@ -149,7 +148,7 @@ class TestBootstrapping: mod = py.std.types.ModuleType("x") mod.pytest_plugins = "pytest_a" aplugin = testdir.makepyfile(pytest_a="#") - pluginmanager = PluginManager() + pluginmanager = get_plugin_manager() reprec = testdir.getreportrecorder(pluginmanager) #syspath.prepend(aplugin.dirpath()) py.std.sys.path.insert(0, str(aplugin.dirpath())) @@ -169,7 +168,7 @@ class TestBootstrapping: def test_consider_conftest_deps(self, testdir): mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() pp = PluginManager() - pytest.raises(ImportError, "pp.consider_conftest(mod)") + pytest.raises(ImportError, lambda: pp.consider_conftest(mod)) def test_pm(self): pp = PluginManager() @@ -209,9 +208,7 @@ class TestBootstrapping: l = pp.getplugins() assert mod in l pytest.raises(ValueError, "pp.register(mod)") - mod2 = py.std.types.ModuleType("pytest_hello") - #pp.register(mod2) # double pm - pytest.raises(ValueError, "pp.register(mod)") + pytest.raises(ValueError, lambda: pp.register(mod)) #assert not pp.isregistered(mod2) assert pp.getplugins() == l @@ -224,36 +221,21 @@ class TestBootstrapping: assert pp.isregistered(mod) def test_register_mismatch_method(self): - pp = PluginManager(load=True) + pp = get_plugin_manager() class hello: def pytest_gurgel(self): pass - pytest.raises(Exception, "pp.register(hello())") + pytest.raises(Exception, lambda: pp.register(hello())) def test_register_mismatch_arg(self): - pp = PluginManager(load=True) + pp = get_plugin_manager() class hello: def pytest_configure(self, asd): pass - excinfo = pytest.raises(Exception, "pp.register(hello())") - - - def test_notify_exception(self, capfd): - pp = PluginManager() - excinfo = pytest.raises(ValueError, "raise ValueError(1)") - pp.notify_exception(excinfo) - out, err = capfd.readouterr() - assert "ValueError" in err - class A: - def pytest_internalerror(self, excrepr): - return True - pp.register(A()) - pp.notify_exception(excinfo) - out, err = capfd.readouterr() - assert not err + pytest.raises(Exception, lambda: pp.register(hello())) def test_register(self): - pm = PluginManager(load=False) + pm = get_plugin_manager() class MyPlugin: pass my = MyPlugin() @@ -261,13 +243,13 @@ class TestBootstrapping: assert pm.getplugins() my2 = MyPlugin() pm.register(my2) - assert pm.getplugins()[1:] == [my, my2] + assert pm.getplugins()[-2:] == [my, my2] assert pm.isregistered(my) assert pm.isregistered(my2) pm.unregister(my) assert not pm.isregistered(my) - assert pm.getplugins()[1:] == [my2] + assert pm.getplugins()[-1:] == [my2] def test_listattr(self): plugins = PluginManager() @@ -284,7 +266,7 @@ class TestBootstrapping: assert l == [41, 42, 43] def test_hook_tracing(self): - pm = PluginManager() + pm = get_plugin_manager() saveindent = [] class api1: x = 41 @@ -307,7 +289,7 @@ class TestBootstrapping: class TestPytestPluginInteractions: def test_addhooks_conftestplugin(self, testdir): - newhooks = testdir.makepyfile(newhooks=""" + testdir.makepyfile(newhooks=""" def pytest_myhook(xyz): "new hook" """) @@ -319,14 +301,14 @@ class TestPytestPluginInteractions: def pytest_myhook(xyz): return xyz + 1 """) - config = testdir.Config() + config = get_plugin_manager().config config._conftest.importconftest(conf) print(config.pluginmanager.getplugins()) res = config.hook.pytest_myhook(xyz=10) assert res == [11] def test_addhooks_nohooks(self, testdir): - conf = testdir.makeconftest(""" + testdir.makeconftest(""" import sys def pytest_addhooks(pluginmanager): pluginmanager.addhooks(sys) @@ -352,16 +334,14 @@ class TestPytestPluginInteractions: return {'hello': 'world'} """) p = testdir.makepyfile(""" - from py.test import hello - import py + from pytest import hello + import pytest def test_hello(): assert hello == "world" - assert 'hello' in py.test.__all__ + assert 'hello' in pytest.__all__ """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*1 passed*" - ]) + reprec = testdir.inline_run(p) + reprec.assertoutcome(passed=1) def test_do_option_postinitialize(self, testdir): config = testdir.parseconfigure() @@ -383,13 +363,13 @@ class TestPytestPluginInteractions: config.pluginmanager.register(A()) assert len(l) == 0 - config.pluginmanager.do_configure(config=config) + config.do_configure() assert len(l) == 1 config.pluginmanager.register(A()) # leads to a configured() plugin assert len(l) == 2 assert l[0] != l[1] - config.pluginmanager.do_unconfigure(config=config) + config.do_unconfigure() config.pluginmanager.register(A()) assert len(l) == 2 @@ -445,7 +425,7 @@ def test_namespace_has_default_and_env_plugins(testdir): def test_varnames(): def f(x): - i = 3 + i = 3 # noqa class A: def f(self, y): pass @@ -456,6 +436,15 @@ def test_varnames(): assert varnames(A().f) == ('y',) assert varnames(B()) == ('z',) +def test_varnames_class(): + class C: + def __init__(self, x): + pass + class D: + pass + assert varnames(C) == ("x",) + assert varnames(D) == () + class TestMultiCall: def test_uses_copy_of_methods(self): l = [lambda: 42] @@ -510,7 +499,7 @@ class TestMultiCall: def test_tags_call_error(self): multicall = MultiCall([lambda x: x], {}) - pytest.raises(TypeError, "multicall.execute()") + pytest.raises(TypeError, multicall.execute) def test_call_subexecute(self): def m(__multicall__): @@ -558,7 +547,7 @@ class TestHookRelay: def hello(self, arg): "api hook 1" mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") - pytest.raises(TypeError, "mcm.hello(3)") + pytest.raises(TypeError, lambda: mcm.hello(3)) def test_firstresult_definition(self): pm = PluginManager() @@ -669,3 +658,10 @@ def test_default_markers(testdir): "*tryfirst*first*", "*trylast*last*", ]) + +def test_importplugin_issue375(testdir): + testdir.makepyfile(qwe="import aaaa") + excinfo = pytest.raises(ImportError, lambda: importplugin("qwe")) + assert "qwe" not in str(excinfo.value) + assert "aaaa" in str(excinfo.value) + diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 15f28613e..1d4e41333 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -4,7 +4,7 @@ import py, pytest import pdb xfail_if_pdbpp_installed = pytest.mark.xfail(hasattr(pdb, "__author__"), - reason="doctest/pdbpp problem: https://bitbucket.org/antocuni/pdb/issue/24/doctests-fail-when-pdbpp-is-installed") + reason="doctest/pdbpp problem: https://bitbucket.org/antocuni/pdb/issue/24/doctests-fail-when-pdbpp-is-installed", run=False) class TestDoctests: @@ -99,7 +99,7 @@ class TestDoctests: reprec.assertoutcome(failed=1) def test_doctest_unexpected_exception(self, testdir): - p = testdir.maketxtfile(""" + testdir.maketxtfile(""" >>> i = 0 >>> 0 / i 2 @@ -136,7 +136,7 @@ class TestDoctests: testdir.tmpdir.join("hello.py").write(py.code.Source(""" import asdalsdkjaslkdjasd """)) - p = testdir.maketxtfile(""" + testdir.maketxtfile(""" >>> import hello >>> """) @@ -209,6 +209,26 @@ class TestDoctests: reprec = testdir.inline_run(p, ) reprec.assertoutcome(passed=1) + @xfail_if_pdbpp_installed + def test_txtfile_with_usefixtures_in_ini(self, testdir): + testdir.makeini(""" + [pytest] + usefixtures = myfixture + """) + testdir.makeconftest(""" + import pytest + @pytest.fixture + def myfixture(monkeypatch): + monkeypatch.setenv("HELLO", "WORLD") + """) + + p = testdir.maketxtfile(""" + >>> import os + >>> os.environ["HELLO"] + 'WORLD' + """) + reprec = testdir.inline_run(p, ) + reprec.assertoutcome(passed=1) @xfail_if_pdbpp_installed def test_doctestmodule_with_fixtures(self, testdir): diff --git a/testing/test_fixture_finalizer.py b/testing/test_fixture_finalizer.py deleted file mode 100644 index 0fdaa68fc..000000000 --- a/testing/test_fixture_finalizer.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Tests for fixtures with different scoping.""" -import py.code - - -def test_fixture_finalizer(testdir): - testdir.makeconftest(""" - import pytest - import sys - - @pytest.fixture - def browser(request): - - def finalize(): - sys.stdout.write('Finalized') - request.addfinalizer(finalize) - return {} - """) - b = testdir.mkdir("subdir") - b.join("test_overriden_fixture_finalizer.py").write(py.code.Source(""" - import pytest - @pytest.fixture - def browser(browser): - browser['visited'] = True - return browser - - def test_browser(browser): - assert browser['visited'] is True - """)) - reprec = testdir.runpytest("-s") - for test in ['test_browser']: - reprec.stdout.fnmatch_lines('Finalized') diff --git a/testing/test_fixture_scope.py b/testing/test_fixture_scope.py deleted file mode 100644 index 0bd8c5206..000000000 --- a/testing/test_fixture_scope.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Tests for fixtures with different scoping.""" - - -def test_class_scope_with_normal_tests(testdir): - testpath = testdir.makepyfile(""" - import pytest - - class Box: - value = 0 - - @pytest.fixture(scope='class') - def a(request): - Box.value += 1 - return Box.value - - def test_a(a): - assert a == 1 - - class Test1: - def test_b(self, a): - assert a == 2 - - class Test2: - def test_c(self, a): - assert a == 3""") - reprec = testdir.inline_run(testpath) - for test in ['test_a', 'test_b', 'test_c']: - assert reprec.matchreport(test).passed diff --git a/testing/test_genscript.py b/testing/test_genscript.py index 88f7e832f..02bbabb6f 100644 --- a/testing/test_genscript.py +++ b/testing/test_genscript.py @@ -1,6 +1,5 @@ import pytest -import py, os, sys -import subprocess +import sys @pytest.fixture(scope="module") @@ -23,7 +22,8 @@ class Standalone: def test_gen(testdir, anypython, standalone): if sys.version_info >= (2,7): result = testdir._run(anypython, "-c", - "import sys;print sys.version_info >=(2,7)") + "import sys;print (sys.version_info >=(2,7))") + assert result.ret == 0 if result.stdout.str() == "False": pytest.skip("genscript called from python2.7 cannot work " "earlier python versions") @@ -36,14 +36,3 @@ def test_gen(testdir, anypython, standalone): result = standalone.run(anypython, testdir, p) assert result.ret != 0 -def test_rundist(testdir, pytestconfig, standalone): - pytestconfig.pluginmanager.skipifmissing("xdist") - testdir.makepyfile(""" - def test_one(): - pass - """) - result = standalone.run(sys.executable, testdir, '-n', '3') - assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*1 passed*", - ]) diff --git a/testing/test_helpconfig.py b/testing/test_helpconfig.py index d9679f653..df78ccecc 100644 --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -1,4 +1,4 @@ -import py, pytest,os +import py, pytest from _pytest.helpconfig import collectattr def test_version(testdir, pytestconfig): @@ -6,7 +6,7 @@ def test_version(testdir, pytestconfig): assert result.ret == 0 #p = py.path.local(py.__file__).dirpath() result.stderr.fnmatch_lines([ - '*py.test*%s*imported from*' % (pytest.__version__, ) + '*pytest*%s*imported from*' % (pytest.__version__, ) ]) if pytestconfig.pluginmanager._plugin_distinfo: result.stderr.fnmatch_lines([ diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 139ba50e5..05573ad68 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1,6 +1,8 @@ +# -*- coding: utf-8 -*- from xml.dom import minidom import py, sys, os +from _pytest.junitxml import LogXML def runandparse(testdir, *args): resultpath = testdir.tmpdir.join("junit.xml") @@ -37,7 +39,7 @@ class TestPython: result, dom = runandparse(testdir) assert result.ret node = dom.getElementsByTagName("testsuite")[0] - assert_attr(node, errors=0, failures=1, skips=3, tests=2) + assert_attr(node, name="pytest", errors=0, failures=1, skips=3, tests=2) def test_timing_function(self, testdir): testdir.makepyfile(""" @@ -282,6 +284,19 @@ class TestPython: if not sys.platform.startswith("java"): assert "hx" in fnode.toxml() + def test_assertion_binchars(self, testdir): + """this test did fail when the escaping wasnt strict""" + testdir.makepyfile(""" + + M1 = '\x01\x02\x03\x04' + M2 = '\x01\x02\x03\x05' + + def test_str_compare(): + assert M1 == M2 + """) + result, dom = runandparse(testdir) + print(dom.toxml()) + def test_pass_captures_stdout(self, testdir): testdir.makepyfile(""" def test_pass(): @@ -370,7 +385,7 @@ def test_nullbyte(testdir): assert False """) xmlf = testdir.tmpdir.join('junit.xml') - result = testdir.runpytest('--junitxml=%s' % xmlf) + testdir.runpytest('--junitxml=%s' % xmlf) text = xmlf.read() assert '\x00' not in text assert '#x00' in text @@ -386,11 +401,10 @@ def test_nullbyte_replace(testdir): assert False """) xmlf = testdir.tmpdir.join('junit.xml') - result = testdir.runpytest('--junitxml=%s' % xmlf) + testdir.runpytest('--junitxml=%s' % xmlf) text = xmlf.read() assert '#x0' in text - def test_invalid_xml_escape(): # Test some more invalid xml chars, the full range should be # tested really but let's just thest the edges of the ranges @@ -405,7 +419,6 @@ def test_invalid_xml_escape(): unichr(65) except NameError: unichr = chr - u = py.builtin._totext invalid = (0x00, 0x1, 0xB, 0xC, 0xE, 0x19, 27, # issue #126 0xD800, 0xDFFF, 0xFFFE, 0x0FFFF) #, 0x110000) @@ -425,18 +438,16 @@ def test_invalid_xml_escape(): assert chr(i) == bin_xml_escape(unichr(i)).uniobj def test_logxml_path_expansion(tmpdir, monkeypatch): - from _pytest.junitxml import LogXML + home_tilde = py.path.local(os.path.expanduser('~')).join('test.xml') - home_tilde = os.path.normpath(os.path.expanduser('~/test.xml')) - - xml_tilde = LogXML('~/test.xml', None) + xml_tilde = LogXML('~%stest.xml' % tmpdir.sep, None) assert xml_tilde.logfile == home_tilde # this is here for when $HOME is not set correct monkeypatch.setenv("HOME", tmpdir) home_var = os.path.normpath(os.path.expandvars('$HOME/test.xml')) - xml_var = LogXML('$HOME/test.xml', None) + xml_var = LogXML('$HOME%stest.xml' % tmpdir.sep, None) assert xml_var.logfile == home_var def test_logxml_changingdir(testdir): @@ -463,3 +474,26 @@ def test_escaped_parametrized_names_xml(testdir): assert_attr(node, name="test_func[#x00]") +def test_unicode_issue368(testdir): + path = testdir.tmpdir.join("test.xml") + log = LogXML(str(path), None) + ustr = py.builtin._totext("ВНИ!", "utf-8") + class report: + longrepr = ustr + sections = [] + nodeid = "something" + + # hopefully this is not too brittle ... + log.pytest_sessionstart() + log._opentestcase(report) + log.append_failure(report) + log.append_collect_failure(report) + log.append_collect_skipped(report) + log.append_error(report) + report.longrepr = "filename", 1, ustr + log.append_skipped(report) + report.wasxfail = ustr + log.append_skipped(report) + log.pytest_sessionfinish() + + diff --git a/testing/test_mark.py b/testing/test_mark.py index eeea4f023..75e28541c 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -13,7 +13,11 @@ class TestMark: def test_pytest_mark_notcallable(self): mark = Mark() - pytest.raises((AttributeError, TypeError), "mark()") + pytest.raises((AttributeError, TypeError), mark) + + def test_pytest_mark_name_starts_with_underscore(self): + mark = Mark() + pytest.raises(AttributeError, getattr, mark, '_some_name') def test_pytest_mark_bare(self): mark = Mark() @@ -35,7 +39,7 @@ class TestMark: mark = Mark() def f(): pass - marker = mark.world + mark.world mark.world(x=3)(f) assert f.world.kwargs['x'] == 3 mark.world(y=4)(f) @@ -53,6 +57,17 @@ class TestMark: assert f.world.args[0] == "hello" mark.world("world")(f) + def test_pytest_mark_positional_func_and_keyword(self): + mark = Mark() + def f(): + raise Exception + m = mark.world(f, omega="hello") + def g(): + pass + assert m(g) == g + assert g.world.args[0] is f + assert g.world.kwargs["omega"] == "hello" + def test_pytest_mark_reuse(self): mark = Mark() def f(): @@ -100,6 +115,38 @@ def test_markers_option(testdir): "*a1some*another marker", ]) +def test_markers_option_with_plugin_in_current_dir(testdir): + testdir.makeconftest('pytest_plugins = "flip_flop"') + testdir.makepyfile(flip_flop="""\ + def pytest_configure(config): + config.addinivalue_line("markers", "flip:flop") + + def pytest_generate_tests(metafunc): + try: + mark = metafunc.function.flipper + except AttributeError: + return + metafunc.parametrize("x", (10, 20))""") + testdir.makepyfile("""\ + import pytest + @pytest.mark.flipper + def test_example(x): + assert x""") + + result = testdir.runpytest("--markers") + result.stdout.fnmatch_lines(["*flip*flop*"]) + + +def test_mark_on_pseudo_function(testdir): + testdir.makepyfile(""" + import pytest + + @pytest.mark.r(lambda x: 0/0) + def test_hello(): + pass + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) def test_strict_prohibits_unregistered_markers(testdir): testdir.makepyfile(""" @@ -114,7 +161,7 @@ def test_strict_prohibits_unregistered_markers(testdir): "*unregisteredmark*not*registered*", ]) -@pytest.mark.multi(spec=[ +@pytest.mark.parametrize("spec", [ ("xyz", ("test_one",)), ("xyz and xyz2", ()), ("xyz2", ("test_two",)), @@ -137,7 +184,7 @@ def test_mark_option(spec, testdir): assert len(passed) == len(passed_result) assert list(passed) == list(passed_result) -@pytest.mark.multi(spec=[ +@pytest.mark.parametrize("spec", [ ("interface", ("test_interface",)), ("not interface", ("test_nointer",)), ]) @@ -162,9 +209,11 @@ def test_mark_option_custom(spec, testdir): assert len(passed) == len(passed_result) assert list(passed) == list(passed_result) -@pytest.mark.multi(spec=[ +@pytest.mark.parametrize("spec", [ ("interface", ("test_interface",)), - ("not interface", ("test_nointer",)), + ("not interface", ("test_nointer", "test_pass")), + ("pass", ("test_pass",)), + ("not pass", ("test_interface", "test_nointer")), ]) def test_keyword_option_custom(spec, testdir): testdir.makepyfile(""" @@ -172,6 +221,27 @@ def test_keyword_option_custom(spec, testdir): pass def test_nointer(): pass + def test_pass(): + pass + """) + opt, passed_result = spec + rec = testdir.inline_run("-k", opt) + passed, skipped, fail = rec.listoutcomes() + passed = [x.nodeid.split("::")[-1] for x in passed] + assert len(passed) == len(passed_result) + assert list(passed) == list(passed_result) + + +@pytest.mark.parametrize("spec", [ + ("None", ("test_func[None]",)), + ("1.3", ("test_func[1.3]",)) +]) +def test_keyword_option_parametrize(spec, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.parametrize("arg", [None, 1.3]) + def test_func(arg): + pass """) opt, passed_result = spec rec = testdir.inline_run("-k", opt) @@ -317,7 +387,7 @@ class TestFunctional: request.applymarker(pytest.mark.hello) def pytest_terminal_summary(terminalreporter): l = terminalreporter.stats['passed'] - terminalreporter._tw.line("keyword: %s" % l[0].keywords) + terminalreporter.writer.line("keyword: %s" % l[0].keywords) """) testdir.makepyfile(""" def test_func(arg): @@ -362,9 +432,8 @@ class TestFunctional: deselected_tests = dlist[0].items assert len(deselected_tests) == 2 - def test_keywords_at_node_level(self, testdir): - p = testdir.makepyfile(""" + testdir.makepyfile(""" import pytest @pytest.fixture(scope="session", autouse=True) def some(request): @@ -383,6 +452,30 @@ class TestFunctional: reprec = testdir.inline_run() reprec.assertoutcome(passed=1) + def test_keyword_added_for_session(self, testdir): + testdir.makeconftest(""" + import pytest + def pytest_collection_modifyitems(session): + session.add_marker("mark1") + session.add_marker(pytest.mark.mark2) + session.add_marker(pytest.mark.mark3) + pytest.raises(ValueError, lambda: + session.add_marker(10)) + """) + testdir.makepyfile(""" + def test_some(request): + assert "mark1" in request.keywords + assert "mark2" in request.keywords + assert "mark3" in request.keywords + assert 10 not in request.keywords + marker = request.node.get_marker("mark1") + assert marker.name == "mark1" + assert marker.args == () + assert marker.kwargs == {} + """) + reprec = testdir.inline_run("-m", "mark1") + reprec.assertoutcome(passed=1) + class TestKeywordSelection: def test_select_simple(self, testdir): file_test = testdir.makepyfile(""" @@ -486,3 +579,4 @@ class TestKeywordSelection: assert_test_is_not_selected("__") assert_test_is_not_selected("()") + diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 6c6b17480..99f77a646 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -35,26 +35,38 @@ def test_setattr(): monkeypatch.undo() # double-undo makes no modification assert A.x == 5 -class TestReplace: +class TestSetattrWithImportPath: def test_string_expression(self, monkeypatch): - monkeypatch.replace("os.path.abspath", lambda x: "hello2") + monkeypatch.setattr("os.path.abspath", lambda x: "hello2") assert os.path.abspath("123") == "hello2" def test_string_expression_class(self, monkeypatch): - monkeypatch.replace("_pytest.config.Config", 42) + monkeypatch.setattr("_pytest.config.Config", 42) import _pytest assert _pytest.config.Config == 42 + def test_unicode_string(self, monkeypatch): + monkeypatch.setattr("_pytest.config.Config", 42) + import _pytest + assert _pytest.config.Config == 42 + monkeypatch.delattr("_pytest.config.Config") + def test_wrong_target(self, monkeypatch): - pytest.raises(TypeError, lambda: monkeypatch.replace(None, None)) + pytest.raises(TypeError, lambda: monkeypatch.setattr(None, None)) def test_unknown_import(self, monkeypatch): pytest.raises(pytest.fail.Exception, - lambda: monkeypatch.replace("unkn123.classx", None)) + lambda: monkeypatch.setattr("unkn123.classx", None)) def test_unknown_attr(self, monkeypatch): pytest.raises(pytest.fail.Exception, - lambda: monkeypatch.replace("os.path.qweqwe", None)) + lambda: monkeypatch.setattr("os.path.qweqwe", None)) + + def test_delattr(self, monkeypatch): + monkeypatch.delattr("os.path.abspath") + assert not hasattr(os.path, "abspath") + monkeypatch.undo() + assert os.path.abspath def test_delattr(): class A: @@ -262,3 +274,5 @@ def test_issue156_undo_staticmethod(Sample): monkeypatch.undo() assert Sample.hello() + + diff --git a/testing/test_nose.py b/testing/test_nose.py index 12f9dece3..692759bc0 100644 --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -1,7 +1,7 @@ import py, pytest def setup_module(mod): - mod.nose = py.test.importorskip("nose") + mod.nose = pytest.importorskip("nose") def test_nose_setup(testdir): p = testdir.makepyfile(""" @@ -96,7 +96,7 @@ def test_nose_setup_func_failure(testdir): def test_nose_setup_func_failure_2(testdir): - p = testdir.makepyfile(""" + testdir.makepyfile(""" l = [] my_setup = 1 @@ -112,7 +112,7 @@ def test_nose_setup_func_failure_2(testdir): reprec.assertoutcome(passed=1) def test_nose_setup_partial(testdir): - py.test.importorskip("functools") + pytest.importorskip("functools") p = testdir.makepyfile(""" from functools import partial diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 84c7d2757..fc09bee7c 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -1,7 +1,8 @@ from __future__ import with_statement +import sys +import os import py, pytest from _pytest import config as parseopt -from textwrap import dedent @pytest.fixture def parser(): @@ -10,7 +11,7 @@ def parser(): class TestParser: def test_no_help_by_default(self, capsys): parser = parseopt.Parser(usage="xyz") - pytest.raises(SystemExit, 'parser.parse(["-h"])') + pytest.raises(SystemExit, lambda: parser.parse(["-h"])) out, err = capsys.readouterr() assert err.find("error: unrecognized arguments") != -1 @@ -63,9 +64,9 @@ class TestParser: assert group2 is group def test_group_ordering(self, parser): - group0 = parser.getgroup("1") - group1 = parser.getgroup("2") - group1 = parser.getgroup("3", after="1") + parser.getgroup("1") + parser.getgroup("2") + parser.getgroup("3", after="1") groups = parser._groups groups_names = [x.name for x in groups] assert groups_names == list("132") @@ -95,18 +96,24 @@ class TestParser: parser.addoption("--hello", dest="hello", action="store") args = parser.parse(['--hello', 'world']) assert args.hello == "world" - assert not getattr(args, parseopt.Config._file_or_dir) + assert not getattr(args, parseopt.FILE_OR_DIR) def test_parse2(self, parser): args = parser.parse([py.path.local()]) - assert getattr(args, parseopt.Config._file_or_dir)[0] == py.path.local() + assert getattr(args, parseopt.FILE_OR_DIR)[0] == py.path.local() + + def test_parse_known_args(self, parser): + parser.parse_known_args([py.path.local()]) + parser.addoption("--hello", action="store_true") + ns = parser.parse_known_args(["x", "--y", "--hello", "this"]) + assert ns.hello def test_parse_will_set_default(self, parser): parser.addoption("--hello", dest="hello", default="x", action="store") option = parser.parse([]) assert option.hello == "x" del option.hello - args = parser.parse_setoption([], option) + parser.parse_setoption([], option) assert option.hello == "x" def test_parse_setoption(self, parser): @@ -120,7 +127,7 @@ class TestParser: assert not args def test_parse_special_destination(self, parser): - x = parser.addoption("--ultimate-answer", type=int) + parser.addoption("--ultimate-answer", type=int) args = parser.parse(['--ultimate-answer', '42']) assert args.ultimate_answer == 42 @@ -128,13 +135,13 @@ class TestParser: parser.addoption("-R", action='store_true') parser.addoption("-S", action='store_false') args = parser.parse(['-R', '4', '2', '-S']) - assert getattr(args, parseopt.Config._file_or_dir) == ['4', '2'] + assert getattr(args, parseopt.FILE_OR_DIR) == ['4', '2'] args = parser.parse(['-R', '-S', '4', '2', '-R']) - assert getattr(args, parseopt.Config._file_or_dir) == ['4', '2'] + assert getattr(args, parseopt.FILE_OR_DIR) == ['4', '2'] assert args.R == True assert args.S == False args = parser.parse(['-R', '4', '-S', '2']) - assert getattr(args, parseopt.Config._file_or_dir) == ['4', '2'] + assert getattr(args, parseopt.FILE_OR_DIR) == ['4', '2'] assert args.R == True assert args.S == False @@ -244,14 +251,16 @@ def test_addoption_parser_epilog(testdir): def test_argcomplete(testdir, monkeypatch): if not py.path.local.sysfind('bash'): pytest.skip("bash not available") - import os - script = os.path.join(os.getcwd(), 'test_argcomplete') + script = str(testdir.tmpdir.join("test_argcomplete")) + pytest_bin = sys.argv[0] + if "py.test" not in os.path.basename(pytest_bin): + pytest.skip("need to be run with py.test executable, not %s" %(pytest_bin,)) + with open(str(script), 'w') as fp: # redirect output from argcomplete to stdin and stderr is not trivial # http://stackoverflow.com/q/12589419/1307905 # so we use bash - fp.write('COMP_WORDBREAKS="$COMP_WORDBREAKS" $(which py.test) ' - '8>&1 9>&2') + fp.write('COMP_WORDBREAKS="$COMP_WORDBREAKS" %s 8>&1 9>&2' % pytest_bin) # alternative would be exteneded Testdir.{run(),_run(),popen()} to be able # to handle a keyword argument env that replaces os.environ in popen or # extends the copy, advantage: could not forget to restore diff --git a/testing/test_pastebin.py b/testing/test_pastebin.py index fc24b88c0..c81bf88bf 100644 --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -1,4 +1,3 @@ -import pytest class TestPasting: def pytest_funcarg__pastebinlist(self, request): @@ -56,4 +55,4 @@ class TestRPCClient: assert proxy is not None assert proxy.__class__.__module__.startswith('xmlrpc') - + diff --git a/testing/test_pdb.py b/testing/test_pdb.py index b5dd8df2b..4cdf06e75 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -1,4 +1,5 @@ -import py, pytest + +import py import sys from test_doctest import xfail_if_pdbpp_installed @@ -62,7 +63,7 @@ class TestPDB: child.expect(".*i = 0") child.expect("(Pdb)") child.sendeof() - rest = child.read() + rest = child.read().decode("utf8") assert "1 failed" in rest assert "def test_1" not in rest if child.isalive(): @@ -127,7 +128,7 @@ class TestPDB: child.expect("x = 3") child.expect("(Pdb)") child.sendeof() - rest = child.read() + rest = child.read().decode("utf-8") assert "1 failed" in rest assert "def test_1" in rest assert "hello17" in rest # out is captured @@ -144,7 +145,7 @@ class TestPDB: child.expect("test_1") child.expect("(Pdb)") child.sendeof() - rest = child.read() + rest = child.read().decode("utf8") assert "1 failed" in rest assert "reading from stdin while output" not in rest if child.isalive(): @@ -162,7 +163,7 @@ class TestPDB: child.send("capsys.readouterr()\n") child.expect("hello1") child.sendeof() - rest = child.read() + child.read() if child.isalive(): child.wait() @@ -182,7 +183,7 @@ class TestPDB: child.expect("0") child.expect("(Pdb)") child.sendeof() - rest = child.read() + rest = child.read().decode("utf8") assert "1 failed" in rest if child.isalive(): child.wait() @@ -206,7 +207,7 @@ class TestPDB: child.sendline('c') child.expect("x = 4") child.sendeof() - rest = child.read() + rest = child.read().decode("utf8") assert "1 failed" in rest assert "def test_1" in rest assert "hello17" in rest # out is captured @@ -238,6 +239,7 @@ class TestPDB: child.expect("x = 5") child.sendeof() child.wait() + def test_pdb_collection_failure_is_shown(self, testdir): p1 = testdir.makepyfile("""xxx """) result = testdir.runpytest("--pdb", p1) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 64600f686..cf171b2a4 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -1,7 +1,7 @@ import py import pytest -import os, sys -from _pytest.pytester import LineMatcher, LineComp, HookRecorder +import os +from _pytest.pytester import HookRecorder from _pytest.core import PluginManager def test_reportrecorder(testdir): @@ -56,11 +56,10 @@ def test_reportrecorder(testdir): def test_parseconfig(testdir): - import py config1 = testdir.parseconfig() config2 = testdir.parseconfig() assert config2 != config1 - assert config1 != py.test.config + assert config1 != pytest.config def test_testdir_runs_with_plugin(testdir): testdir.makepyfile(""" @@ -104,7 +103,7 @@ def test_functional(testdir, linecomp): def test_func(_pytest): class ApiClass: def pytest_xyz(self, arg): "x" - hook = HookRelay([ApiClass], PluginManager(load=False)) + hook = HookRelay([ApiClass], PluginManager()) rec = _pytest.gethookrecorder(hook) class Plugin: def pytest_xyz(self, arg): diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 492f41e63..d8fe1784e 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -37,7 +37,7 @@ def test_recwarn_functional(testdir): assert tuple(res) == (2, 0, 0), res # -# ============ test py.test.deprecated_call() ============== +# ============ test pytest.deprecated_call() ============== # def dep(i): @@ -53,14 +53,14 @@ def dep_explicit(i): def test_deprecated_call_raises(): excinfo = pytest.raises(AssertionError, - "py.test.deprecated_call(dep, 3)") + "pytest.deprecated_call(dep, 3)") assert str(excinfo).find("did not produce") != -1 def test_deprecated_call(): - py.test.deprecated_call(dep, 0) + pytest.deprecated_call(dep, 0) def test_deprecated_call_ret(): - ret = py.test.deprecated_call(dep, 0) + ret = pytest.deprecated_call(dep, 0) assert ret == 42 def test_deprecated_call_preserves(): @@ -73,9 +73,9 @@ def test_deprecated_call_preserves(): def test_deprecated_explicit_call_raises(): pytest.raises(AssertionError, - "py.test.deprecated_call(dep_explicit, 3)") + "pytest.deprecated_call(dep_explicit, 3)") def test_deprecated_explicit_call(): - py.test.deprecated_call(dep_explicit, 0) - py.test.deprecated_call(dep_explicit, 0) + pytest.deprecated_call(dep_explicit, 0) + pytest.deprecated_call(dep_explicit, 0) diff --git a/testing/test_resultlog.py b/testing/test_resultlog.py index 9a3bb6465..e4f3faccc 100644 --- a/testing/test_resultlog.py +++ b/testing/test_resultlog.py @@ -195,3 +195,23 @@ def test_no_resultlog_on_slaves(testdir): pytest_unconfigure(config) assert not hasattr(config, '_resultlog') + +def test_failure_issue380(testdir): + testdir.makeconftest(""" + import pytest + class MyCollector(pytest.File): + def collect(self): + raise ValueError() + def repr_failure(self, excinfo): + return "somestring" + def pytest_collect_file(path, parent): + return MyCollector(parent=parent, fspath=path) + """) + testdir.makepyfile(""" + def test_func(): + pass + """) + result = testdir.runpytest("--resultlog=log") + assert result.ret == 1 + + diff --git a/testing/test_runner.py b/testing/test_runner.py index 916746be1..b65f75aea 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -1,6 +1,7 @@ +from __future__ import with_statement + import pytest, py, sys, os from _pytest import runner, main -from py._code.code import ReprExceptionInfo class TestSetupState: def test_setup(self, testdir): @@ -39,10 +40,39 @@ class TestSetupState: def setup_module(mod): raise ValueError(42) def test_func(): pass - """) + """) # noqa ss = runner.SetupState() - pytest.raises(ValueError, "ss.prepare(item)") - pytest.raises(ValueError, "ss.prepare(item)") + pytest.raises(ValueError, lambda: ss.prepare(item)) + pytest.raises(ValueError, lambda: ss.prepare(item)) + + def test_teardown_multiple_one_fails(self, testdir): + r = [] + def fin1(): r.append('fin1') + def fin2(): raise Exception('oops') + def fin3(): r.append('fin3') + item = testdir.getitem("def test_func(): pass") + ss = runner.SetupState() + ss.addfinalizer(fin1, item) + ss.addfinalizer(fin2, item) + ss.addfinalizer(fin3, item) + with pytest.raises(Exception) as err: + ss._callfinalizers(item) + assert err.value.args == ('oops',) + assert r == ['fin3', 'fin1'] + + def test_teardown_multiple_fail(self, testdir): + # Ensure the first exception is the one which is re-raised. + # Ideally both would be reported however. + def fin1(): raise Exception('oops1') + def fin2(): raise Exception('oops2') + item = testdir.getitem("def test_func(): pass") + ss = runner.SetupState() + ss.addfinalizer(fin1, item) + ss.addfinalizer(fin2, item) + with pytest.raises(Exception) as err: + ss._callfinalizers(item) + assert err.value.args == ('oops2',) + class BaseFunctionalTests: def test_passfunction(self, testdir): @@ -238,7 +268,7 @@ class BaseFunctionalTests: raise SystemExit(42) """) except SystemExit: - py.test.fail("runner did not catch SystemExit") + pytest.fail("runner did not catch SystemExit") rep = reports[1] assert rep.failed assert rep.when == "call" @@ -250,10 +280,10 @@ class BaseFunctionalTests: def test_func(): raise pytest.exit.Exception() """) - except py.test.exit.Exception: + except pytest.exit.Exception: pass else: - py.test.fail("did not raise") + pytest.fail("did not raise") class TestExecutionNonForked(BaseFunctionalTests): def getrunner(self): @@ -270,14 +300,14 @@ class TestExecutionNonForked(BaseFunctionalTests): except KeyboardInterrupt: pass else: - py.test.fail("did not raise") + pytest.fail("did not raise") class TestExecutionForked(BaseFunctionalTests): pytestmark = pytest.mark.skipif("not hasattr(os, 'fork')") def getrunner(self): # XXX re-arrange this test to live in pytest-xdist - xplugin = py.test.importorskip("xdist.plugin") + xplugin = pytest.importorskip("xdist.plugin") return xplugin.forked_run_report def test_suicide(self, testdir): @@ -387,15 +417,15 @@ def test_outcomeexception_exceptionattributes(): def test_pytest_exit(): try: - py.test.exit("hello") - except py.test.exit.Exception: + pytest.exit("hello") + except pytest.exit.Exception: excinfo = py.code.ExceptionInfo() assert excinfo.errisinstance(KeyboardInterrupt) def test_pytest_fail(): try: - py.test.fail("hello") - except py.test.fail.Exception: + pytest.fail("hello") + except pytest.fail.Exception: excinfo = py.code.ExceptionInfo() s = excinfo.exconly(tryshort=True) assert s.startswith("Failed") @@ -424,47 +454,49 @@ def test_exception_printing_skip(): assert s.startswith("Skipped") def test_importorskip(): - importorskip = py.test.importorskip + importorskip = pytest.importorskip def f(): importorskip("asdlkj") try: - sys = importorskip("sys") + sys = importorskip("sys") # noqa assert sys == py.std.sys - #path = py.test.importorskip("os.path") + #path = pytest.importorskip("os.path") #assert path == py.std.os.path excinfo = pytest.raises(pytest.skip.Exception, f) path = py.path.local(excinfo.getrepr().reprcrash.path) # check that importorskip reports the actual call # in this test the test_runner.py file assert path.purebasename == "test_runner" - pytest.raises(SyntaxError, "py.test.importorskip('x y z')") - pytest.raises(SyntaxError, "py.test.importorskip('x=y')") - path = importorskip("py", minversion=".".join(py.__version__)) + pytest.raises(SyntaxError, "pytest.importorskip('x y z')") + pytest.raises(SyntaxError, "pytest.importorskip('x=y')") mod = py.std.types.ModuleType("hello123") mod.__version__ = "1.3" + sys.modules["hello123"] = mod pytest.raises(pytest.skip.Exception, """ - py.test.importorskip("hello123", minversion="5.0") + pytest.importorskip("hello123", minversion="1.3.1") """) + mod2 = pytest.importorskip("hello123", minversion="1.3") + assert mod2 == mod except pytest.skip.Exception: print(py.code.ExceptionInfo()) - py.test.fail("spurious skip") + pytest.fail("spurious skip") def test_importorskip_imports_last_module_part(): - ospath = py.test.importorskip("os.path") + ospath = pytest.importorskip("os.path") assert os.path == ospath def test_pytest_cmdline_main(testdir): p = testdir.makepyfile(""" - import py + import pytest def test_hello(): assert 1 if __name__ == '__main__': - py.test.cmdline.main([__file__]) + pytest.cmdline.main([__file__]) """) import subprocess popen = subprocess.Popen([sys.executable, str(p)], stdout=subprocess.PIPE) - s = popen.stdout.read() + popen.communicate() ret = popen.wait() assert ret == 0 diff --git a/testing/test_session.py b/testing/test_session.py index e8e67bde2..af31b9f8e 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -54,7 +54,7 @@ class SessionTests: out = failed[0].longrepr.reprcrash.message if not out.find("DID NOT RAISE") != -1: print(out) - py.test.fail("incorrect raises() output") + pytest.fail("incorrect raises() output") def test_generator_yields_None(self, testdir): reprec = testdir.inline_runsource(""" @@ -127,7 +127,7 @@ class SessionTests: try: reprec = testdir.inline_run(testdir.tmpdir) except pytest.skip.Exception: - py.test.fail("wrong skipped caught") + pytest.fail("wrong skipped caught") reports = reprec.getreports("pytest_collectreport") assert len(reports) == 1 assert reports[0].skipped @@ -204,17 +204,18 @@ class TestNewSession(SessionTests): def test_plugin_specify(testdir): testdir.chdir() - config = pytest.raises(ImportError, """ + pytest.raises(ImportError, """ testdir.parseconfig("-p", "nqweotexistent") """) #pytest.raises(ImportError, - # "config.pluginmanager.do_configure(config)" + # "config.do_configure(config)" #) def test_plugin_already_exists(testdir): config = testdir.parseconfig("-p", "terminal") assert config.option.plugins == ['terminal'] - config.pluginmanager.do_configure(config) + config.do_configure() + config.do_unconfigure() def test_exclude(testdir): hellodir = testdir.mkdir("hello") diff --git a/testing/test_skipping.py b/testing/test_skipping.py index a3074b191..d85a5d635 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1,8 +1,7 @@ import pytest import sys -from _pytest.skipping import MarkEvaluator, folded_skips -from _pytest.skipping import pytest_runtest_setup +from _pytest.skipping import MarkEvaluator, folded_skips, pytest_runtest_setup from _pytest.runner import runtestprotocol class TestEvaluator: @@ -108,7 +107,7 @@ class TestEvaluator: pass """) ev = MarkEvaluator(item, 'skipif') - exc = pytest.raises(pytest.fail.Exception, "ev.istrue()") + exc = pytest.raises(pytest.fail.Exception, ev.istrue) assert """Failed: you need to specify reason=STRING when using booleans as conditions.""" in exc.value.msg def test_skipif_class(self, testdir): @@ -159,13 +158,14 @@ class TestXFail: @pytest.mark.xfail def test_func(): assert 0 + def test_func2(): + pytest.xfail("hello") """) result = testdir.runpytest("--runxfail") - assert result.ret == 1 result.stdout.fnmatch_lines([ "*def test_func():*", "*assert 0*", - "*1 failed*", + "*1 failed*1 pass*", ]) def test_xfail_evalfalse_but_fails(self, testdir): @@ -188,7 +188,7 @@ class TestXFail: def test_this(): assert 0 """) - result = testdir.runpytest(p, '-v') + testdir.runpytest(p, '-v') #result.stdout.fnmatch_lines([ # "*HINT*use*-r*" #]) @@ -261,10 +261,7 @@ class TestXFail: "*reason:*hello*", ]) result = testdir.runpytest(p, "--runxfail") - result.stdout.fnmatch_lines([ - "*def test_this():*", - "*pytest.xfail*", - ]) + result.stdout.fnmatch_lines("*1 pass*") def test_xfail_imperative_in_setup_function(self, testdir): p = testdir.makepyfile(""" @@ -285,10 +282,10 @@ class TestXFail: "*reason:*hello*", ]) result = testdir.runpytest(p, "--runxfail") - result.stdout.fnmatch_lines([ - "*def setup_function(function):*", - "*pytest.xfail*", - ]) + result.stdout.fnmatch_lines(""" + *def test_this* + *1 fail* + """) def xtest_dynamic_xfail_set_during_setup(self, testdir): p = testdir.makepyfile(""" @@ -372,8 +369,9 @@ class TestSkipif: @pytest.mark.skipif("hasattr(os, 'sep')") def test_func(): pass - """) - x = pytest.raises(pytest.skip.Exception, "pytest_runtest_setup(item)") + """) # noqa + x = pytest.raises(pytest.skip.Exception, lambda: + pytest_runtest_setup(item)) assert x.value.msg == "condition: hasattr(os, 'sep')" diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 7007bcc67..b1755b0cf 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -39,7 +39,7 @@ def pytest_generate_tests(metafunc): class TestTerminal: def test_pass_skip_fail(self, testdir, option): - p = testdir.makepyfile(""" + testdir.makepyfile(""" import pytest def test_ok(): pass @@ -76,7 +76,6 @@ class TestTerminal: def test_writeline(self, testdir, linecomp): modcol = testdir.getmodulecol("def test_one(): pass") - stringio = py.io.TextIO() rep = TerminalReporter(modcol.config, file=linecomp.stringio) rep.write_fspath_result(py.path.local("xy.py"), '.') rep.write_line("hello world") @@ -97,7 +96,7 @@ class TestTerminal: ]) def test_runtest_location_shown_before_test_starts(self, testdir): - p1 = testdir.makepyfile(""" + testdir.makepyfile(""" def test_1(): import time time.sleep(20) @@ -108,7 +107,7 @@ class TestTerminal: child.kill(15) def test_itemreport_subclasses_show_subclassed_file(self, testdir): - p1 = testdir.makepyfile(test_p1=""" + testdir.makepyfile(test_p1=""" class BaseTests: def test_p1(self): pass @@ -145,7 +144,7 @@ class TestTerminal: assert " <- " not in result.stdout.str() def test_keyboard_interrupt(self, testdir, option): - p = testdir.makepyfile(""" + testdir.makepyfile(""" def test_foobar(): assert 0 def test_spamegg(): @@ -172,7 +171,7 @@ class TestTerminal: def pytest_sessionstart(): raise KeyboardInterrupt """) - p = testdir.makepyfile(""" + testdir.makepyfile(""" def test_foobar(): pass """) @@ -214,7 +213,7 @@ class TestCollectonly: ]) def test_collectonly_fatal(self, testdir): - p1 = testdir.makeconftest(""" + testdir.makeconftest(""" def pytest_collectstart(collector): assert 0, "urgs" """) @@ -233,7 +232,6 @@ class TestCollectonly: pass """) result = testdir.runpytest("--collect-only", p) - stderr = result.stderr.str().strip() #assert stderr.startswith("inserting into sys.path") assert result.ret == 0 result.stdout.fnmatch_lines([ @@ -247,7 +245,6 @@ class TestCollectonly: def test_collectonly_error(self, testdir): p = testdir.makepyfile("import Errlkjqweqwe") result = testdir.runpytest("--collect-only", p) - stderr = result.stderr.str().strip() assert result.ret == 1 result.stdout.fnmatch_lines(py.code.Source(""" *ERROR* @@ -293,7 +290,7 @@ def test_repr_python_version(monkeypatch): class TestFixtureReporting: def test_setup_fixture_error(self, testdir): - p = testdir.makepyfile(""" + testdir.makepyfile(""" def setup_function(function): print ("setup func") assert 0 @@ -311,7 +308,7 @@ class TestFixtureReporting: assert result.ret != 0 def test_teardown_fixture_error(self, testdir): - p = testdir.makepyfile(""" + testdir.makepyfile(""" def test_nada(): pass def teardown_function(function): @@ -329,7 +326,7 @@ class TestFixtureReporting: ]) def test_teardown_fixture_error_and_test_failure(self, testdir): - p = testdir.makepyfile(""" + testdir.makepyfile(""" def test_fail(): assert 0, "failingfunc" @@ -403,7 +400,7 @@ class TestTerminalFunctional: assert result.ret == 0 def test_header_trailer_info(self, testdir): - p1 = testdir.makepyfile(""" + testdir.makepyfile(""" def test_passes(): pass """) @@ -411,8 +408,9 @@ class TestTerminalFunctional: verinfo = ".".join(map(str, py.std.sys.version_info[:3])) result.stdout.fnmatch_lines([ "*===== test session starts ====*", - "platform %s -- Python %s*" % ( - py.std.sys.platform, verinfo), # , py.std.sys.executable), + "platform %s -- Python %s* -- py-%s -- pytest-%s" % ( + py.std.sys.platform, verinfo, + py.__version__, pytest.__version__), "*test_header_trailer_info.py .", "=* 1 passed in *.[0-9][0-9] seconds *=", ]) @@ -486,20 +484,32 @@ class TestTerminalFunctional: def test_fail_extra_reporting(testdir): - p = testdir.makepyfile("def test_this(): assert 0") - result = testdir.runpytest(p) + testdir.makepyfile("def test_this(): assert 0") + result = testdir.runpytest() assert 'short test summary' not in result.stdout.str() - result = testdir.runpytest(p, '-rf') + result = testdir.runpytest('-rf') result.stdout.fnmatch_lines([ "*test summary*", "FAIL*test_fail_extra_reporting*", ]) def test_fail_reporting_on_pass(testdir): - p = testdir.makepyfile("def test_this(): assert 1") - result = testdir.runpytest(p, '-rf') + testdir.makepyfile("def test_this(): assert 1") + result = testdir.runpytest('-rf') assert 'short test summary' not in result.stdout.str() +def test_color_yes(testdir): + testdir.makepyfile("def test_this(): assert 1") + result = testdir.runpytest('--color=yes') + assert 'test session starts' in result.stdout.str() + assert '\x1b[1m' in result.stdout.str() + +def test_color_no(testdir): + testdir.makepyfile("def test_this(): assert 1") + result = testdir.runpytest('--color=no') + assert 'test session starts' in result.stdout.str() + assert '\x1b[1m' not in result.stdout.str() + def test_getreportopt(): class config: class option: @@ -522,7 +532,7 @@ def test_getreportopt(): def test_terminalreporter_reportopt_addopts(testdir): testdir.makeini("[pytest]\naddopts=-rs") - p = testdir.makepyfile(""" + testdir.makepyfile(""" def pytest_funcarg__tr(request): tr = request.config.pluginmanager.getplugin("terminalreporter") return tr @@ -570,7 +580,7 @@ class TestGenericReporting: provider to run e.g. distributed tests. """ def test_collect_fail(self, testdir, option): - p = testdir.makepyfile("import xyz\n") + testdir.makepyfile("import xyz\n") result = testdir.runpytest(*option.args) result.stdout.fnmatch_lines([ "> import xyz", @@ -579,7 +589,7 @@ class TestGenericReporting: ]) def test_maxfailures(self, testdir, option): - p = testdir.makepyfile(""" + testdir.makepyfile(""" def test_1(): assert 0 def test_2(): @@ -597,7 +607,7 @@ class TestGenericReporting: def test_tb_option(self, testdir, option): - p = testdir.makepyfile(""" + testdir.makepyfile(""" import pytest def g(): raise IndexError @@ -672,19 +682,18 @@ def test_fdopen_kept_alive_issue124(testdir): stdout = k.pop() stdout.close() """) - result = testdir.runpytest("-s") + result = testdir.runpytest() result.stdout.fnmatch_lines([ "*2 passed*" ]) - def test_tbstyle_native_setup_error(testdir): - p = testdir.makepyfile(""" + testdir.makepyfile(""" import pytest @pytest.fixture def setup_error_fixture(): raise Exception("error in exception") - + def test_error_fixture(setup_error_fixture): pass """) @@ -692,3 +701,16 @@ def test_tbstyle_native_setup_error(testdir): result.stdout.fnmatch_lines([ '*File *test_tbstyle_native_setup_error.py", line *, in setup_error_fixture*' ]) + +def test_terminal_summary(testdir): + testdir.makeconftest(""" + def pytest_terminal_summary(terminalreporter): + w = terminalreporter + w.section("hello") + w.line("world") + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(""" + *==== hello ====* + world + """) diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 164ee68c6..a7870ff89 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -1,5 +1,4 @@ import py, pytest -import os from _pytest.tmpdir import tmpdir, TempdirHandler @@ -28,15 +27,15 @@ def test_funcarg(testdir): assert bn == "qwe__abc" def test_ensuretemp(recwarn): - #py.test.deprecated_call(py.test.ensuretemp, 'hello') - d1 = py.test.ensuretemp('hello') - d2 = py.test.ensuretemp('hello') + #pytest.deprecated_call(pytest.ensuretemp, 'hello') + d1 = pytest.ensuretemp('hello') + d2 = pytest.ensuretemp('hello') assert d1 == d2 assert d1.check(dir=1) class TestTempdirHandler: def test_mktemp(self, testdir): - config = testdir.Config() + config = testdir.parseconfig() config.option.basetemp = testdir.mkdir("hello") t = TempdirHandler(config) tmp = t.mktemp("world") @@ -91,3 +90,12 @@ def test_tmpdir_always_is_realpath(testdir): result = testdir.runpytest("-s", p, '--basetemp=%s/bt' % linktemp) assert not result.ret +def test_tmpdir_too_long_on_parametrization(testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.parametrize("arg", ["1"*1000]) + def test_some(arg, tmpdir): + tmpdir.ensure("hello") + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 3752cf868..68b629705 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -15,7 +15,7 @@ def test_simple_unittest(testdir): assert reprec.matchreport("test_failing").failed def test_runTest_method(testdir): - testpath=testdir.makepyfile(""" + testdir.makepyfile(""" import unittest pytest_plugins = "pytest_unittest" class MyTestCaseWithRunTest(unittest.TestCase): @@ -236,7 +236,7 @@ def test_setup_class(testdir): reprec.assertoutcome(passed=3) -@pytest.mark.multi(type=['Error', 'Failure']) +@pytest.mark.parametrize("type", ['Error', 'Failure']) def test_testcase_adderrorandfailure_defers(testdir, type): testdir.makepyfile(""" from unittest import TestCase @@ -256,7 +256,7 @@ def test_testcase_adderrorandfailure_defers(testdir, type): result = testdir.runpytest() assert 'should not raise' not in result.stdout.str() -@pytest.mark.multi(type=['Error', 'Failure']) +@pytest.mark.parametrize("type", ['Error', 'Failure']) def test_testcase_custom_exception_info(testdir, type): testdir.makepyfile(""" from unittest import TestCase @@ -310,9 +310,10 @@ def test_module_level_pytestmark(testdir): reprec.assertoutcome(skipped=1) -def test_testcase_skip_property(testdir): +def test_trial_testcase_skip_property(testdir): + pytest.importorskip('twisted.trial.unittest') testpath = testdir.makepyfile(""" - import unittest + from twisted.trial import unittest class MyTestCase(unittest.TestCase): skip = 'dont run' def test_func(self): @@ -321,9 +322,11 @@ def test_testcase_skip_property(testdir): reprec = testdir.inline_run(testpath, "-s") reprec.assertoutcome(skipped=1) -def test_testfunction_skip_property(testdir): + +def test_trial_testfunction_skip_property(testdir): + pytest.importorskip('twisted.trial.unittest') testpath = testdir.makepyfile(""" - import unittest + from twisted.trial import unittest class MyTestCase(unittest.TestCase): def test_func(self): pass @@ -333,6 +336,32 @@ def test_testfunction_skip_property(testdir): reprec.assertoutcome(skipped=1) +def test_trial_testcase_todo_property(testdir): + pytest.importorskip('twisted.trial.unittest') + testpath = testdir.makepyfile(""" + from twisted.trial import unittest + class MyTestCase(unittest.TestCase): + todo = 'dont run' + def test_func(self): + assert 0 + """) + reprec = testdir.inline_run(testpath, "-s") + reprec.assertoutcome(skipped=1) + + +def test_trial_testfunction_todo_property(testdir): + pytest.importorskip('twisted.trial.unittest') + testpath = testdir.makepyfile(""" + from twisted.trial import unittest + class MyTestCase(unittest.TestCase): + def test_func(self): + assert 0 + test_func.todo = 'dont run' + """) + reprec = testdir.inline_run(testpath, "-s") + reprec.assertoutcome(skipped=1) + + class TestTrialUnittest: def setup_class(cls): cls.ut = pytest.importorskip("twisted.trial.unittest") @@ -654,3 +683,21 @@ def test_no_teardown_if_setupclass_failed(testdir): reprec = testdir.inline_run(testpath) reprec.assertoutcome(passed=1, failed=1) + +def test_issue333_result_clearing(testdir): + testdir.makeconftest(""" + def pytest_runtest_call(__multicall__, item): + __multicall__.execute() + assert 0 + """) + testdir.makepyfile(""" + import unittest + class TestIt(unittest.TestCase): + def test_func(self): + 0/0 + """) + + reprec = testdir.inline_run() + reprec.assertoutcome(failed=1) + + diff --git a/tox.ini b/tox.ini index fe0be41c1..670709aa1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,21 +1,21 @@ [tox] distshare={homedir}/.tox/distshare -envlist=py26,py27,py27-nobyte,py32,py33,py27-xdist,trial +envlist=flakes,py26,py27,pypy,py27-pexpect,py33-pexpect,py27-nobyte,py32,py33,py27-xdist,py33-xdist,trial [testenv] changedir=testing commands= py.test --lsof -rfsxX --junitxml={envlogdir}/junit-{envname}.xml [] deps= - pexpect nose [testenv:genscript] changedir=. commands= py.test --genscript=pytest1 -[testenv:py25] -setenv = - PIP_INSECURE=1 +[testenv:flakes] +changedir= +deps = pytest-flakes>=0.2 +commands = py.test --flakes -m flakes _pytest testing [testenv:py27-xdist] changedir=. @@ -27,6 +27,28 @@ commands= py.test -n3 -rfsxX \ --junitxml={envlogdir}/junit-{envname}.xml testing +[testenv:py33-xdist] +changedir=. +basepython=python3.3 +deps={[testenv:py27-xdist]deps} +commands= + py.test -n3 -rfsxX \ + --junitxml={envlogdir}/junit-{envname}.xml testing + +[testenv:py27-pexpect] +changedir=testing +basepython=python2.7 +deps=pexpect +commands= + py.test -rfsxX test_pdb.py test_terminal.py test_unittest.py + +[testenv:py33-pexpect] +changedir=testing +basepython=python2.7 +deps={[testenv:py27-pexpect]deps} +commands= + py.test -rfsxX test_pdb.py test_terminal.py test_unittest.py + [testenv:py27-nobyte] changedir=. basepython=python2.7 @@ -41,7 +63,6 @@ commands= [testenv:trial] changedir=. deps=twisted - pexpect commands= py.test -rsxf \ --junitxml={envlogdir}/junit-{envname}.xml {posargs:testing/test_unittest.py} @@ -50,14 +71,6 @@ changedir=. commands=py.test --doctest-modules _pytest deps= -[testenv:py32] -deps= - nose - -[testenv:py33] -deps= - nose - [testenv:doc] basepython=python changedir=doc/en @@ -97,7 +110,7 @@ commands= minversion=2.0 plugins=pytester #--pyargs --doctest-modules --ignore=.tox -addopts= -rxs +addopts= -rxsX rsyncdirs=tox.ini pytest.py _pytest testing python_files=test_*.py *_test.py testing/*/*.py python_classes=Test Acceptance