diff --git a/.hgtags b/.hgtags index 6cac1da67..eadb1993b 100644 --- a/.hgtags +++ b/.hgtags @@ -28,3 +28,6 @@ d5eacf390af74553227122b85e20345d47b2f9e6 1.3.1 8b8e7c25a13cf863f01b2dd955978285ae9daf6a 1.3.1 3bff44b188a7ec1af328d977b9d39b6757bb38df 1.3.2 c59d3fa8681a5b5966b8375b16fccd64a3a8dbeb 1.3.3 +79ef6377705184c55633d456832eea318fedcf61 1.3.4 +79ef6377705184c55633d456832eea318fedcf61 1.3.4 +90fffd35373e9f125af233f78b19416f0938d841 1.3.4 diff --git a/CHANGELOG b/CHANGELOG index 5c66a8772..cbd47878f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,21 @@ + +Changes between 1.3.4 and 1.4.0.dev0 +================================================== + +- introduce (customizable) assertion failure representations (Floris Bruynooghe) +- major refactoring of internal collection handling +- majorly reduce py.test core code, shift function/python testing to own plugin +- fix issue88 (finding custom test nodes from command line arg) + +Changes between 1.3.3 and 1.3.4 +================================================== + +- fix issue111: improve install documentation for windows +- fix issue119: fix custom collectability of __init__.py as a module +- fix issue116: --doctestmodules work with __init__.py files as well +- 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 ================================================== diff --git a/ISSUES.txt b/ISSUES.txt index f585b00f1..415ed8b9a 100644 --- a/ISSUES.txt +++ b/ISSUES.txt @@ -1,3 +1,12 @@ +checks / deprecations for next release +--------------------------------------------------------------- +tags: bug 1.4 core xdist + +* reportinfo -> location in hooks and items +* check oejskit plugin compatibility +* terminal reporting - dot-printing +* some simple profiling + refine session initialization / fix custom collect crash --------------------------------------------------------------- tags: bug 1.4 core xdist diff --git a/doc/announce/release-1.3.4.txt b/doc/announce/release-1.3.4.txt new file mode 100644 index 000000000..c156c8bdb --- /dev/null +++ b/doc/announce/release-1.3.4.txt @@ -0,0 +1,22 @@ +py.test/pylib 1.3.4: fixes and new native traceback option +=========================================================================== + +pylib/py.test 1.3.4 is a minor maintenance release mostly containing bug fixes +and a new "--tb=native" traceback option to show "normal" Python standard +tracebacks instead of the py.test enhanced tracebacks. See below for more +change info and http://pytest.org for more general information on features +and configuration of the testing tool. + +Thanks to the issue reporters and generally to Ronny Pfannschmidt for help. + +cheers, +holger krekel + +Changes between 1.3.3 and 1.3.4 +================================================== + +- fix issue111: improve install documentation for windows +- fix issue119: fix custom collectability of __init__.py as a module +- fix issue116: --doctestmodules work with __init__.py files as well +- fix issue115: unify internal exception passthrough/catching/GeneratorExit +- fix issue118: new --tb=native for presenting cpython-standard exceptions diff --git a/doc/example/assertion/failure_demo.py b/doc/example/assertion/failure_demo.py index 5fba99db7..f0266b82c 100644 --- a/doc/example/assertion/failure_demo.py +++ b/doc/example/assertion/failure_demo.py @@ -118,5 +118,43 @@ def test_dynamic_compile_shows_nicely(): module.foo() +class TestSpecialisedExplanations(object): + def test_eq_text(self): + assert 'spam' == 'eggs' + + def test_eq_similar_text(self): + assert 'foo 1 bar' == 'foo 2 bar' + + def test_eq_multiline_text(self): + assert 'foo\nspam\nbar' == 'foo\neggs\nbar' + + def test_eq_long_text(self): + a = '1'*100 + 'a' + '2'*100 + b = '1'*100 + 'b' + '2'*100 + assert a == b + + def test_eq_long_text_multiline(self): + a = '1\n'*100 + 'a' + '2\n'*100 + b = '1\n'*100 + 'b' + '2\n'*100 + assert a == b + + def test_eq_list(self): + assert [0, 1, 2] == [0, 1, 3] + + def test_eq_list_long(self): + a = [0]*100 + [1] + [3]*100 + b = [0]*100 + [2] + [3]*100 + assert a == b + + def test_eq_dict(self): + assert {'a': 0, 'b': 1} == {'a': 0, 'b': 2} + + def test_eq_set(self): + assert set([0, 10, 11, 12]) == set([0, 20, 21]) + + def test_in_list(self): + assert 1 in [0, 2, 3, 4, 5] + + def globf(x): return x+1 diff --git a/doc/install.txt b/doc/install.txt index 64d9b0c6f..43d37b1bf 100644 --- a/doc/install.txt +++ b/doc/install.txt @@ -26,7 +26,47 @@ py.test/pylib installation info in a nutshell .. _`bin`: bin.html -Best practise: install tool and dependencies virtually +.. _`easy_install`: + +Installation using easy_install +=================================================== + +Both `Distribute`_ and setuptools_ provide the ``easy_install`` +installation tool with which you can type into a command line window:: + + easy_install -U py + +to install the latest release of the py lib and py.test. The ``-U`` switch +will trigger an upgrade if you already have an older version installed. +Note that setuptools works ok with Python2 interpreters while `Distribute`_ +additionally works with Python3 and also avoid some issues on Windows. + +Known issues: + +- **Windows**: If "easy_install" or "py.test" are not found + please see here for preparing your environment for running + command line tools: `Python for Windows`_. You may alternatively + use an `ActivePython install`_ which makes command line tools + automatically available under Windows. + +.. _`ActivePython install`: http://www.activestate.com/activepython/downloads + +.. _`Jython does not create command line launchers`: http://bugs.jython.org/issue1491 + +- **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 in Jython. + +- **On Linux**: If ``easy_install`` fails because it needs to run + as the superuser you are trying to install things globally + and need to put ``sudo`` in front of the command. + + +.. _quickstart: test/quickstart.html + + +Recommendation: install tool and dependencies virtually =========================================================== It is recommended to work with virtual environments @@ -36,34 +76,9 @@ you need to run your tests. Local virtual Python environments (as opposed to system-wide "global" environments) make for a more reproducible and reliable test environment. - .. _`virtualenv`: http://pypi.python.org/pypi/virtualenv .. _`buildout`: http://www.buildout.org/ .. _pip: http://pypi.python.org/pypi/pip -.. _`easy_install`: - -using easy_install (from setuptools or Distribute) -=================================================== - -Both `Distribute`_ and setuptools_ provide the ``easy_install`` -installation tool. While setuptools should work ok with -Python2 interpreters, `Distribute`_ also works with Python3 -and it avoids some issues on Windows. In both cases you -can open a command line window and then type:: - - easy_install -U py - -to install the latest release of the py lib and py.test. The ``-U`` switch -will trigger an upgrade if you already have an older version installed. - -If you now type:: - - py.test --version - -you should see the version number and the import location of the tool. -Maybe you want to head on with the `quickstart`_ now? - -.. _quickstart: test/quickstart.html .. _standalone: @@ -84,24 +99,7 @@ disguise. You can tell people to download and then e.g. run it like this:: and ask them to send you the resulting URL. The resulting script has all core features and runs unchanged under Python2 and Python3 interpreters. -Troubleshooting / known issues -=============================== - -.. _`Jython does not create command line launchers`: http://bugs.jython.org/issue1491 - -**Jython2.5.1 on 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 in Jython. - -**On Linux**: If ``easy_install`` fails because it needs to run -as the superuser you are trying to install things globally -and need to put ``sudo`` in front of the command. - -**On Windows**: If "easy_install" or "py.test" are not found -please see here: `How do i run a Python program under Windows?`_ - -.. _`How do i run a Python program under Windows?`: http://www.python.org/doc/faq/windows/#how-do-i-run-a-python-program-under-windows +.. _`Python for Windows`: http://www.imladris.com/Scripts/PythonForWindows.html .. _mercurial: http://mercurial.selenic.com/wiki/ .. _`Distribute`: diff --git a/doc/test/plugin/capturelog.txt b/doc/test/plugin/capturelog.txt index 0725c39c2..8ca148e68 100644 --- a/doc/test/plugin/capturelog.txt +++ b/doc/test/plugin/capturelog.txt @@ -9,10 +9,10 @@ capture output of logging module. Installation ------------ -You can install the `pytest-capturelog pypi`_ package +You can install the `pytest-capturelog pypi`_ package with pip:: - pip install pytest-capturelog + pip install pytest-capturelog or with easy install:: diff --git a/doc/test/plugin/cov.txt b/doc/test/plugin/cov.txt index 4b91fdb55..3a2548b60 100644 --- a/doc/test/plugin/cov.txt +++ b/doc/test/plugin/cov.txt @@ -6,27 +6,39 @@ produce code coverage reports using the 'coverage' package, including support fo .. contents:: :local: -This plugin produces coverage reports using the coverage package. It -supports centralised testing and distributed testing in both load and -each modes. +This plugin produces coverage reports. It supports centralised testing and distributed testing in +both load and each modes. It also supports coverage of subprocesses. -All features offered by the coverage package should be available, -either through this plugin or through coverage's own config file. +All features offered by the coverage package should be available, either through pytest-cov or +through coverage's config file. Installation ------------ -The `pytest-cov pypi`_ package may be installed / uninstalled with pip:: +The `pytest-cov`_ package may be installed with pip or easy_install:: pip install pytest-cov - pip uninstall pytest-cov - -Alternatively easy_install can be used:: - easy_install pytest-cov -.. _`pytest-cov pypi`: http://pypi.python.org/pypi/pytest-cov/ +.. _`pytest-cov`: http://pypi.python.org/pypi/pytest-cov/ + + +Uninstallation +-------------- + +Uninstalling packages is supported by pip:: + + pip uninstall pytest-cov + +However easy_install does not provide an uninstall facility. + +.. IMPORTANT:: + + Ensure that you manually delete the init_cov_core.pth file in your site-packages directory. + + This file starts coverage collection of subprocesses if appropriate during site initialisation + at python startup. Usage @@ -35,6 +47,9 @@ Usage Centralised Testing ~~~~~~~~~~~~~~~~~~~ +Centralised testing will report on the combined coverage of the main process and all of it's +subprocesses. + Running centralised testing:: py.test --cov myproj tests/ @@ -42,150 +57,149 @@ Running centralised testing:: Shows a terminal report:: -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- - Name Stmts Exec Cover Missing - -------------------------------------------------- - myproj/__init__ 2 2 100% - myproj/myproj 257 244 94% 24-26, 99, 149, 233-236, 297-298, 369-370 - myproj/feature4286 94 87 92% 183-188, 197 - -------------------------------------------------- - TOTAL 353 333 94% + Name Stmts Miss Cover + ---------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% -Distributed Testing -~~~~~~~~~~~~~~~~~~~ +Distributed Testing: Load +~~~~~~~~~~~~~~~~~~~~~~~~~ -Distributed testing with dist mode set to load:: +Distributed testing with dist mode set to load will report on the combined coverage of all slaves. +The slaves may be spread out over any number of hosts and each slave may be located anywhere on the +file system. Each slave will have it's subprocesses measured. + +Running distributed testing with dist mode set to load:: py.test --cov myproj -n 2 tests/ -The results from the slaves will be combined like so:: +Shows a terminal report:: -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- - Name Stmts Exec Cover Missing - -------------------------------------------------- - myproj/__init__ 2 2 100% - myproj/myproj 257 244 94% 24-26, 99, 149, 233-236, 297-298, 369-370 - myproj/feature4286 94 87 92% 183-188, 197 - -------------------------------------------------- - TOTAL 353 333 94% + Name Stmts Miss Cover + ---------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% -Distributed testing in each mode:: +Again but spread over different hosts and different directories:: - py.test --cov myproj --dist=each - --tx=popen//python=/usr/local/python265/bin/python - --tx=popen//python=/usr/local/python27b1/bin/python + py.test --cov myproj --dist load + --tx ssh=memedough@host1//chdir=testenv1 + --tx ssh=memedough@host2//chdir=/tmp/testenv2//python=/tmp/env1/bin/python + --rsyncdir myproj --rsyncdir tests --rsync examples tests/ -Will produce a report for each slave:: +Shows a terminal report:: - -------------------- coverage: platform linux2, python 2.6.5-final-0 --------------------- - Name Stmts Exec Cover Missing - -------------------------------------------------- - myproj/__init__ 2 2 100% - myproj/myproj 257 244 94% 24-26, 99, 149, 233-236, 297-298, 369-370 - myproj/feature4286 94 87 92% 183-188, 197 - -------------------------------------------------- - TOTAL 353 333 94% - --------------------- coverage: platform linux2, python 2.7.0-beta-1 --------------------- - Name Stmts Exec Cover Missing - -------------------------------------------------- - myproj/__init__ 2 2 100% - myproj/myproj 257 244 94% 24-26, 99, 149, 233-236, 297-298, 369-370 - myproj/feature4286 94 87 92% 183-188, 197 - -------------------------------------------------- - TOTAL 353 333 94% + -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- + Name Stmts Miss Cover + ---------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% -Distributed testing in each mode can also produce a single combined -report. This is useful to get coverage information spanning things -such as all python versions:: +Distributed Testing: Each +~~~~~~~~~~~~~~~~~~~~~~~~~ - py.test --cov myproj --cov-combine-each --dist=each - --tx=popen//python=/usr/local/python265/bin/python - --tx=popen//python=/usr/local/python27b1/bin/python +Distributed testing with dist mode set to each will report on the combined coverage of all slaves. +Since each slave is running all tests this allows generating a combined coverage report for multiple +environments. + +Running distributed testing with dist mode set to each:: + + py.test --cov myproj --dist each + --tx popen//chdir=/tmp/testenv3//python=/usr/local/python27/bin/python + --tx ssh=memedough@host2//chdir=/tmp/testenv4//python=/tmp/env2/bin/python + --rsyncdir myproj --rsyncdir tests --rsync examples tests/ -Which looks like:: +Shows a terminal report:: ---------------------------------------- coverage ---------------------------------------- platform linux2, python 2.6.5-final-0 - platform linux2, python 2.7.0-beta-1 - Name Stmts Exec Cover Missing - -------------------------------------------------- - myproj/__init__ 2 2 100% - myproj/myproj 257 244 94% 24-26, 99, 149, 233-236, 297-298, 369-370 - myproj/feature4286 94 87 92% 183-188, 197 - -------------------------------------------------- - TOTAL 353 333 94% + platform linux2, python 2.7.0-final-0 + Name Stmts Miss Cover + ---------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% Reporting --------- -By default a terminal report is output. This report can be disabled -if desired, such as when results are going to a continuous integration -system and the terminal output won't be seen. +It is possible to generate any combination of the reports for a single test run. -In addition and without rerunning tests it is possible to generate -annotated source code, a html report and an xml report. +The available reports are terminal (with or without missing line numbers shown), HTML, XML and +annotated source code. -The directories for annotated source code and html reports can be -specified as can the file name for the xml report. +The terminal report without line numbers (default):: -Since testing often takes a non trivial amount of time at the end of -testing any / all of the reports may be generated. + py.test --cov-report term --cov myproj tests/ + + -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- + Name Stmts Miss Cover + ---------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% + + +The terminal report with line numbers:: + + py.test --cov-report term-missing --cov myproj tests/ + + -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- + Name Stmts Miss Cover Missing + -------------------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% 24-26, 99, 149, 233-236, 297-298, 369-370 + myproj/feature4286 94 7 92% 183-188, 197 + -------------------------------------------------- + TOTAL 353 20 94% + + +The remaining three reports output to files without showing anything on the terminal (useful for +when the output is going to a continuous integration server):: + + py.test --cov-report html --cov-report xml --cov-report annotate --cov myproj tests/ Coverage Data File ------------------ -During testing there may be many data files with coverage data. These -will have unique suffixes and will be combined at the end of testing. +The data file is erased at the beginning of testing to ensure clean data for each test run. -Upon completion, for --dist=load (and also for --dist=each when the ---cov-combine-each option is used) there will only be one data file. - -For --dist=each there may be many data files where each one will have -the platform / python version info appended to the name. - -These data files are left at the end of testing so that it is possible -to use normal coverage tools to examine them. - -At the beginning of testing any data files that are about to be used -will first be erased so ensure the data is clean for each test run. - -It is possible to set the name of the data file. If needed the -platform / python version will be appended automatically to this name. - - -Coverage Config File --------------------- - -Coverage by default will read its own config file. An alternative -file name may be specified or reading config can be disabled entirely. - -Care has been taken to ensure that the coverage env vars and config -file options work the same under this plugin as they do under coverage -itself. - -Since options may be specified in different ways the order of -precedence between pytest-cov and coverage from highest to lowest is: - -1. pytest command line -2. pytest env var -3. pytest conftest -4. coverage env var -5. coverage config file -6. coverage default +The data file is left at the end of testing so that it is possible to use normal coverage tools to +examine it. Limitations ----------- -For distributed testing the slaves must have the pytest-cov package -installed. This is needed since the plugin must be registered through -setuptools / distribute for pytest to start the plugin on the slave. +For distributed testing the slaves must have the pytest-cov package installed. This is needed since +the plugin must be registered through setuptools / distribute for pytest to start the plugin on the +slave. + +For subprocess measurement environment variables must make it from the main process to the +subprocess. The python used by the subprocess must have pytest-cov installed. The subprocess must +do normal site initialisation so that the environment variables can be detected and coverage +started. Acknowledgements @@ -193,14 +207,11 @@ Acknowledgements Holger Krekel for pytest with its distributed testing support. -Ned Batchelder for coverage and its ability to combine the coverage -results of parallel runs. +Ned Batchelder for coverage and its ability to combine the coverage results of parallel runs. -Whilst this plugin has been built fresh from the ground up to support -distributed testing it has been influenced by the work done on -pytest-coverage (Ross Lawley, James Mills, Holger Krekel) and -nose-cover (Jason Pellerin) which are other coverage plugins for -pytest and nose respectively. +Whilst this plugin has been built fresh from the ground up to support distributed testing it has +been influenced by the work done on pytest-coverage (Ross Lawley, James Mills, Holger Krekel) and +nose-cover (Jason Pellerin) which are other coverage plugins for pytest and nose respectively. No doubt others have contributed to these tools as well. @@ -208,43 +219,11 @@ command line options -------------------- -``--cov-on`` - enable coverage, only needed if not specifying any --cov options -``--cov=package`` - collect coverage for the specified package (multi-allowed) -``--cov-no-terminal`` - disable printing a report on the terminal -``--cov-annotate`` - generate an annotated source code report -``--cov-html`` - generate a html report -``--cov-xml`` - generate an xml report -``--cov-annotate-dir=dir`` - directory for the annotate report, default: %default -``--cov-html-dir=dir`` - directory for the html report, default: coverage_html -``--cov-xml-file=path`` - file for the xml report, default: coverage.xml -``--cov-data-file=path`` - file containing coverage data, default: .coverage -``--cov-combine-each`` - for dist=each mode produce a single combined report -``--cov-branch`` - enable branch coverage -``--cov-pylib`` - enable python library coverage -``--cov-timid`` - enable slower and simpler tracing -``--cov-no-missing-lines`` - disable showing missing lines, only relevant to the terminal report -``--cov-no-missing-files`` - disable showing message about missing source files -``--cov-omit=prefix1,prefix2,...`` - ignore files with these prefixes -``--cov-no-config`` - disable coverage reading its config file -``--cov-config-file=path`` - config file for coverage, default: %default +``--cov=path`` + measure coverage for filesystem path (multi-allowed) +``--cov-report=type`` + type of report to generate: term, term-missing, annotate, html, xml (multi-allowed) +``--cov-config=path`` + config file for coverage, default: .coveragerc .. include:: links.txt diff --git a/doc/test/plugin/coverage.txt b/doc/test/plugin/coverage.txt index e22bab43b..965b4a4ee 100644 --- a/doc/test/plugin/coverage.txt +++ b/doc/test/plugin/coverage.txt @@ -6,10 +6,27 @@ Write and report coverage data with the 'coverage' package. .. contents:: :local: -Original code by Ross Lawley. +Note: Original code by Ross Lawley. -Requires Ned Batchelder's excellent coverage: -http://nedbatchelder.com/code/coverage/ +Install +-------------- + +Use pip to (un)install:: + + pip install pytest-coverage + pip uninstall pytest-coverage + +or alternatively use easy_install to install:: + + easy_install pytest-coverage + + +Usage +------------- + +To get full test coverage reports for a particular package type:: + + py.test --cover-report=report command line options -------------------- @@ -21,8 +38,11 @@ command line options html: Directory for html output. report: Output a text report. annotate: Annotate your source code for which lines were executed and which were not. + xml: Output an xml report compatible with the cobertura plugin for hudson. ``--cover-directory=DIRECTORY`` Directory for the reports (html / annotate results) defaults to ./coverage +``--cover-xml-file=XML_FILE`` + File for the xml report defaults to ./coverage.xml ``--cover-show-missing`` Show missing files ``--cover-ignore-errors=IGNORE_ERRORS`` diff --git a/doc/test/plugin/figleaf.txt b/doc/test/plugin/figleaf.txt index c2fe6f0b3..86e0da65b 100644 --- a/doc/test/plugin/figleaf.txt +++ b/doc/test/plugin/figleaf.txt @@ -6,16 +6,29 @@ report test coverage using the 'figleaf' package. .. contents:: :local: +Install +--------------- + +To install the plugin issue:: + + easy_install pytest-figleaf # or + pip install pytest-figleaf + +and if you are using pip you can also uninstall:: + + pip uninstall pytest-figleaf + + Usage --------------- -after pip or easy_install mediated installation of ``pytest-figleaf`` you can type:: +After installation you can simply type:: py.test --figleaf [...] to enable figleaf coverage in your test run. A default ".figleaf" data file -and "html" directory will be created. You can use ``--fig-data`` -and ``fig-html`` to modify the paths. +and "html" directory will be created. You can use command line options +to control where data and html files are created. command line options -------------------- diff --git a/doc/test/plugin/helpconfig.txt b/doc/test/plugin/helpconfig.txt index 5c307a7bb..966bbc988 100644 --- a/doc/test/plugin/helpconfig.txt +++ b/doc/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`` diff --git a/doc/test/plugin/hookspec.txt b/doc/test/plugin/hookspec.txt index d56501d54..3f52d9bb4 100644 --- a/doc/test/plugin/hookspec.txt +++ b/doc/test/plugin/hookspec.txt @@ -7,67 +7,67 @@ hook specification sourcecode """ hook specifications for py.test plugins """ - + # ------------------------------------------------------------------------- # Command line and configuration # ------------------------------------------------------------------------- - + def pytest_namespace(): "return dict of name->object which will get stored at py.test. namespace" - + def pytest_addoption(parser): "add optparse-style options via parser.addoption." - + def pytest_addhooks(pluginmanager): "add hooks via pluginmanager.registerhooks(module)" - + def pytest_configure(config): """ called after command line options have been parsed. and all plugins and initial conftest files been loaded. """ - + def pytest_unconfigure(config): """ called before test process is exited. """ - + # ------------------------------------------------------------------------- # collection hooks # ------------------------------------------------------------------------- - + def pytest_ignore_collect(path, config): """ return true value to prevent considering this path for collection. This hook is consulted for all files and directories prior to considering collection hooks. """ pytest_ignore_collect.firstresult = True - + def pytest_collect_directory(path, parent): """ return Collection node or None for the given path. """ pytest_collect_directory.firstresult = True - + def pytest_collect_file(path, parent): """ return Collection node or None for the given path. """ - + def pytest_collectstart(collector): """ collector starts collecting. """ - + def pytest_collectreport(report): """ collector finished collecting. """ - + def pytest_deselected(items): """ called for test items deselected by keyword. """ - + def pytest_make_collect_report(collector): """ perform a collection and return a collection. """ pytest_make_collect_report.firstresult = True - + # XXX rename to item_collected()? meaning in distribution context? def pytest_itemstart(item, node=None): """ test item gets collected. """ - + # ------------------------------------------------------------------------- # Python test function related hooks # ------------------------------------------------------------------------- - + def pytest_pycollect_makemodule(path, parent): """ return a Module collector or None for the given path. This hook will be called for each matching test module path. @@ -75,105 +75,105 @@ hook specification sourcecode create test modules for files that do not match as a test module. """ pytest_pycollect_makemodule.firstresult = True - + def pytest_pycollect_makeitem(collector, name, obj): """ return custom item/collector for a python object in a module, or None. """ pytest_pycollect_makeitem.firstresult = True - + def pytest_pyfunc_call(pyfuncitem): """ call underlying test function. """ pytest_pyfunc_call.firstresult = True - + def pytest_generate_tests(metafunc): """ generate (multiple) parametrized calls to a test function.""" - + # ------------------------------------------------------------------------- # generic runtest related hooks # ------------------------------------------------------------------------- - + def pytest_runtest_protocol(item): """ implement fixture, run and report about the given test item. """ pytest_runtest_protocol.firstresult = True - + def pytest_runtest_setup(item): """ called before pytest_runtest_call(). """ - + def pytest_runtest_call(item): """ execute test item. """ - + def pytest_runtest_teardown(item): """ called after pytest_runtest_call(). """ - + def pytest_runtest_makereport(item, call): """ make a test report for the given item and call outcome. """ pytest_runtest_makereport.firstresult = True - + def pytest_runtest_logreport(report): """ process item test report. """ - + # special handling for final teardown - somewhat internal for now def pytest__teardown_final(session): """ called before test session finishes. """ pytest__teardown_final.firstresult = True - + def pytest__teardown_final_logerror(report): """ called if runtest_teardown_final failed. """ - + # ------------------------------------------------------------------------- # test session related hooks # ------------------------------------------------------------------------- - + def pytest_sessionstart(session): """ before session.main() is called. """ - + def pytest_sessionfinish(session, exitstatus): """ whole test run finishes. """ - + # ------------------------------------------------------------------------- # hooks for influencing reporting (invoked from pytest_terminal) # ------------------------------------------------------------------------- - + def pytest_report_header(config): """ return a string to be displayed as header info for terminal reporting.""" - + def pytest_report_teststatus(report): """ return result-category, shortletter and verbose word for reporting.""" pytest_report_teststatus.firstresult = True - + def pytest_terminal_summary(terminalreporter): """ add additional section in terminal summary reporting. """ - + def pytest_report_iteminfo(item): """ return (fspath, lineno, name) for the item. the information is used for result display and to sort tests """ pytest_report_iteminfo.firstresult = True - + # ------------------------------------------------------------------------- # doctest hooks # ------------------------------------------------------------------------- - + def pytest_doctest_prepare_content(content): """ return processed content for a given doctest""" pytest_doctest_prepare_content.firstresult = True - - + + # ------------------------------------------------------------------------- # error handling and internal debugging hooks # ------------------------------------------------------------------------- - + def pytest_plugin_registered(plugin, manager): """ a new py lib plugin got registered. """ - + def pytest_plugin_unregistered(plugin): """ a py lib plugin got unregistered. """ - + def pytest_internalerror(excrepr): """ called for internal errors. """ - + def pytest_keyboard_interrupt(excinfo): """ called for keyboard interrupt. """ - + def pytest_trace(category, msg): """ called for debug info. """ @@ -182,25 +182,25 @@ hook specification sourcecode .. sourcecode:: python - + def pytest_gwmanage_newgateway(gateway, platinfo): """ called on new raw gateway creation. """ - + def pytest_gwmanage_rsyncstart(source, gateways): """ called before rsyncing a directory to remote gateways takes place. """ - + def pytest_gwmanage_rsyncfinish(source, gateways): """ called after rsyncing a directory to remote gateways takes place. """ - + def pytest_configure_node(node): """ configure node information before it gets instantiated. """ - + def pytest_testnodeready(node): """ Test Node is ready to operate. """ - + def pytest_testnodedown(node, error): """ Test Node is down. """ - + def pytest_rescheduleitems(items): """ reschedule Items from a node that went down. """ diff --git a/doc/test/plugin/links.txt b/doc/test/plugin/links.txt index 568c74f29..b7d427856 100644 --- a/doc/test/plugin/links.txt +++ b/doc/test/plugin/links.txt @@ -1,47 +1,47 @@ .. _`helpconfig`: helpconfig.html -.. _`pytest_recwarn.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_recwarn.py +.. _`pytest_recwarn.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_recwarn.py .. _`unittest`: unittest.html -.. _`pytest_monkeypatch.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_monkeypatch.py -.. _`pytest_genscript.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_genscript.py +.. _`pytest_monkeypatch.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_monkeypatch.py +.. _`pytest_genscript.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_genscript.py .. _`pastebin`: pastebin.html .. _`skipping`: skipping.html .. _`genscript`: genscript.html .. _`plugins`: index.html .. _`mark`: mark.html .. _`tmpdir`: tmpdir.html -.. _`pytest_doctest.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_doctest.py +.. _`pytest_doctest.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_doctest.py .. _`capture`: capture.html -.. _`pytest_nose.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_nose.py -.. _`pytest_restdoc.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_restdoc.py +.. _`pytest_nose.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_nose.py +.. _`pytest_restdoc.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_restdoc.py .. _`restdoc`: restdoc.html .. _`xdist`: xdist.html -.. _`pytest_pastebin.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_pastebin.py -.. _`pytest_tmpdir.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_tmpdir.py +.. _`pytest_pastebin.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_pastebin.py +.. _`pytest_tmpdir.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_tmpdir.py .. _`terminal`: terminal.html -.. _`pytest_hooklog.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_hooklog.py +.. _`pytest_hooklog.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_hooklog.py .. _`capturelog`: capturelog.html .. _`junitxml`: junitxml.html -.. _`pytest_skipping.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_skipping.py +.. _`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 -.. _`pytest_helpconfig.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_helpconfig.py +.. _`pytest_helpconfig.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_helpconfig.py .. _`oejskit`: oejskit.html .. _`doctest`: doctest.html -.. _`pytest_mark.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_mark.py +.. _`pytest_mark.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_mark.py .. _`get in contact`: ../../contact.html -.. _`pytest_capture.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_capture.py +.. _`pytest_capture.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_capture.py .. _`figleaf`: figleaf.html .. _`customize`: ../customize.html .. _`hooklog`: hooklog.html -.. _`pytest_terminal.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_terminal.py +.. _`pytest_terminal.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_terminal.py .. _`recwarn`: recwarn.html -.. _`pytest_pdb.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_pdb.py +.. _`pytest_pdb.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_pdb.py .. _`monkeypatch`: monkeypatch.html .. _`coverage`: coverage.html .. _`resultlog`: resultlog.html .. _`cov`: cov.html -.. _`pytest_junitxml.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_junitxml.py +.. _`pytest_junitxml.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_junitxml.py .. _`django`: django.html -.. _`pytest_unittest.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_unittest.py +.. _`pytest_unittest.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_unittest.py .. _`nose`: nose.html -.. _`pytest_resultlog.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_resultlog.py +.. _`pytest_resultlog.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_resultlog.py .. _`pdb`: pdb.html diff --git a/doc/test/plugin/xdist.txt b/doc/test/plugin/xdist.txt index 5d05ccc91..4dbcc78c7 100644 --- a/doc/test/plugin/xdist.txt +++ b/doc/test/plugin/xdist.txt @@ -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/py/__init__.py b/py/__init__.py index 32fa85b41..4a55d189d 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -8,7 +8,7 @@ dictionary or an import path. (c) Holger Krekel and others, 2004-2010 """ -__version__ = version = "1.3.4a1" +__version__ = version = "1.4.0a1" import py.apipkg @@ -45,15 +45,9 @@ py.apipkg.initpkg(__name__, dict( 'Directory' : '._test.collect:Directory', 'File' : '._test.collect:File', 'Item' : '._test.collect:Item', - 'Module' : '._test.pycollect:Module', - 'Class' : '._test.pycollect:Class', - 'Instance' : '._test.pycollect:Instance', - 'Generator' : '._test.pycollect:Generator', - 'Function' : '._test.pycollect:Function', - '_fillfuncargs' : '._test.funcargs:fillfuncargs', }, 'cmdline': { - 'main' : '._test.cmdline:main', # backward compat + 'main' : '._test.session:main', # backward compat }, }, @@ -99,6 +93,7 @@ py.apipkg.initpkg(__name__, dict( '_AssertionError' : '._code.assertion:AssertionError', '_reinterpret_old' : '._code.assertion:reinterpret_old', '_reinterpret' : '._code.assertion:reinterpret', + '_reprcompare' : '._code.assertion:_reprcompare', }, # backports and additions of builtins @@ -111,6 +106,7 @@ py.apipkg.initpkg(__name__, dict( 'frozenset' : '._builtin:frozenset', 'BaseException' : '._builtin:BaseException', 'GeneratorExit' : '._builtin:GeneratorExit', + '_sysex' : '._builtin:_sysex', 'print_' : '._builtin:print_', '_reraise' : '._builtin:_reraise', '_tryimport' : '._builtin:_tryimport', diff --git a/py/_builtin.py b/py/_builtin.py index 8b7ca1421..a356db044 100644 --- a/py/_builtin.py +++ b/py/_builtin.py @@ -87,6 +87,8 @@ except NameError: pass GeneratorExit.__module__ = 'exceptions' +_sysex = (KeyboardInterrupt, SystemExit, MemoryError, GeneratorExit) + if sys.version_info >= (3, 0): exec ("print_ = print ; exec_=exec") import builtins diff --git a/py/_code/_assertionnew.py b/py/_code/_assertionnew.py index 0c7b0090e..c3d8df6aa 100644 --- a/py/_code/_assertionnew.py +++ b/py/_code/_assertionnew.py @@ -108,7 +108,7 @@ unary_map = { class DebugInterpreter(ast.NodeVisitor): - """Interpret AST nodes to gleam useful debugging information.""" + """Interpret AST nodes to gleam useful debugging information. """ def __init__(self, frame): self.frame = frame @@ -162,10 +162,7 @@ class DebugInterpreter(ast.NodeVisitor): def visit_Compare(self, comp): left = comp.left left_explanation, left_result = self.visit(left) - got_result = False for op, next_op in zip(comp.ops, comp.comparators): - if got_result and not result: - break next_explanation, next_result = self.visit(next_op) op_symbol = operator_map[op.__class__] explanation = "%s %s %s" % (left_explanation, op_symbol, @@ -177,9 +174,15 @@ class DebugInterpreter(ast.NodeVisitor): __exprinfo_right=next_result) except Exception: raise Failure(explanation) - else: - got_result = True + if not result: + break left_explanation, left_result = next_explanation, next_result + + rcomp = py.code._reprcompare + if rcomp: + res = rcomp(op_symbol, left_result, next_result) + if res: + explanation = res return explanation, result def visit_BoolOp(self, boolop): diff --git a/py/_code/_assertionold.py b/py/_code/_assertionold.py index 6df1d55d3..4e81fb3ef 100644 --- a/py/_code/_assertionold.py +++ b/py/_code/_assertionold.py @@ -3,7 +3,7 @@ import sys, inspect from compiler import parse, ast, pycodegen from py._code.assertion import BuiltinAssertionError, _format_explanation -passthroughex = (KeyboardInterrupt, SystemExit, MemoryError) +passthroughex = py.builtin._sysex class Failure: def __init__(self, node): diff --git a/py/_code/assertion.py b/py/_code/assertion.py index 2a2da9cfb..e77f250df 100644 --- a/py/_code/assertion.py +++ b/py/_code/assertion.py @@ -3,14 +3,23 @@ import py BuiltinAssertionError = py.builtin.builtins.AssertionError +_reprcompare = None # if set, will be called by assert reinterp for comparison ops def _format_explanation(explanation): - # uck! See CallFunc for where \n{ and \n} escape sequences are used + """This formats an explanation + + Normally all embedded newlines are escaped, however there are + three exceptions: \n{, \n} and \n~. The first two are intended + cover nested explanations, see function and attribute explanations + for examples (.visit_Call(), visit_Attribute()). The last one is + for when one explanation needs to span multiple lines, e.g. when + displaying diffs. + """ raw_lines = (explanation or '').split('\n') - # escape newlines not followed by { and } + # escape newlines not followed by {, } and ~ lines = [raw_lines[0]] for l in raw_lines[1:]: - if l.startswith('{') or l.startswith('}'): + if l.startswith('{') or l.startswith('}') or l.startswith('~'): lines.append(l) else: lines[-1] += '\\n' + l @@ -28,23 +37,25 @@ def _format_explanation(explanation): stackcnt[-1] += 1 stackcnt.append(0) result.append(' +' + ' '*(len(stack)-1) + s + line[1:]) - else: + elif line.startswith('}'): assert line.startswith('}') stack.pop() stackcnt.pop() result[stack[-1]] += line[1:] + else: + assert line.startswith('~') + result.append(' '*len(stack) + line[1:]) assert len(stack) == 1 return '\n'.join(result) class AssertionError(BuiltinAssertionError): - def __init__(self, *args): BuiltinAssertionError.__init__(self, *args) if args: try: self.msg = str(args[0]) - except (KeyboardInterrupt, SystemExit): + except py.builtin._sysex: raise except: self.msg = "<[broken __repr__] %s at %0xd>" %( diff --git a/py/_code/code.py b/py/_code/code.py index 4e8842b61..b40ef28c4 100644 --- a/py/_code/code.py +++ b/py/_code/code.py @@ -189,7 +189,7 @@ class TracebackEntry(object): """ try: return self.frame.eval("__tracebackhide__") - except (SystemExit, KeyboardInterrupt): + except py.builtin._sysex: raise except: return False @@ -354,9 +354,17 @@ class ExceptionInfo(object): abspath=False, tbfilter=True, funcargs=False): """ return str()able representation of this exception info. showlocals: show locals per traceback entry - style: long|short|no traceback style + style: long|short|no|native traceback style tbfilter: hide entries (where __tracebackhide__ is true) """ + if style == 'native': + import traceback + return ''.join(traceback.format_exception( + self.type, + self.value, + self.traceback[0]._rawentry, + )) + fmt = FormattedExcinfo(showlocals=showlocals, style=style, abspath=abspath, tbfilter=tbfilter, funcargs=funcargs) return fmt.repr_excinfo(self) diff --git a/py/_code/source.py b/py/_code/source.py index 52ab252a0..be2ca55d6 100644 --- a/py/_code/source.py +++ b/py/_code/source.py @@ -276,7 +276,7 @@ def getfslineno(obj): def findsource(obj): try: sourcelines, lineno = py.std.inspect.findsource(obj) - except (KeyboardInterrupt, SystemExit): + except py.builtin._sysex: raise except: return None, None diff --git a/py/_io/saferepr.py b/py/_io/saferepr.py index db52ca92b..afc968d3a 100644 --- a/py/_io/saferepr.py +++ b/py/_io/saferepr.py @@ -5,8 +5,6 @@ builtin_repr = repr reprlib = py.builtin._tryimport('repr', 'reprlib') -sysex = (KeyboardInterrupt, MemoryError, SystemExit) - class SafeRepr(reprlib.Repr): """ subclass of repr.Repr that limits the resulting size of repr() and includes information on exceptions raised during the call. @@ -21,7 +19,7 @@ class SafeRepr(reprlib.Repr): try: # Try the vanilla repr and make sure that the result is a string s = call(x, *args) - except sysex: + except py.builtin._sysex: raise except: cls, e, tb = sys.exc_info() diff --git a/py/_io/terminalwriter.py b/py/_io/terminalwriter.py index c140dbb64..982d5a661 100644 --- a/py/_io/terminalwriter.py +++ b/py/_io/terminalwriter.py @@ -26,7 +26,7 @@ def _getdimensions(): def get_terminal_width(): try: height, width = _getdimensions() - except (SystemExit, KeyboardInterrupt): + except py.builtin._sysex: raise except: # FALLBACK diff --git a/py/_path/local.py b/py/_path/local.py index 0e051eb8d..1765b80ff 100644 --- a/py/_path/local.py +++ b/py/_path/local.py @@ -519,6 +519,8 @@ class LocalPath(FSBase): pkg = __import__(pkgpath.basename, None, None, []) names = self.new(ext='').relto(pkgpath.dirpath()) names = names.split(self.sep) + if names and names[-1] == "__init__": + names.pop() modname = ".".join(names) else: # no package scope, still make it possible @@ -532,7 +534,8 @@ class LocalPath(FSBase): elif modfile.endswith('$py.class'): modfile = modfile[:-9] + '.py' if modfile.endswith("__init__.py"): - modfile = modfile[:-12] + if self.basename != "__init__.py": + modfile = modfile[:-12] if not self.samefile(modfile): raise self.ImportMismatchError(modname, modfile, self) return mod diff --git a/py/_plugin/hookspec.py b/py/_plugin/hookspec.py index 04e1bd0d2..02a310da5 100644 --- a/py/_plugin/hookspec.py +++ b/py/_plugin/hookspec.py @@ -20,6 +20,14 @@ def pytest_configure(config): and all plugins and initial conftest files been loaded. """ +def pytest_cmdline_main(config): + """ called for performing the main (cmdline) action. """ +pytest_cmdline_main.firstresult = True + +def pytest_runtest_mainloop(session): + """ called for performing the main runtest loop (after collection. """ +pytest_runtest_mainloop.firstresult = True + def pytest_unconfigure(config): """ called before test process is exited. """ @@ -27,6 +35,16 @@ def pytest_unconfigure(config): # collection hooks # ------------------------------------------------------------------------- +def pytest_perform_collection(session): + """ perform the collection protocol for the given session. """ +pytest_perform_collection.firstresult = True + +def pytest_collection_modifyitems(config, items): + """ called to allow filtering and selecting of test items (inplace). """ + +def pytest_log_finishcollection(collection): + """ called after collection has finished. """ + def pytest_ignore_collect(path, config): """ return true value to prevent considering this path for collection. This hook is consulted for all files and directories prior to considering @@ -41,9 +59,13 @@ pytest_collect_directory.firstresult = True def pytest_collect_file(path, parent): """ return Collection node or None for the given path. """ +# logging hooks for collection def pytest_collectstart(collector): """ collector starts collecting. """ +def pytest_log_itemcollect(item): + """ we just collected a test item. """ + def pytest_collectreport(report): """ collector finished collecting. """ @@ -54,10 +76,6 @@ def pytest_make_collect_report(collector): """ perform a collection and return a collection. """ pytest_make_collect_report.firstresult = True -# XXX rename to item_collected()? meaning in distribution context? -def pytest_itemstart(item, node=None): - """ test item gets collected. """ - # ------------------------------------------------------------------------- # Python test function related hooks # ------------------------------------------------------------------------- @@ -84,11 +102,16 @@ def pytest_generate_tests(metafunc): # ------------------------------------------------------------------------- # generic runtest related hooks # ------------------------------------------------------------------------- +def pytest_itemstart(item, node=None): + """ (deprecated, use pytest_runtest_logstart). """ def pytest_runtest_protocol(item): """ implement fixture, run and report about the given test item. """ pytest_runtest_protocol.firstresult = True +def pytest_runtest_logstart(nodeid, location, fspath): + """ signal the start of a test run. """ + def pytest_runtest_setup(item): """ called before pytest_runtest_call(). """ @@ -110,7 +133,7 @@ def pytest__teardown_final(session): """ called before test session finishes. """ pytest__teardown_final.firstresult = True -def pytest__teardown_final_logerror(report): +def pytest__teardown_final_logerror(report, session): """ called if runtest_teardown_final failed. """ # ------------------------------------------------------------------------- @@ -123,6 +146,20 @@ def pytest_sessionstart(session): def pytest_sessionfinish(session, exitstatus): """ whole test run finishes. """ + +# ------------------------------------------------------------------------- +# hooks for customising the assert methods +# ------------------------------------------------------------------------- + +def pytest_assertrepr_compare(config, op, left, right): + """return explanation for comparisons in failing assert expressions. + + Return None for no custom explanation, otherwise return a list + of strings. The strings will be joined by newlines but any newlines + *in* a string will be escaped. Note that all but the first line will + be indented sligthly, the intention is for the first line to be a summary. + """ + # ------------------------------------------------------------------------- # hooks for influencing reporting (invoked from pytest_terminal) # ------------------------------------------------------------------------- @@ -138,8 +175,10 @@ def pytest_terminal_summary(terminalreporter): """ add additional section in terminal summary reporting. """ def pytest_report_iteminfo(item): - """ return (fspath, lineno, name) for the item. - the information is used for result display and to sort tests + """ return (fspath, lineno, domainpath) location info for the item. + the information is used for result display and to sort tests. + fspath,lineno: file and linenumber of source of item definition. + domainpath: custom id - e.g. for python: dotted import address """ pytest_report_iteminfo.firstresult = True diff --git a/py/_plugin/pytest__pytest.py b/py/_plugin/pytest__pytest.py index 5d82edf63..f5846a3e5 100644 --- a/py/_plugin/pytest__pytest.py +++ b/py/_plugin/pytest__pytest.py @@ -87,6 +87,28 @@ class HookRecorder: l.append(call) return l + def contains(self, entries): + from py.builtin import print_ + i = 0 + entries = list(entries) + backlocals = py.std.sys._getframe(1).f_locals + while entries: + name, check = entries.pop(0) + for ind, call in enumerate(self.calls[i:]): + if call._name == name: + print_("NAMEMATCH", name, call) + if eval(check, backlocals, call.__dict__): + print_("CHECKERMATCH", repr(check), "->", call) + else: + print_("NOCHECKERMATCH", repr(check), "-", call) + continue + i += ind + 1 + break + print_("NONAMEMATCH", name, "with", call) + else: + raise AssertionError("could not find %r in %r" %( + name, self.calls[i:])) + def popcall(self, name): for i, call in enumerate(self.calls): if call._name == name: diff --git a/py/_plugin/pytest_assertion.py b/py/_plugin/pytest_assertion.py index f18350e7c..e9a04975f 100644 --- a/py/_plugin/pytest_assertion.py +++ b/py/_plugin/pytest_assertion.py @@ -8,15 +8,29 @@ def pytest_addoption(parser): help="disable python assert expression reinterpretation."), def pytest_configure(config): + # The _pytesthook attribute on the AssertionError is used by + # py._code._assertionnew to detect this plugin was loaded and in + # turn call the hooks defined here as part of the + # DebugInterpreter. if not config.getvalue("noassert") and not config.getvalue("nomagic"): warn_about_missing_assertion() config._oldassertion = py.builtin.builtins.AssertionError + config._oldbinrepr = py.code._reprcompare py.builtin.builtins.AssertionError = py.code._AssertionError + def callbinrepr(op, left, right): + hook_result = config.hook.pytest_assertrepr_compare( + config=config, op=op, left=left, right=right) + for new_expl in hook_result: + if new_expl: + return '\n~'.join(new_expl) + py.code._reprcompare = callbinrepr def pytest_unconfigure(config): if hasattr(config, '_oldassertion'): py.builtin.builtins.AssertionError = config._oldassertion + py.code._reprcompare = config._oldbinrepr del config._oldassertion + del config._oldbinrepr def warn_about_missing_assertion(): try: @@ -26,3 +40,109 @@ def warn_about_missing_assertion(): else: py.std.warnings.warn("Assertions are turned off!" " (are you using python -O?)") + + +# Provide basestring in python3 +try: + basestring = basestring +except NameError: + basestring = str + + +def pytest_assertrepr_compare(op, left, right): + """return specialised explanations for some operators/operands""" + left_repr = py.io.saferepr(left, maxsize=30) + right_repr = py.io.saferepr(right, maxsize=30) + summary = '%s %s %s' % (left_repr, op, right_repr) + + issequence = lambda x: isinstance(x, (list, tuple)) + istext = lambda x: isinstance(x, basestring) + isdict = lambda x: isinstance(x, dict) + isset = lambda x: isinstance(x, set) + + explanation = None + if op == '==': + if istext(left) and istext(right): + explanation = _diff_text(left, right) + elif issequence(left) and issequence(right): + explanation = _compare_eq_sequence(left, right) + elif isset(left) and isset(right): + explanation = _compare_eq_set(left, right) + elif isdict(left) and isdict(right): + explanation = _diff_text(py.std.pprint.pformat(left), + py.std.pprint.pformat(right)) + elif op == 'in': + pass # XXX + + if not explanation: + return None + + # Don't include pageloads of data, should be configurable + if len(''.join(explanation)) > 80*8: + explanation = ['Detailed information too verbose, truncated'] + + return [summary] + explanation + + +def _diff_text(left, right): + """Return the explanation for the diff between text + + This will skip leading and trailing characters which are + identical to keep the diff minimal. + """ + explanation = [] + for i in range(min(len(left), len(right))): + if left[i] != right[i]: + break + if i > 42: + i -= 10 # Provide some context + explanation = ['Skipping %s identical ' + 'leading characters in diff' % i] + left = left[i:] + right = right[i:] + if len(left) == len(right): + for i in range(len(left)): + if left[-i] != right[-i]: + break + if i > 42: + i -= 10 # Provide some context + explanation += ['Skipping %s identical ' + 'trailing characters in diff' % i] + left = left[:-i] + right = right[:-i] + explanation += [line.strip('\n') + for line in py.std.difflib.ndiff(left.splitlines(), + right.splitlines())] + return explanation + + +def _compare_eq_sequence(left, right): + explanation = [] + for i in range(min(len(left), len(right))): + if left[i] != right[i]: + explanation += ['First differing item %s: %s != %s' % + (i, left[i], right[i])] + break + if len(left) > len(right): + explanation += ['Left contains more items, ' + 'first extra item: %s' % left[len(right)]] + elif len(left) < len(right): + explanation += ['Right contains more items, ' + 'first extra item: %s' % right[len(left)]] + return explanation + _diff_text(py.std.pprint.pformat(left), + py.std.pprint.pformat(right)) + + +def _compare_eq_set(left, right): + explanation = [] + diff_left = left - right + diff_right = right - left + if diff_left: + explanation.append('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:') + for item in diff_right: + explanation.append(py.io.saferepr(item)) + return explanation diff --git a/py/_plugin/pytest_default.py b/py/_plugin/pytest_default.py index 2ced855a0..25e388d0f 100644 --- a/py/_plugin/pytest_default.py +++ b/py/_plugin/pytest_default.py @@ -3,30 +3,27 @@ import sys import py -def pytest_pyfunc_call(__multicall__, pyfuncitem): - if not __multicall__.execute(): - testfunction = pyfuncitem.obj - if pyfuncitem._isyieldedfunction(): - testfunction(*pyfuncitem._args) - else: - funcargs = pyfuncitem.funcargs - testfunction(**funcargs) +def pytest_cmdline_main(config): + from py._test.session import Session + return Session(config).main() -def pytest_collect_file(path, parent): - ext = path.ext - pb = path.purebasename - if pb.startswith("test_") or pb.endswith("_test") or \ - path in parent.config._argfspaths: - if ext == ".py": - return parent.ihook.pytest_pycollect_makemodule( - path=path, parent=parent) +def pytest_perform_collection(session): + collection = session.collection + assert not hasattr(collection, 'items') + hook = session.config.hook + collection.items = items = collection.perform_collect() + hook.pytest_collection_modifyitems(config=session.config, items=items) + hook.pytest_log_finishcollection(collection=collection) + return True -def pytest_pycollect_makemodule(path, parent): - return parent.Module(path, parent) - -def pytest_funcarg__pytestconfig(request): - """ the pytest config object with access to command line opts.""" - return request.config +def pytest_runtest_mainloop(session): + if session.config.option.collectonly: + return True + for item in session.collection.items: + item.config.hook.pytest_runtest_protocol(item=item) + if session.shouldstop: + raise session.Interrupted(session.shouldstop) + return True def pytest_ignore_collect(path, config): ignore_paths = config.getconftest_pathlist("collect_ignore", path=path) @@ -35,12 +32,6 @@ def pytest_ignore_collect(path, config): if excludeopt: ignore_paths.extend([py.path.local(x) for x in excludeopt]) return path in ignore_paths - # XXX more refined would be: - if ignore_paths: - for p in ignore_paths: - if path == p or path.relto(p): - return True - def pytest_collect_directory(path, parent): # XXX reconsider the following comment @@ -49,7 +40,7 @@ def pytest_collect_directory(path, parent): # define Directory(dir) already if not parent.recfilter(path): # by default special ".cvs", ... # check if cmdline specified this dir or a subdir directly - for arg in parent.config._argfspaths: + for arg in parent.collection._argfspaths: if path == arg or arg.relto(path): break else: @@ -68,12 +59,6 @@ def pytest_addoption(parser): group._addoption('--maxfail', metavar="num", action="store", type="int", dest="maxfail", default=0, help="exit after first num failures or errors.") - group._addoption('-k', - action="store", dest="keyword", default='', - help="only run test items matching the given " - "space separated keywords. precede a keyword with '-' to negate. " - "Terminate the expression with ':' to treat a match as a signal " - "to run all subsequent tests. ") group = parser.getgroup("collect", "collection") group.addoption('--collectonly', @@ -91,41 +76,7 @@ def pytest_addoption(parser): help="base temporary directory for this test run.") def pytest_configure(config): - setsession(config) # compat if config.getvalue("exitfirst"): config.option.maxfail = 1 -def setsession(config): - val = config.getvalue - if val("collectonly"): - from py._test.session import Session - config.setsessionclass(Session) - -# pycollect related hooks and code, should move to pytest_pycollect.py - -def pytest_pycollect_makeitem(__multicall__, collector, name, obj): - res = __multicall__.execute() - if res is not None: - return res - if collector._istestclasscandidate(name, obj): - res = collector._deprecated_join(name) - if res is not None: - return res - return collector.Class(name, parent=collector) - elif collector.funcnamefilter(name) and hasattr(obj, '__call__'): - res = collector._deprecated_join(name) - if res is not None: - return res - if is_generator(obj): - # XXX deprecation warning - return collector.Generator(name, parent=collector) - else: - return collector._genfunctions(name, obj) - -def is_generator(func): - try: - return py.code.getrawcode(func).co_flags & 32 # generator function - except AttributeError: # builtin functions have no bytecode - # assume them to not be generators - return False diff --git a/py/_plugin/pytest_doctest.py b/py/_plugin/pytest_doctest.py index 20f19b2a0..45ff0ac31 100644 --- a/py/_plugin/pytest_doctest.py +++ b/py/_plugin/pytest_doctest.py @@ -86,6 +86,9 @@ class DoctestItem(py.test.collect.Item): else: return super(DoctestItem, self).repr_failure(excinfo) + def reportinfo(self): + return self.fspath, None, "[doctest]" + class DoctestTextfile(DoctestItem): def runtest(self): if not self._deprecated_testexecution(): diff --git a/py/_plugin/pytest_genscript.py b/py/_plugin/pytest_genscript.py index 3cb710c0c..bd72aadc5 100755 --- a/py/_plugin/pytest_genscript.py +++ b/py/_plugin/pytest_genscript.py @@ -11,7 +11,7 @@ def pytest_addoption(parser): dest="genscript", metavar="path", help="create standalone py.test script at given target path.") -def pytest_configure(config): +def pytest_cmdline_main(config): genscript = config.getvalue("genscript") if genscript: import py @@ -20,7 +20,7 @@ def pytest_configure(config): pybasedir = py.path.local(py.__file__).dirpath().dirpath() genscript = py.path.local(genscript) main(pybasedir, outfile=genscript, infile=infile) - raise SystemExit(0) + return 0 def main(pybasedir, outfile, infile): import base64 diff --git a/py/_plugin/pytest_helpconfig.py b/py/_plugin/pytest_helpconfig.py index 3bb51bbcc..18c810f70 100644 --- a/py/_plugin/pytest_helpconfig.py +++ b/py/_plugin/pytest_helpconfig.py @@ -23,15 +23,18 @@ def pytest_addoption(parser): help="show available conftest.py and ENV-variable names.") -def pytest_configure(__multicall__, config): +def pytest_cmdline_main(config): if config.option.version: p = py.path.local(py.__file__).dirpath() sys.stderr.write("This is py.test version %s, imported from %s\n" % (py.__version__, p)) - sys.exit(0) - if not config.option.helpconfig: - return - __multicall__.execute() + return 0 + elif config.option.helpconfig: + config.pluginmanager.do_configure(config) + showpluginhelp(config) + return 0 + +def showpluginhelp(config): options = [] for group in config._parser._groups: options.extend(group.options) @@ -65,9 +68,7 @@ def pytest_configure(__multicall__, config): help, ) tw.line(line[:tw.fullwidth]) - tw.sep("-") - sys.exit(0) conftest_options = ( ('pytest_plugins', 'list of plugin names to load'), diff --git a/py/_plugin/pytest_junitxml.py b/py/_plugin/pytest_junitxml.py index fc1df8fc4..48b716f9d 100644 --- a/py/_plugin/pytest_junitxml.py +++ b/py/_plugin/pytest_junitxml.py @@ -37,12 +37,9 @@ class LogXML(object): self._durations = {} def _opentestcase(self, report): - if hasattr(report, 'item'): - node = report.item - else: - node = report.collector - d = {'time': self._durations.pop(node, "0")} - names = [x.replace(".py", "") for x in node.listnames() if x != "()"] + names = report.nodenames + d = {'time': self._durations.pop(names, "0")} + names = [x.replace(".py", "") for x in names if x != "()"] classnames = names[:-1] if self.prefix: classnames.insert(0, self.prefix) @@ -122,11 +119,12 @@ class LogXML(object): self.append_skipped(report) def pytest_runtest_call(self, item, __multicall__): + names = tuple(item.listnames()) start = time.time() try: return __multicall__.execute() finally: - self._durations[item] = time.time() - start + self._durations[names] = time.time() - start def pytest_collectreport(self, report): if not report.passed: diff --git a/py/_plugin/pytest_keyword.py b/py/_plugin/pytest_keyword.py new file mode 100644 index 000000000..68d0f5d03 --- /dev/null +++ b/py/_plugin/pytest_keyword.py @@ -0,0 +1,65 @@ + +def pytest_addoption(parser): + group = parser.getgroup("general") + group._addoption('-k', + action="store", dest="keyword", default='', + help="only run test items matching the given " + "space separated keywords. precede a keyword with '-' to negate. " + "Terminate the expression with ':' to treat a match as a signal " + "to run all subsequent tests. ") + +def pytest_collection_modifyitems(items, config): + keywordexpr = config.option.keyword + if not keywordexpr: + return + selectuntil = False + if keywordexpr[-1] == ":": + selectuntil = True + keywordexpr = keywordexpr[:-1] + + remaining = [] + deselected = [] + for colitem in items: + if keywordexpr and skipbykeyword(colitem, keywordexpr): + deselected.append(colitem) + else: + remaining.append(colitem) + if selectuntil: + keywordexpr = None + + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = remaining + +def skipbykeyword(colitem, keywordexpr): + """ return True if they given keyword expression means to + skip this collector/item. + """ + if not keywordexpr: + return + chain = colitem.listchain() + for key in filter(None, keywordexpr.split()): + eor = key[:1] == '-' + if eor: + key = key[1:] + if not (eor ^ matchonekeyword(key, chain)): + return True + +def matchonekeyword(key, chain): + elems = key.split(".") + # XXX O(n^2), anyone cares? + chain = [item.keywords for item in chain if item.keywords] + for start, _ in enumerate(chain): + if start + len(elems) > len(chain): + return False + for num, elem in enumerate(elems): + for keyword in chain[num + start]: + ok = False + if elem in keyword: + ok = True + break + if not ok: + break + if num == len(elems) - 1 and ok: + return True + return False diff --git a/py/_plugin/pytest_monkeypatch.py b/py/_plugin/pytest_monkeypatch.py index 37fc02e8a..6dc3e59e0 100644 --- a/py/_plugin/pytest_monkeypatch.py +++ b/py/_plugin/pytest_monkeypatch.py @@ -53,7 +53,7 @@ you start monkeypatching after the undo call. .. _`monkeypatch blog post`: http://tetamap.wordpress.com/2009/03/03/monkeypatching-in-unit-tests-done-right/ """ -import py, os, sys +import os, sys def pytest_funcarg__monkeypatch(request): """The returned ``monkeypatch`` funcarg provides these diff --git a/py/_plugin/pytest_pytester.py b/py/_plugin/pytest_pytester.py index d96c4c802..fbf47e872 100644 --- a/py/_plugin/pytest_pytester.py +++ b/py/_plugin/pytest_pytester.py @@ -7,13 +7,14 @@ import sys, os import re import inspect import time +from fnmatch import fnmatch from py._test.config import Config as pytestConfig from py.builtin import print_ def pytest_addoption(parser): group = parser.getgroup("pylib") - group.addoption('--tools-on-path', - action="store_true", dest="toolsonpath", default=False, + group.addoption('--no-tools-on-path', + action="store_true", dest="notoolsonpath", default=False, help=("discover tools on PATH instead of going through py.cmdline.") ) @@ -74,10 +75,8 @@ class TmpTestdir: def __repr__(self): return "" % (self.tmpdir,) - def Config(self, topdir=None): - if topdir is None: - topdir = self.tmpdir.dirpath() - return pytestConfig(topdir=topdir) + def Config(self): + return pytestConfig() def finalize(self): for p in self._syspathremove: @@ -149,16 +148,23 @@ class TmpTestdir: p.ensure("__init__.py") return p + def getnode(self, config, arg): + from py._test.session import Collection + collection = Collection(config) + return collection.getbyid(collection._normalizearg(arg))[0] + def genitems(self, colitems): - return list(self.session.genitems(colitems)) + collection = colitems[0].collection + result = [] + collection.genitems(colitems, (), result) + return result def inline_genitems(self, *args): #config = self.parseconfig(*args) - config = self.parseconfig(*args) - session = config.initsession() + from py._test.session import Collection + config = self.parseconfigure(*args) rec = self.getreportrecorder(config) - colitems = [config.getnode(arg) for arg in config.args] - items = list(session.genitems(colitems)) + items = Collection(config).perform_collect() return items, rec def runitem(self, source): @@ -187,12 +193,10 @@ class TmpTestdir: def inline_run(self, *args): args = ("-s", ) + args # otherwise FD leakage config = self.parseconfig(*args) - config.pluginmanager.do_configure(config) - session = config.initsession() reprec = self.getreportrecorder(config) - colitems = config.getinitialnodes() - session.main(colitems) - config.pluginmanager.do_unconfigure(config) + #config.pluginmanager.do_configure(config) + config.hook.pytest_cmdline_main(config=config) + #config.pluginmanager.do_unconfigure(config) return reprec def config_preparse(self): @@ -245,29 +249,17 @@ class TmpTestdir: def getitems(self, source): modcol = self.getmodulecol(source) - return list(modcol.config.initsession().genitems([modcol])) - #assert item is not None, "%r item not found in module:\n%s" %(funcname, source) - #return item - - def getfscol(self, path, configargs=()): - self.config = self.parseconfig(path, *configargs) - self.session = self.config.initsession() - return self.config.getnode(path) + return self.genitems([modcol]) def getmodulecol(self, source, configargs=(), withinit=False): kw = {self.request.function.__name__: py.code.Source(source).strip()} path = self.makepyfile(**kw) if withinit: self.makepyfile(__init__ = "#") - self.config = self.parseconfig(path, *configargs) - self.session = self.config.initsession() - #self.config.pluginmanager.do_configure(config=self.config) - # XXX - self.config.pluginmanager.import_plugin("runner") - plugin = self.config.pluginmanager.getplugin("runner") - plugin.pytest_configure(config=self.config) - - return self.config.getnode(path) + self.config = config = self.parseconfigure(path, *configargs) + node = self.getnode(config, path) + #config.pluginmanager.do_unconfigure(config) + return node def popen(self, cmdargs, stdout, stderr, **kw): if not hasattr(py.std, 'subprocess'): @@ -314,7 +306,7 @@ class TmpTestdir: return self.run(*fullargs) def _getpybinargs(self, scriptname): - if self.request.config.getvalue("toolsonpath"): + if not self.request.config.getvalue("notoolsonpath"): script = py.path.local.sysfind(scriptname) assert script, "script %r not found" % scriptname return (script,) @@ -334,7 +326,7 @@ class TmpTestdir: return self.run(sys.executable, script) def _getsysprepend(self): - if not self.request.config.getvalue("toolsonpath"): + if self.request.config.getvalue("notoolsonpath"): s = "import sys;sys.path.insert(0,%r);" % str(py._pydir.dirpath()) else: s = "" @@ -360,8 +352,8 @@ class TmpTestdir: def spawn_pytest(self, string, expect_timeout=10.0): pexpect = py.test.importorskip("pexpect", "2.4") - if not self.request.config.getvalue("toolsonpath"): - py.test.skip("need --tools-on-path to run py.test script") + if self.request.config.getvalue("notoolsonpath"): + py.test.skip("--no-tools-on-path prevents running pexpect-spawn tests") basetemp = self.tmpdir.mkdir("pexpect") invoke = self._getpybinargs("py.test")[0] cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string) @@ -405,8 +397,7 @@ class ReportRecorder(object): """ return a testreport whose dotted import path matches """ l = [] for rep in self.getreports(names=names): - colitem = rep.getnode() - if not inamepart or inamepart in colitem.listnames(): + if not inamepart or inamepart in rep.nodenames: l.append(rep) if not l: raise ValueError("could not find test report matching %r: no test reports at all!" % @@ -474,13 +465,25 @@ class LineMatcher: def str(self): return "\n".join(self.lines) - def fnmatch_lines(self, lines2): + def _getlines(self, lines2): if isinstance(lines2, str): lines2 = py.code.Source(lines2) if isinstance(lines2, py.code.Source): lines2 = lines2.strip().lines + return lines2 - from fnmatch import fnmatch + def fnmatch_lines_random(self, lines2): + lines2 = self._getlines(lines2) + for line in lines2: + for x in self.lines: + if line == x or fnmatch(x, line): + print_("matched: ", repr(line)) + break + else: + raise ValueError("line %r not found in output" % line) + + def fnmatch_lines(self, lines2): + lines2 = self._getlines(lines2) lines1 = self.lines[:] nextline = None extralines = [] diff --git a/py/_test/pycollect.py b/py/_plugin/pytest_python.py similarity index 54% rename from py/_test/pycollect.py rename to py/_plugin/pytest_python.py index 720e2f1c7..337d4919f 100644 --- a/py/_test/pycollect.py +++ b/py/_plugin/pytest_python.py @@ -5,9 +5,80 @@ import py import inspect import sys from py._test.collect import configproperty, warnoldcollect -from py._test import funcargs from py._code.code import TerminalRepr +def pytest_addoption(parser): + group = parser.getgroup("terminal reporting") + group._addoption('--funcargs', + action="store_true", dest="showfuncargs", default=False, + help="show available function arguments, sorted by plugin") + +def pytest_cmdline_main(config): + if config.option.showfuncargs: + showfuncargs(config) + return 0 + +def pytest_namespace(): + # XXX rather return than set directly + py.test.collect.Module = Module + py.test.collect.Class = Class + py.test.collect.Instance = Instance + py.test.collect.Function = Function + py.test.collect.Generator = Generator + py.test.collect._fillfuncargs = fillfuncargs + +def pytest_funcarg__pytestconfig(request): + """ the pytest config object with access to command line opts.""" + return request.config + +def pytest_pyfunc_call(__multicall__, pyfuncitem): + if not __multicall__.execute(): + testfunction = pyfuncitem.obj + if pyfuncitem._isyieldedfunction(): + testfunction(*pyfuncitem._args) + else: + funcargs = pyfuncitem.funcargs + testfunction(**funcargs) + +def pytest_collect_file(path, parent): + ext = path.ext + pb = path.purebasename + if pb.startswith("test_") or pb.endswith("_test") or \ + path in parent.collection._argfspaths: + if ext == ".py": + return parent.ihook.pytest_pycollect_makemodule( + path=path, parent=parent) + +def pytest_pycollect_makemodule(path, parent): + return parent.Module(path, parent) + + +def pytest_pycollect_makeitem(__multicall__, collector, name, obj): + res = __multicall__.execute() + if res is not None: + return res + if collector._istestclasscandidate(name, obj): + res = collector._deprecated_join(name) + if res is not None: + return res + return collector.Class(name, parent=collector) + elif collector.funcnamefilter(name) and hasattr(obj, '__call__'): + res = collector._deprecated_join(name) + if res is not None: + return res + if is_generator(obj): + # XXX deprecation warning + return collector.Generator(name, parent=collector) + else: + return collector._genfunctions(name, obj) + +def is_generator(func): + try: + return py.code.getrawcode(func).co_flags & 32 # generator function + except AttributeError: # builtin functions have no bytecode + # assume them to not be generators + return False + class PyobjMixin(object): def obj(): def fget(self): @@ -120,10 +191,10 @@ class PyCollectorMixin(PyobjMixin, py.test.collect.Collector): module = self.getparent(Module).obj clscol = self.getparent(Class) cls = clscol and clscol.obj or None - metafunc = funcargs.Metafunc(funcobj, config=self.config, + metafunc = Metafunc(funcobj, config=self.config, cls=cls, module=module) gentesthook = self.config.hook.pytest_generate_tests - plugins = funcargs.getplugins(self, withpy=True) + plugins = getplugins(self, withpy=True) gentesthook.pcall(plugins, metafunc=metafunc) if not metafunc._calls: return self.Function(name, parent=self) @@ -212,16 +283,9 @@ class Class(PyCollectorMixin, py.test.collect.Collector): class Instance(PyCollectorMixin, py.test.collect.Collector): def _getobj(self): return self.parent.obj() - def Function(self): - return getattr(self.obj, 'Function', - PyCollectorMixin.Function.__get__(self)) # XXX for python 2.2 + def _keywords(self): return [] - Function = property(Function) - - #def __repr__(self): - # return "<%s of '%s'>" %(self.__class__.__name__, - # self.parent.obj.__name__) def newinstance(self): self.obj = self._getobj() @@ -270,7 +334,7 @@ class FunctionMixin(PyobjMixin): return traceback def _repr_failure_py(self, excinfo, style="long"): - if excinfo.errisinstance(funcargs.FuncargRequest.LookupError): + if excinfo.errisinstance(FuncargRequest.LookupError): fspath, lineno, msg = self.reportinfo() lines, _ = inspect.getsourcelines(self.obj) for i, line in enumerate(lines): @@ -348,8 +412,9 @@ class Function(FunctionMixin, py.test.collect.Item): """ _genid = None def __init__(self, name, parent=None, args=None, config=None, - callspec=None, callobj=_dummy): - super(Function, self).__init__(name, parent, config=config) + callspec=None, callobj=_dummy, collection=None): + super(Function, self).__init__(name, parent, + config=config, collection=collection) self._args = args if self._isyieldedfunction(): assert not callspec, "yielded functions (deprecated) cannot have funcargs" @@ -383,7 +448,7 @@ class Function(FunctionMixin, py.test.collect.Item): def setup(self): super(Function, self).setup() if hasattr(self, 'funcargs'): - funcargs.fillfuncargs(self) + fillfuncargs(self) def __eq__(self, other): try: @@ -409,3 +474,229 @@ def hasinit(obj): if init: if init != object.__init__: return True + + +def getfuncargnames(function): + argnames = py.std.inspect.getargs(py.code.getrawcode(function))[0] + startindex = py.std.inspect.ismethod(function) and 1 or 0 + defaults = getattr(function, 'func_defaults', + getattr(function, '__defaults__', None)) or () + numdefaults = len(defaults) + if numdefaults: + return argnames[startindex:-numdefaults] + return argnames[startindex:] + +def fillfuncargs(function): + """ fill missing funcargs. """ + request = FuncargRequest(pyfuncitem=function) + request._fillfuncargs() + +def getplugins(node, withpy=False): # might by any node + plugins = node.config._getmatchingplugins(node.fspath) + if withpy: + mod = node.getparent(py.test.collect.Module) + if mod is not None: + plugins.append(mod.obj) + inst = node.getparent(py.test.collect.Instance) + if inst is not None: + plugins.append(inst.obj) + return plugins + +_notexists = object() +class CallSpec: + def __init__(self, funcargs, id, param): + self.funcargs = funcargs + self.id = id + if param is not _notexists: + self.param = param + def __repr__(self): + return "" %( + self.id, getattr(self, 'param', '?'), self.funcargs) + +class Metafunc: + def __init__(self, function, config=None, cls=None, module=None): + self.config = config + self.module = module + self.function = function + self.funcargnames = getfuncargnames(function) + self.cls = cls + self.module = module + self._calls = [] + self._ids = py.builtin.set() + + def addcall(self, funcargs=None, id=_notexists, param=_notexists): + assert funcargs is None or isinstance(funcargs, dict) + if id is None: + raise ValueError("id=None not allowed") + if id is _notexists: + id = len(self._calls) + id = str(id) + if id in self._ids: + raise ValueError("duplicate id %r" % id) + self._ids.add(id) + self._calls.append(CallSpec(funcargs, id, param)) + +class FuncargRequest: + _argprefix = "pytest_funcarg__" + _argname = None + + class LookupError(LookupError): + """ error on performing funcarg request. """ + + def __init__(self, pyfuncitem): + self._pyfuncitem = pyfuncitem + self.function = pyfuncitem.obj + self.module = pyfuncitem.getparent(py.test.collect.Module).obj + clscol = pyfuncitem.getparent(py.test.collect.Class) + self.cls = clscol and clscol.obj or None + self.instance = py.builtin._getimself(self.function) + self.config = pyfuncitem.config + self.fspath = pyfuncitem.fspath + if hasattr(pyfuncitem, '_requestparam'): + self.param = pyfuncitem._requestparam + self._plugins = getplugins(pyfuncitem, withpy=True) + self._funcargs = self._pyfuncitem.funcargs.copy() + self._name2factory = {} + self._currentarg = None + + def _fillfuncargs(self): + argnames = getfuncargnames(self.function) + if argnames: + assert not getattr(self._pyfuncitem, '_args', None), ( + "yielded functions cannot have funcargs") + for argname in argnames: + if argname not in self._pyfuncitem.funcargs: + self._pyfuncitem.funcargs[argname] = self.getfuncargvalue(argname) + + + def applymarker(self, marker): + """ apply a marker to a test function invocation. + + The 'marker' must be created with py.test.mark.* XYZ. + """ + if not isinstance(marker, py.test.mark.XYZ.__class__): + raise ValueError("%r is not a py.test.mark.* object") + self._pyfuncitem.keywords[marker.markname] = marker + + def cached_setup(self, setup, teardown=None, scope="module", extrakey=None): + """ cache and return result of calling setup(). + + The requested argument name, the scope and the ``extrakey`` + determine the cache key. The scope also determines when + teardown(result) will be called. valid scopes are: + scope == 'function': when the single test function run finishes. + scope == 'module': when tests in a different module are run + scope == 'session': when tests of the session have run. + """ + if not hasattr(self.config, '_setupcache'): + self.config._setupcache = {} # XXX weakref? + cachekey = (self._currentarg, self._getscopeitem(scope), extrakey) + cache = self.config._setupcache + try: + val = cache[cachekey] + except KeyError: + val = setup() + cache[cachekey] = val + if teardown is not None: + def finalizer(): + del cache[cachekey] + teardown(val) + self._addfinalizer(finalizer, scope=scope) + return val + + def getfuncargvalue(self, argname): + try: + return self._funcargs[argname] + except KeyError: + pass + if argname not in self._name2factory: + self._name2factory[argname] = self.config.pluginmanager.listattr( + plugins=self._plugins, + attrname=self._argprefix + str(argname) + ) + #else: we are called recursively + if not self._name2factory[argname]: + self._raiselookupfailed(argname) + funcargfactory = self._name2factory[argname].pop() + oldarg = self._currentarg + self._currentarg = argname + try: + self._funcargs[argname] = res = funcargfactory(request=self) + finally: + self._currentarg = oldarg + return res + + def _getscopeitem(self, scope): + if scope == "function": + return self._pyfuncitem + elif scope == "module": + return self._pyfuncitem.getparent(py.test.collect.Module) + elif scope == "session": + return None + raise ValueError("unknown finalization scope %r" %(scope,)) + + def _addfinalizer(self, finalizer, scope): + colitem = self._getscopeitem(scope) + self.config._setupstate.addfinalizer( + finalizer=finalizer, colitem=colitem) + + def addfinalizer(self, finalizer): + """ call the given finalizer after test function finished execution. """ + self._addfinalizer(finalizer, scope="function") + + def __repr__(self): + return "" %(self._pyfuncitem) + + def _raiselookupfailed(self, argname): + available = [] + for plugin in self._plugins: + for name in vars(plugin): + if name.startswith(self._argprefix): + name = name[len(self._argprefix):] + if name not in available: + available.append(name) + fspath, lineno, msg = self._pyfuncitem.reportinfo() + msg = "LookupError: no factory found for function argument %r" % (argname,) + msg += "\n available funcargs: %s" %(", ".join(available),) + msg += "\n use 'py.test --funcargs [testpath]' for help on them." + raise self.LookupError(msg) + +def showfuncargs(config): + from py._test.session import Collection + collection = Collection(config) + colitem = collection.getinitialnodes()[0] + curdir = py.path.local() + tw = py.io.TerminalWriter() + plugins = getplugins(colitem, withpy=True) + verbose = config.getvalue("verbose") + for plugin in plugins: + available = [] + for name, factory in vars(plugin).items(): + if name.startswith(FuncargRequest._argprefix): + name = name[len(FuncargRequest._argprefix):] + if name not in available: + available.append([name, factory]) + if available: + pluginname = plugin.__name__ + for name, factory in available: + loc = getlocation(factory, curdir) + if verbose: + funcargspec = "%s -- %s" %(name, loc,) + else: + funcargspec = name + tw.line(funcargspec, green=True) + doc = factory.__doc__ or "" + if doc: + for line in doc.split("\n"): + tw.line(" " + line.strip()) + else: + tw.line(" %s: no docstring available" %(loc,), + red=True) + +def getlocation(function, curdir): + import inspect + fn = py.path.local(inspect.getfile(function)) + lineno = py.builtin._getcode(function).co_firstlineno + if fn.relto(curdir): + fn = fn.relto(curdir) + return "%s:%d" %(fn, lineno+1) diff --git a/py/_plugin/pytest_resultlog.py b/py/_plugin/pytest_resultlog.py index 7f9292075..09b9213a2 100644 --- a/py/_plugin/pytest_resultlog.py +++ b/py/_plugin/pytest_resultlog.py @@ -57,21 +57,20 @@ class ResultLog(object): self.config = config self.logfile = logfile # preferably line buffered - def write_log_entry(self, testpath, shortrepr, longrepr): - print_("%s %s" % (shortrepr, testpath), file=self.logfile) + def write_log_entry(self, testpath, lettercode, longrepr): + print_("%s %s" % (lettercode, testpath), file=self.logfile) for line in longrepr.splitlines(): print_(" %s" % line, file=self.logfile) - def log_outcome(self, node, shortrepr, longrepr): - testpath = generic_path(node) - self.write_log_entry(testpath, shortrepr, longrepr) + def log_outcome(self, report, lettercode, longrepr): + testpath = getattr(report, 'nodeid', None) + if testpath is None: + testpath = report.fspath + self.write_log_entry(testpath, lettercode, longrepr) def pytest_runtest_logreport(self, report): res = self.config.hook.pytest_report_teststatus(report=report) - if res is not None: - code = res[1] - else: - code = report.shortrepr + code = res[1] if code == 'x': longrepr = str(report.longrepr) elif code == 'X': @@ -82,7 +81,7 @@ class ResultLog(object): longrepr = str(report.longrepr) elif report.skipped: longrepr = str(report.longrepr.reprcrash.message) - self.log_outcome(report.item, code, longrepr) + self.log_outcome(report, code, longrepr) def pytest_collectreport(self, report): if not report.passed: @@ -92,7 +91,7 @@ class ResultLog(object): assert report.skipped code = "S" longrepr = str(report.longrepr.reprcrash) - self.log_outcome(report.collector, code, longrepr) + self.log_outcome(report, code, longrepr) def pytest_internalerror(self, excrepr): path = excrepr.reprcrash.path diff --git a/py/_plugin/pytest_runner.py b/py/_plugin/pytest_runner.py index 784c13402..7f7672b49 100644 --- a/py/_plugin/pytest_runner.py +++ b/py/_plugin/pytest_runner.py @@ -3,6 +3,7 @@ collect and run test items and create reports. """ import py, sys +from py._code.code import TerminalRepr def pytest_namespace(): return { @@ -26,19 +27,35 @@ def pytest_sessionfinish(session, exitstatus): hook = session.config.hook rep = hook.pytest__teardown_final(session=session) if rep: - hook.pytest__teardown_final_logerror(report=rep) + hook.pytest__teardown_final_logerror(session=session, report=rep) + session.exitstatus = 1 -def pytest_make_collect_report(collector): - result = excinfo = None +class NodeInfo: + def __init__(self, nodeid, nodenames, fspath, location): + self.nodeid = nodeid + self.nodenames = nodenames + self.fspath = fspath + self.location = location + +def getitemnodeinfo(item): try: - result = collector._memocollect() - except KeyboardInterrupt: - raise - except: - excinfo = py.code.ExceptionInfo() - return CollectReport(collector, result, excinfo) - + return item._nodeinfo + except AttributeError: + location = item.ihook.pytest_report_iteminfo(item=item) + location = (str(location[0]), location[1], str(location[2])) + nodenames = tuple(item.listnames()) + nodeid = item.collection.getid(item) + fspath = item.fspath + item._nodeinfo = n = NodeInfo(nodeid, nodenames, fspath, location) + return n + def pytest_runtest_protocol(item): + nodeinfo = getitemnodeinfo(item) + item.ihook.pytest_runtest_logstart( + nodeid=nodeinfo.nodeid, + location=nodeinfo.location, + fspath=str(item.fspath), + ) runtestprotocol(item) return True @@ -57,9 +74,6 @@ def pytest_runtest_call(item): if not item._deprecated_testexecution(): item.runtest() -def pytest_runtest_makereport(item, call): - return ItemTestReport(item, call.excinfo, call.when) - def pytest_runtest_teardown(item): item.config._setupstate.teardown_exact(item) @@ -68,8 +82,8 @@ def pytest__teardown_final(session): if call.excinfo: ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir) call.excinfo.traceback = ntraceback.filter() - rep = TeardownErrorReport(call.excinfo) - return rep + longrepr = call.excinfo.getrepr(funcargs=True) + return TeardownErrorReport(longrepr) def pytest_report_teststatus(report): if report.when in ("setup", "teardown"): @@ -80,6 +94,8 @@ def pytest_report_teststatus(report): return "skipped", "s", "SKIPPED" else: return "", "", "" + + # # Implementation @@ -115,123 +131,117 @@ class CallInfo: return "" % (self.when, status) class BaseReport(object): - def __init__(self): - self.headerlines = [] - def __repr__(self): - l = ["%s=%s" %(key, value) - for key, value in self.__dict__.items()] - return "<%s %s>" %(self.__class__.__name__, " ".join(l),) - - def _getcrashline(self): - try: - return str(self.longrepr.reprcrash) - except AttributeError: - try: - return str(self.longrepr)[:50] - except AttributeError: - return "" - def toterminal(self, out): - for line in self.headerlines: - out.line(line) longrepr = self.longrepr if hasattr(longrepr, 'toterminal'): longrepr.toterminal(out) else: out.line(str(longrepr)) -class CollectErrorRepr(BaseReport): + passed = property(lambda x: x.outcome == "passed") + failed = property(lambda x: x.outcome == "failed") + skipped = property(lambda x: x.outcome == "skipped") + + +def pytest_runtest_makereport(item, call): + nodeinfo = getitemnodeinfo(item) + when = call.when + keywords = dict([(x,1) for x in item.keywords]) + excinfo = call.excinfo + if not call.excinfo: + outcome = "passed" + longrepr = None + else: + if not isinstance(excinfo, py.code.ExceptionInfo): + outcome = "failed" + longrepr = excinfo + elif excinfo.errisinstance(py.test.skip.Exception): + outcome = "skipped" + longrepr = item._repr_failure_py(excinfo) + else: + outcome = "failed" + if call.when == "call": + longrepr = item.repr_failure(excinfo) + else: # exception in setup or teardown + longrepr = item._repr_failure_py(excinfo) + return TestReport(nodeinfo.nodeid, nodeinfo.nodenames, + nodeinfo.fspath, nodeinfo.location, + keywords, outcome, longrepr, when) + +class TestReport(BaseReport): + def __init__(self, nodeid, nodenames, fspath, location, + keywords, outcome, longrepr, when): + self.nodeid = nodeid + self.nodenames = nodenames + self.fspath = fspath # where the test was collected + self.location = location + self.keywords = keywords + self.outcome = outcome + self.longrepr = longrepr + self.when = when + + def __repr__(self): + return "" % ( + self.nodeid, self.when, self.outcome) + +class TeardownErrorReport(BaseReport): + outcome = "failed" + when = "teardown" + def __init__(self, longrepr): + self.longrepr = longrepr + +def pytest_make_collect_report(collector): + result = excinfo = None + try: + result = collector._memocollect() + except KeyboardInterrupt: + raise + except: + excinfo = py.code.ExceptionInfo() + nodenames = tuple(collector.listnames()) + nodeid = collector.collection.getid(collector) + fspath = str(collector.fspath) + reason = longrepr = None + if not excinfo: + outcome = "passed" + else: + if excinfo.errisinstance(py.test.skip.Exception): + outcome = "skipped" + reason = str(excinfo.value) + longrepr = collector._repr_failure_py(excinfo, "line") + else: + outcome = "failed" + errorinfo = collector.repr_failure(excinfo) + if not hasattr(errorinfo, "toterminal"): + errorinfo = CollectErrorRepr(errorinfo) + longrepr = errorinfo + return CollectReport(nodenames, nodeid, fspath, + outcome, longrepr, result, reason) + +class CollectReport(BaseReport): + def __init__(self, nodenames, nodeid, fspath, outcome, + longrepr, result, reason): + self.nodenames = nodenames + self.nodeid = nodeid + self.fspath = fspath + self.outcome = outcome + self.longrepr = longrepr + self.result = result + self.reason = reason + + @property + def location(self): + return (self.fspath, None, self.fspath) + + def __repr__(self): + return "" % (self.nodeid, self.outcome) + +class CollectErrorRepr(TerminalRepr): def __init__(self, msg): - super(CollectErrorRepr, self).__init__() self.longrepr = msg def toterminal(self, out): out.line(str(self.longrepr), red=True) -class ItemTestReport(BaseReport): - failed = passed = skipped = False - - def __init__(self, item, excinfo=None, when=None): - super(ItemTestReport, self).__init__() - self.item = item - self.when = when - if item and when != "setup": - self.keywords = item.keywords - else: - # if we fail during setup it might mean - # we are not able to access the underlying object - # this might e.g. happen if we are unpickled - # and our parent collector did not collect us - # (because it e.g. skipped for platform reasons) - self.keywords = {} - if not excinfo: - self.passed = True - self.shortrepr = "." - else: - if not isinstance(excinfo, py.code.ExceptionInfo): - self.failed = True - shortrepr = "?" - longrepr = excinfo - elif excinfo.errisinstance(py.test.skip.Exception): - self.skipped = True - shortrepr = "s" - longrepr = self.item._repr_failure_py(excinfo) - else: - self.failed = True - shortrepr = self.item.shortfailurerepr - if self.when == "call": - longrepr = self.item.repr_failure(excinfo) - else: # exception in setup or teardown - longrepr = self.item._repr_failure_py(excinfo) - shortrepr = shortrepr.lower() - self.shortrepr = shortrepr - self.longrepr = longrepr - - def __repr__(self): - status = (self.passed and "passed" or - self.skipped and "skipped" or - self.failed and "failed" or - "CORRUPT") - l = [repr(self.item.name), "when=%r" % self.when, "outcome %r" % status,] - if hasattr(self, 'node'): - l.append("txnode=%s" % self.node.gateway.id) - info = " " .join(map(str, l)) - return "" % info - - def getnode(self): - return self.item - -class CollectReport(BaseReport): - skipped = failed = passed = False - - def __init__(self, collector, result, excinfo=None): - super(CollectReport, self).__init__() - self.collector = collector - if not excinfo: - self.passed = True - self.result = result - else: - if excinfo.errisinstance(py.test.skip.Exception): - self.skipped = True - self.reason = str(excinfo.value) - self.longrepr = self.collector._repr_failure_py(excinfo, "line") - else: - self.failed = True - errorinfo = self.collector.repr_failure(excinfo) - if not hasattr(errorinfo, "toterminal"): - errorinfo = CollectErrorRepr(errorinfo) - self.longrepr = errorinfo - - def getnode(self): - return self.collector - -class TeardownErrorReport(BaseReport): - skipped = passed = False - failed = True - when = "teardown" - def __init__(self, excinfo): - super(TeardownErrorReport, self).__init__() - self.longrepr = excinfo.getrepr(funcargs=True) - class SetupState(object): """ shared state for setting up/tearing down test items or collectors. """ def __init__(self): diff --git a/py/_plugin/pytest_skipping.py b/py/_plugin/pytest_skipping.py index 70b64aabd..6c01a4c9c 100644 --- a/py/_plugin/pytest_skipping.py +++ b/py/_plugin/pytest_skipping.py @@ -231,19 +231,16 @@ def pytest_runtest_makereport(__multicall__, item, call): if not item.config.getvalue("runxfail"): rep = __multicall__.execute() rep.keywords['xfail'] = "reason: " + call.excinfo.value.msg - rep.skipped = True - rep.failed = False + rep.outcome = "skipped" return rep if call.when == "call": rep = __multicall__.execute() evalxfail = getattr(item, '_evalxfail') if not item.config.getvalue("runxfail") and evalxfail.istrue(): if call.excinfo: - rep.skipped = True - rep.failed = rep.passed = False + rep.outcome = "skipped" else: - rep.skipped = rep.passed = False - rep.failed = True + rep.outcome = "failed" rep.keywords['xfail'] = evalxfail.getexplanation() else: if 'xfail' in rep.keywords: @@ -275,9 +272,9 @@ def pytest_terminal_summary(terminalreporter): show_xfailed(terminalreporter, lines) elif char == "X": show_xpassed(terminalreporter, lines) - elif char == "f": + elif char in "fF": show_failed(terminalreporter, lines) - elif char == "s": + elif char in "sS": show_skipped(terminalreporter, lines) if lines: tr._tw.sep("=", "short test summary info") @@ -289,22 +286,24 @@ def show_failed(terminalreporter, lines): failed = terminalreporter.stats.get("failed") if failed: for rep in failed: - pos = terminalreporter.gettestid(rep.item) + pos = rep.nodeid lines.append("FAIL %s" %(pos, )) def show_xfailed(terminalreporter, lines): xfailed = terminalreporter.stats.get("xfailed") if xfailed: for rep in xfailed: - pos = terminalreporter.gettestid(rep.item) + pos = rep.nodeid reason = rep.keywords['xfail'] - lines.append("XFAIL %s %s" %(pos, reason)) + lines.append("XFAIL %s" % (pos,)) + if reason: + lines.append(" " + str(reason)) def show_xpassed(terminalreporter, lines): xpassed = terminalreporter.stats.get("xpassed") if xpassed: for rep in xpassed: - pos = terminalreporter.gettestid(rep.item) + pos = rep.nodeid reason = rep.keywords['xfail'] lines.append("XPASS %s %s" %(pos, reason)) diff --git a/py/_plugin/pytest_terminal.py b/py/_plugin/pytest_terminal.py index be3d21e5f..78e49ac14 100644 --- a/py/_plugin/pytest_terminal.py +++ b/py/_plugin/pytest_terminal.py @@ -22,32 +22,20 @@ def pytest_addoption(parser): help="(deprecated, use -r)") group._addoption('--tb', metavar="style", action="store", dest="tbstyle", default='long', - type="choice", choices=['long', 'short', 'no', 'line'], + type="choice", choices=['long', 'short', 'no', 'line', 'native'], help="traceback print mode (long/short/line/no).") group._addoption('--fulltrace', action="store_true", dest="fulltrace", default=False, help="don't cut any tracebacks (default is to cut).") - group._addoption('--funcargs', - action="store_true", dest="showfuncargs", default=False, - help="show available function arguments, sorted by plugin") def pytest_configure(config): + if config.option.showfuncargs: + return if config.option.collectonly: reporter = CollectonlyReporter(config) - elif config.option.showfuncargs: - config.setsessionclass(ShowFuncargSession) - reporter = None else: reporter = TerminalReporter(config) - if reporter: - # XXX see remote.py's XXX - for attr in 'pytest_terminal_hasmarkup', 'pytest_terminal_fullwidth': - if hasattr(config, attr): - #print "SETTING TERMINAL OPTIONS", attr, getattr(config, attr) - name = attr.split("_")[-1] - assert hasattr(self.reporter._tw, name), name - setattr(reporter._tw, name, getattr(config, attr)) - config.pluginmanager.register(reporter, 'terminalreporter') + config.pluginmanager.register(reporter, 'terminalreporter') def getreportopt(config): reportopts = "" @@ -69,6 +57,17 @@ def getreportopt(config): reportopts += char return reportopts +def pytest_report_teststatus(report): + if report.passed: + letter = "." + elif report.skipped: + letter = "s" + elif report.failed: + letter = "F" + if report.when != "call": + letter = "f" + return report.outcome, letter, report.outcome.upper() + class TerminalReporter: def __init__(self, config, file=None): self.config = config @@ -85,12 +84,12 @@ class TerminalReporter: return char in self.reportchars def write_fspath_result(self, fspath, res): - fspath = self.curdir.bestrelpath(fspath) if fspath != self.currentfspath: + self.currentfspath = fspath + fspath = self.curdir.bestrelpath(fspath) self._tw.line() relpath = self.curdir.bestrelpath(fspath) self._tw.write(relpath + " ") - self.currentfspath = fspath self._tw.write(res) def write_ensure_prefix(self, prefix, extra="", **kwargs): @@ -116,42 +115,6 @@ class TerminalReporter: self.ensure_newline() self._tw.sep(sep, title, **markup) - def getcategoryletterword(self, rep): - res = self.config.hook.pytest_report_teststatus(report=rep) - if res: - return res - for cat in 'skipped failed passed ???'.split(): - if getattr(rep, cat, None): - break - return cat, self.getoutcomeletter(rep), self.getoutcomeword(rep) - - def getoutcomeletter(self, rep): - return rep.shortrepr - - def getoutcomeword(self, rep): - if rep.passed: - return "PASS", dict(green=True) - elif rep.failed: - return "FAIL", dict(red=True) - elif rep.skipped: - return "SKIP" - else: - return "???", dict(red=True) - - def gettestid(self, item, relative=True): - fspath = item.fspath - chain = [x for x in item.listchain() if x.fspath == fspath] - chain = chain[1:] - names = [x.name for x in chain if x.name != "()"] - path = item.fspath - if relative: - relpath = path.relto(self.curdir) - if relpath: - path = relpath - names.insert(0, str(path)) - return "::".join(names) - - def pytest_internalerror(self, excrepr): for line in str(excrepr).split("\n"): self.write_line("INTERNALERROR> " + line) @@ -170,37 +133,47 @@ class TerminalReporter: self.write_line("[%s] %s" %(category, msg)) def pytest_deselected(self, items): - self.stats.setdefault('deselected', []).append(items) - - def pytest_itemstart(self, item, node=None): - if self.config.option.verbose: - line = self._reportinfoline(item) - self.write_ensure_prefix(line, "") - else: - # ensure that the path is printed before the - # 1st test of a module starts running - self.write_fspath_result(self._getfspath(item), "") + self.stats.setdefault('deselected', []).extend(items) def pytest__teardown_final_logerror(self, report): self.stats.setdefault("error", []).append(report) + def pytest_runtest_logstart(self, nodeid, location, fspath): + # ensure that the path is printed before the + # 1st test of a module starts running + if self.config.option.verbose: + line = self._locationline(fspath, *location) + self.write_ensure_prefix(line, "") + else: + self.write_fspath_result(py.path.local(fspath), "") + def pytest_runtest_logreport(self, report): rep = report - cat, letter, word = self.getcategoryletterword(rep) + res = self.config.hook.pytest_report_teststatus(report=rep) + cat, letter, word = res + self.stats.setdefault(cat, []).append(rep) if not letter and not word: # probably passed setup/teardown return - if isinstance(word, tuple): - word, markup = word - else: - markup = {} - self.stats.setdefault(cat, []).append(rep) if not self.config.option.verbose: - self.write_fspath_result(self._getfspath(rep.item), letter) + if not hasattr(rep, 'node'): + self.write_fspath_result(rep.fspath, letter) + else: + self._tw.write(letter) else: - line = self._reportinfoline(rep.item) + if isinstance(word, tuple): + word, markup = word + else: + if rep.passed: + markup = {'green':True} + elif rep.failed: + markup = {'red':True} + elif rep.skipped: + markup = {'yellow':True} + line = self._locationline(str(rep.fspath), *rep.location) if not hasattr(rep, 'node'): self.write_ensure_prefix(line, word, **markup) + #self._tw.write(word, **markup) else: self.ensure_newline() if hasattr(rep, 'node'): @@ -213,25 +186,27 @@ class TerminalReporter: if not report.passed: if report.failed: self.stats.setdefault("error", []).append(report) - self.write_fspath_result(report.collector.fspath, "E") + self.write_fspath_result(report.fspath, "E") elif report.skipped: self.stats.setdefault("skipped", []).append(report) - self.write_fspath_result(report.collector.fspath, "S") + self.write_fspath_result(report.fspath, "S") def pytest_sessionstart(self, session): self.write_sep("=", "test session starts", bold=True) self._sessionstarttime = py.std.time.time() - verinfo = ".".join(map(str, sys.version_info[:3])) msg = "platform %s -- Python %s" % (sys.platform, verinfo) msg += " -- pytest-%s" % (py.__version__) - if self.config.option.verbose or self.config.option.debug or getattr(self.config.option, 'pastebin', None): + if self.config.option.verbose or self.config.option.debug or \ + getattr(self.config.option, 'pastebin', None): msg += " -- " + str(sys.executable) self.write_line(msg) lines = self.config.hook.pytest_report_header(config=self.config) lines.reverse() for line in flatten(lines): self.write_line(line) + + def pytest_log_finishcollection(self): for i, testarg in enumerate(self.config.args): self.write_line("test path %d: %s" %(i+1, testarg)) @@ -260,52 +235,40 @@ class TerminalReporter: else: excrepr.reprcrash.toterminal(self._tw) - def _reportinfoline(self, item): - collect_fspath = self._getfspath(item) - fspath, lineno, msg = self._getreportinfo(item) + def _locationline(self, collect_fspath, fspath, lineno, domain): if fspath and fspath != collect_fspath: fspath = "%s <- %s" % ( - self.curdir.bestrelpath(collect_fspath), - self.curdir.bestrelpath(fspath)) + self.curdir.bestrelpath(py.path.local(collect_fspath)), + self.curdir.bestrelpath(py.path.local(fspath))) elif fspath: - fspath = self.curdir.bestrelpath(fspath) + fspath = self.curdir.bestrelpath(py.path.local(fspath)) if lineno is not None: lineno += 1 - if fspath and lineno and msg: - line = "%(fspath)s:%(lineno)s: %(msg)s" - elif fspath and msg: - line = "%(fspath)s: %(msg)s" + if fspath and lineno and domain: + line = "%(fspath)s:%(lineno)s: %(domain)s" + elif fspath and domain: + line = "%(fspath)s: %(domain)s" elif fspath and lineno: line = "%(fspath)s:%(lineno)s %(extrapath)s" else: - line = "[noreportinfo]" + line = "[nolocation]" return line % locals() + " " def _getfailureheadline(self, rep): - if hasattr(rep, "collector"): - return str(rep.collector.fspath) - elif hasattr(rep, 'item'): - fspath, lineno, msg = self._getreportinfo(rep.item) - return msg + if hasattr(rep, 'location'): + fspath, lineno, domain = rep.location + return domain else: - return "test session" + return "test session" # XXX? - def _getreportinfo(self, item): + def _getcrashline(self, rep): try: - return item.__reportinfo + return str(rep.longrepr.reprcrash) except AttributeError: - pass - reportinfo = item.config.hook.pytest_report_iteminfo(item=item) - # cache on item - item.__reportinfo = reportinfo - return reportinfo - - def _getfspath(self, item): - try: - return item.fspath - except AttributeError: - fspath, lineno, msg = self._getreportinfo(item) - return fspath + try: + return str(rep.longrepr)[:50] + except AttributeError: + return "" # # summaries for sessionfinish @@ -317,7 +280,7 @@ class TerminalReporter: self.write_sep("=", "FAILURES") for rep in self.stats['failed']: if tbstyle == "line": - line = rep._getcrashline() + line = self._getcrashline(rep) self.write_line(line) else: msg = self._getfailureheadline(rep) @@ -383,21 +346,27 @@ class CollectonlyReporter: self.outindent(collector) self.indent += self.INDENT - def pytest_itemstart(self, item, node=None): + def pytest_log_itemcollect(self, item): self.outindent(item) def pytest_collectreport(self, report): if not report.passed: - self.outindent("!!! %s !!!" % report.longrepr.reprcrash.message) + if hasattr(report.longrepr, 'reprcrash'): + msg = report.longrepr.reprcrash.message + else: + # XXX unify (we have CollectErrorRepr here) + msg = str(report.longrepr.longrepr) + self.outindent("!!! %s !!!" % msg) + #self.outindent("!!! error !!!") self._failed.append(report) self.indent = self.indent[:-len(self.INDENT)] - def pytest_sessionfinish(self, session, exitstatus): + def pytest_log_finishcollection(self): if self._failed: self._tw.sep("!", "collection failures") for rep in self._failed: rep.toterminal(self._tw) - + return self._failed and 1 or 0 def repr_pythonversion(v=None): if v is None: @@ -415,50 +384,3 @@ def flatten(l): else: yield x -from py._test.session import Session -class ShowFuncargSession(Session): - def main(self, colitems): - self.fspath = py.path.local() - self.sessionstarts() - try: - self.showargs(colitems[0]) - finally: - self.sessionfinishes(exitstatus=1) - - def showargs(self, colitem): - tw = py.io.TerminalWriter() - from py._test.funcargs import getplugins - from py._test.funcargs import FuncargRequest - plugins = getplugins(colitem, withpy=True) - verbose = self.config.getvalue("verbose") - for plugin in plugins: - available = [] - for name, factory in vars(plugin).items(): - if name.startswith(FuncargRequest._argprefix): - name = name[len(FuncargRequest._argprefix):] - if name not in available: - available.append([name, factory]) - if available: - pluginname = plugin.__name__ - for name, factory in available: - loc = self.getlocation(factory) - if verbose: - funcargspec = "%s -- %s" %(name, loc,) - else: - funcargspec = name - tw.line(funcargspec, green=True) - doc = factory.__doc__ or "" - if doc: - for line in doc.split("\n"): - tw.line(" " + line.strip()) - else: - tw.line(" %s: no docstring available" %(loc,), - red=True) - - def getlocation(self, function): - import inspect - fn = py.path.local(inspect.getfile(function)) - lineno = py.builtin._getcode(function).co_firstlineno - if fn.relto(self.fspath): - fn = fn.relto(self.fspath) - return "%s:%d" %(fn, lineno+1) diff --git a/py/_test/cmdline.py b/py/_test/cmdline.py deleted file mode 100644 index 10f13f795..000000000 --- a/py/_test/cmdline.py +++ /dev/null @@ -1,24 +0,0 @@ -import py -import sys - -# -# main entry point -# - -def main(args=None): - if args is None: - args = sys.argv[1:] - config = py.test.config - try: - config.parse(args) - config.pluginmanager.do_configure(config) - session = config.initsession() - colitems = config.getinitialnodes() - exitstatus = session.main(colitems) - config.pluginmanager.do_unconfigure(config) - except config.Error: - e = sys.exc_info()[1] - sys.stderr.write("ERROR: %s\n" %(e.args[0],)) - exitstatus = 3 - py.test.config = py.test.config.__class__() - return exitstatus diff --git a/py/_test/collect.py b/py/_test/collect.py index 4e59eb59d..6a94cf2ee 100644 --- a/py/_test/collect.py +++ b/py/_test/collect.py @@ -25,47 +25,15 @@ class Node(object): """ base class for all Nodes in the collection tree. Collector subclasses have children, Items are terminal nodes. """ - def __init__(self, name, parent=None, config=None): + def __init__(self, name, parent=None, config=None, collection=None): self.name = name self.parent = parent self.config = config or parent.config + self.collection = collection or getattr(parent, 'collection', None) self.fspath = getattr(parent, 'fspath', None) self.ihook = HookProxy(self) self.keywords = self.readkeywords() - def _reraiseunpicklingproblem(self): - if hasattr(self, '_unpickle_exc'): - py.builtin._reraise(*self._unpickle_exc) - - # - # note to myself: Pickling is uh. - # - def __getstate__(self): - return (self.name, self.parent) - def __setstate__(self, nameparent): - name, parent = nameparent - try: - colitems = parent._memocollect() - for colitem in colitems: - if colitem.name == name: - # we are a copy that will not be returned - # by our parent - self.__dict__ = colitem.__dict__ - break - else: - raise ValueError("item %r not found in parent collection %r" %( - name, [x.name for x in colitems])) - except KeyboardInterrupt: - raise - except Exception: - # our parent can't collect us but we want unpickling to - # otherwise continue - self._reraiseunpicklingproblem() will - # reraise the problem - self._unpickle_exc = py.std.sys.exc_info() - self.name = name - self.parent = parent - self.config = parent.config - def __repr__(self): if getattr(self.config.option, 'debug', False): return "<%s %r %0x>" %(self.__class__.__name__, @@ -79,7 +47,8 @@ class Node(object): def __eq__(self, other): if not isinstance(other, Node): return False - return self.name == other.name and self.parent == other.parent + return self.__class__ == other.__class__ and \ + self.name == other.name and self.parent == other.parent def __ne__(self, other): return not self == other @@ -102,7 +71,7 @@ class Node(object): return getattr(self, attrname) try: res = function() - except (KeyboardInterrupt, SystemExit): + except py.builtin._sysex: raise except: failure = py.std.sys.exc_info() @@ -117,7 +86,7 @@ class Node(object): l = [self] while 1: x = l[0] - if x.parent is not None and x.parent.parent is not None: + if x.parent is not None: # and x.parent.parent is not None: l.insert(0, x.parent) else: return l @@ -137,39 +106,6 @@ class Node(object): def _keywords(self): return [self.name] - def _skipbykeyword(self, keywordexpr): - """ return True if they given keyword expression means to - skip this collector/item. - """ - if not keywordexpr: - return - chain = self.listchain() - for key in filter(None, keywordexpr.split()): - eor = key[:1] == '-' - if eor: - key = key[1:] - if not (eor ^ self._matchonekeyword(key, chain)): - return True - - def _matchonekeyword(self, key, chain): - elems = key.split(".") - # XXX O(n^2), anyone cares? - chain = [item.keywords for item in chain if item.keywords] - for start, _ in enumerate(chain): - if start + len(elems) > len(chain): - return False - for num, elem in enumerate(elems): - for keyword in chain[num + start]: - ok = False - if elem in keyword: - ok = True - break - if not ok: - break - if num == len(elems) - 1 and ok: - return True - return False - def _prunetraceback(self, traceback): return traceback @@ -190,7 +126,6 @@ class Node(object): style=style) repr_failure = _repr_failure_py - shortfailurerepr = "F" class Collector(Node): """ @@ -270,19 +205,12 @@ class Collector(Node): return traceback class FSCollector(Collector): - def __init__(self, fspath, parent=None, config=None): + def __init__(self, fspath, parent=None, config=None, collection=None): fspath = py.path.local(fspath) - super(FSCollector, self).__init__(fspath.basename, parent, config=config) + super(FSCollector, self).__init__(fspath.basename, + parent, config, collection) self.fspath = fspath - def __getstate__(self): - # RootCollector.getbynames() inserts a directory which we need - # to throw out here for proper re-instantiation - if isinstance(self.parent.parent, RootCollector): - assert self.parent.fspath == self.parent.parent.fspath, self.parent - return (self.name, self.parent.parent) # shortcut - return super(Collector, self).__getstate__() - class File(FSCollector): """ base class for collecting tests from a file. """ @@ -368,59 +296,3 @@ def warnoldtestrun(function=None): "item.run() and item.execute()", stacklevel=2, function=function) - - -class RootCollector(Directory): - def __init__(self, config): - Directory.__init__(self, config.topdir, parent=None, config=config) - self.name = None - - def __repr__(self): - return "" %(self.fspath,) - - def getbynames(self, names): - current = self.consider(self.config.topdir) - while names: - name = names.pop(0) - if name == ".": # special "identity" name - continue - l = [] - for x in current._memocollect(): - if x.name == name: - l.append(x) - elif x.fspath == current.fspath.join(name): - l.append(x) - elif x.name == "()": - names.insert(0, name) - l.append(x) - break - if not l: - raise ValueError("no node named %r below %r" %(name, current)) - current = l[0] - return current - - def totrail(self, node): - chain = node.listchain() - names = [self._getrelpath(chain[0].fspath)] - names += [x.name for x in chain[1:]] - return names - - def fromtrail(self, trail): - return self.config._rootcol.getbynames(trail) - - def _getrelpath(self, fspath): - topdir = self.config.topdir - relpath = fspath.relto(topdir) - if not relpath: - if fspath == topdir: - relpath = "." - else: - raise ValueError("%r not relative to topdir %s" - %(self.fspath, topdir)) - return relpath - - def __getstate__(self): - return self.config - - def __setstate__(self, config): - self.__init__(config) diff --git a/py/_test/config.py b/py/_test/config.py index d72ddb722..47ffd4f09 100644 --- a/py/_test/config.py +++ b/py/_test/config.py @@ -2,7 +2,6 @@ import py, os from py._test.conftesthandle import Conftest from py._test.pluginmanager import PluginManager from py._test import parseopt -from py._test.collect import RootCollector def ensuretemp(string, dir=1): """ (deprecated) return temporary directory path with @@ -31,9 +30,8 @@ class Config(object): basetemp = None _sessionclass = None - def __init__(self, topdir=None, option=None): - self.option = option or CmdOptions() - self.topdir = topdir + def __init__(self): + self.option = CmdOptions() self._parser = parseopt.Parser( usage="usage: %prog [options] [file_or_dir] [file_or_dir] [...]", processopt=self._processopt, @@ -97,39 +95,7 @@ class Config(object): args = self._parser.parse_setoption(args, self.option) if not args: args.append(py.std.os.getcwd()) - self.topdir = gettopdir(args) - self._rootcol = RootCollector(config=self) - self._setargs(args) - - def _setargs(self, args): - self.args = list(args) - self._argfspaths = [py.path.local(decodearg(x)[0]) for x in args] - - # config objects are usually pickled across system - # barriers but they contain filesystem paths. - # upon getstate/setstate we take care to do everything - # relative to "topdir". - def __getstate__(self): - l = [] - for path in self.args: - path = py.path.local(path) - l.append(path.relto(self.topdir)) - return l, self.option.__dict__ - - def __setstate__(self, repr): - # we have to set py.test.config because loading - # of conftest files may use it (deprecated) - # mainly by py.test.config.addoptions() - global config_per_process - py.test.config = config_per_process = self - args, cmdlineopts = repr - cmdlineopts = CmdOptions(**cmdlineopts) - # next line will registers default plugins - self.__init__(topdir=py.path.local(), option=cmdlineopts) - self._rootcol = RootCollector(config=self) - args = [str(self.topdir.join(x)) for x in args] - self._preparse(args) - self._setargs(args) + self.args = args def ensuretemp(self, string, dir=True): return self.getbasetemp().ensure(string, dir=dir) @@ -154,27 +120,6 @@ class Config(object): return py.path.local.make_numbered_dir(prefix=basename, keep=0, rootdir=basetemp, lock_timeout=None) - def getinitialnodes(self): - return [self.getnode(arg) for arg in self.args] - - def getnode(self, arg): - parts = decodearg(arg) - path = py.path.local(parts.pop(0)) - if not path.check(): - raise self.Error("file not found: %s" %(path,)) - topdir = self.topdir - if path != topdir and not path.relto(topdir): - raise self.Error("path %r is not relative to %r" % - (str(path), str(topdir))) - # assumtion: pytest's fs-collector tree follows the filesystem tree - names = list(filter(None, path.relto(topdir).split(path.sep))) - names += parts - try: - return self._rootcol.getbynames(names) - except ValueError: - e = py.std.sys.exc_info()[1] - raise self.Error("can't collect: %s\n%s" % (arg, e.args[0])) - def _getcollectclass(self, name, path): try: cls = self._conftest.rget(name, path) @@ -239,48 +184,10 @@ class Config(object): except AttributeError: return self._conftest.rget(name, path) - def setsessionclass(self, cls): - if self._sessionclass is not None: - raise ValueError("sessionclass already set to: %r" %( - self._sessionclass)) - self._sessionclass = cls - - def initsession(self): - """ return an initialized session object. """ - cls = self._sessionclass - if cls is None: - from py._test.session import Session - cls = Session - session = cls(self) - self.trace("instantiated session %r" % session) - return session - # # helpers # -def gettopdir(args): - """ return the top directory for the given paths. - if the common base dir resides in a python package - parent directory of the root package is returned. - """ - fsargs = [py.path.local(decodearg(arg)[0]) for arg in args] - p = fsargs and fsargs[0] or None - for x in fsargs[1:]: - p = p.common(x) - assert p, "cannot determine common basedir of %s" %(fsargs,) - pkgdir = p.pypkgpath() - if pkgdir is None: - if p.check(file=1): - p = p.dirpath() - return p - else: - return pkgdir.dirpath() - -def decodearg(arg): - arg = str(arg) - return arg.split("::") - def onpytestaccess(): # it's enough to have our containing module loaded as # it initializes a per-process config instance diff --git a/py/_test/funcargs.py b/py/_test/funcargs.py deleted file mode 100644 index 5d69b1939..000000000 --- a/py/_test/funcargs.py +++ /dev/null @@ -1,186 +0,0 @@ -import py - -def getfuncargnames(function): - argnames = py.std.inspect.getargs(py.code.getrawcode(function))[0] - startindex = py.std.inspect.ismethod(function) and 1 or 0 - defaults = getattr(function, 'func_defaults', - getattr(function, '__defaults__', None)) or () - numdefaults = len(defaults) - if numdefaults: - return argnames[startindex:-numdefaults] - return argnames[startindex:] - -def fillfuncargs(function): - """ fill missing funcargs. """ - request = FuncargRequest(pyfuncitem=function) - request._fillfuncargs() - -def getplugins(node, withpy=False): # might by any node - plugins = node.config._getmatchingplugins(node.fspath) - if withpy: - mod = node.getparent(py.test.collect.Module) - if mod is not None: - plugins.append(mod.obj) - inst = node.getparent(py.test.collect.Instance) - if inst is not None: - plugins.append(inst.obj) - return plugins - -_notexists = object() -class CallSpec: - def __init__(self, funcargs, id, param): - self.funcargs = funcargs - self.id = id - if param is not _notexists: - self.param = param - def __repr__(self): - return "" %( - self.id, getattr(self, 'param', '?'), self.funcargs) - -class Metafunc: - def __init__(self, function, config=None, cls=None, module=None): - self.config = config - self.module = module - self.function = function - self.funcargnames = getfuncargnames(function) - self.cls = cls - self.module = module - self._calls = [] - self._ids = py.builtin.set() - - def addcall(self, funcargs=None, id=_notexists, param=_notexists): - assert funcargs is None or isinstance(funcargs, dict) - if id is None: - raise ValueError("id=None not allowed") - if id is _notexists: - id = len(self._calls) - id = str(id) - if id in self._ids: - raise ValueError("duplicate id %r" % id) - self._ids.add(id) - self._calls.append(CallSpec(funcargs, id, param)) - -class FuncargRequest: - _argprefix = "pytest_funcarg__" - _argname = None - - class LookupError(LookupError): - """ error on performing funcarg request. """ - - def __init__(self, pyfuncitem): - self._pyfuncitem = pyfuncitem - self.function = pyfuncitem.obj - self.module = pyfuncitem.getparent(py.test.collect.Module).obj - clscol = pyfuncitem.getparent(py.test.collect.Class) - self.cls = clscol and clscol.obj or None - self.instance = py.builtin._getimself(self.function) - self.config = pyfuncitem.config - self.fspath = pyfuncitem.fspath - if hasattr(pyfuncitem, '_requestparam'): - self.param = pyfuncitem._requestparam - self._plugins = getplugins(pyfuncitem, withpy=True) - self._funcargs = self._pyfuncitem.funcargs.copy() - self._name2factory = {} - self._currentarg = None - - def _fillfuncargs(self): - argnames = getfuncargnames(self.function) - if argnames: - assert not getattr(self._pyfuncitem, '_args', None), ( - "yielded functions cannot have funcargs") - for argname in argnames: - if argname not in self._pyfuncitem.funcargs: - self._pyfuncitem.funcargs[argname] = self.getfuncargvalue(argname) - - - def applymarker(self, marker): - """ apply a marker to a test function invocation. - - The 'marker' must be created with py.test.mark.* XYZ. - """ - if not isinstance(marker, py.test.mark.XYZ.__class__): - raise ValueError("%r is not a py.test.mark.* object") - self._pyfuncitem.keywords[marker.markname] = marker - - def cached_setup(self, setup, teardown=None, scope="module", extrakey=None): - """ cache and return result of calling setup(). - - The requested argument name, the scope and the ``extrakey`` - determine the cache key. The scope also determines when - teardown(result) will be called. valid scopes are: - scope == 'function': when the single test function run finishes. - scope == 'module': when tests in a different module are run - scope == 'session': when tests of the session have run. - """ - if not hasattr(self.config, '_setupcache'): - self.config._setupcache = {} # XXX weakref? - cachekey = (self._currentarg, self._getscopeitem(scope), extrakey) - cache = self.config._setupcache - try: - val = cache[cachekey] - except KeyError: - val = setup() - cache[cachekey] = val - if teardown is not None: - def finalizer(): - del cache[cachekey] - teardown(val) - self._addfinalizer(finalizer, scope=scope) - return val - - def getfuncargvalue(self, argname): - try: - return self._funcargs[argname] - except KeyError: - pass - if argname not in self._name2factory: - self._name2factory[argname] = self.config.pluginmanager.listattr( - plugins=self._plugins, - attrname=self._argprefix + str(argname) - ) - #else: we are called recursively - if not self._name2factory[argname]: - self._raiselookupfailed(argname) - funcargfactory = self._name2factory[argname].pop() - oldarg = self._currentarg - self._currentarg = argname - try: - self._funcargs[argname] = res = funcargfactory(request=self) - finally: - self._currentarg = oldarg - return res - - def _getscopeitem(self, scope): - if scope == "function": - return self._pyfuncitem - elif scope == "module": - return self._pyfuncitem.getparent(py.test.collect.Module) - elif scope == "session": - return None - raise ValueError("unknown finalization scope %r" %(scope,)) - - def _addfinalizer(self, finalizer, scope): - colitem = self._getscopeitem(scope) - self.config._setupstate.addfinalizer( - finalizer=finalizer, colitem=colitem) - - def addfinalizer(self, finalizer): - """ call the given finalizer after test function finished execution. """ - self._addfinalizer(finalizer, scope="function") - - def __repr__(self): - return "" %(self._pyfuncitem) - - def _raiselookupfailed(self, argname): - available = [] - for plugin in self._plugins: - for name in vars(plugin): - if name.startswith(self._argprefix): - name = name[len(self._argprefix):] - if name not in available: - available.append(name) - fspath, lineno, msg = self._pyfuncitem.reportinfo() - msg = "LookupError: no factory found for function argument %r" % (argname,) - msg += "\n available funcargs: %s" %(", ".join(available),) - msg += "\n use 'py.test --funcargs [testpath]' for help on them." - raise self.LookupError(msg) diff --git a/py/_test/pluginmanager.py b/py/_test/pluginmanager.py index e6a0895ce..89bd995b3 100644 --- a/py/_test/pluginmanager.py +++ b/py/_test/pluginmanager.py @@ -6,9 +6,9 @@ import inspect from py._plugin import hookspec default_plugins = ( - "default runner pdb capture mark terminal skipping tmpdir monkeypatch " - "recwarn pastebin unittest helpconfig nose assertion genscript " - "junitxml doctest").split() + "default terminal python runner pdb capture mark skipping tmpdir monkeypatch " + "recwarn pastebin unittest helpconfig nose assertion genscript " + "junitxml doctest keyword").split() def check_old_use(mod, modname): clsname = modname[len('pytest_'):].capitalize() + "Plugin" @@ -32,7 +32,7 @@ class PluginManager(object): name = id(plugin) return name - def register(self, plugin, name=None): + def register(self, plugin, name=None, prepend=False): assert not self.isregistered(plugin), plugin assert not self.registry.isregistered(plugin), plugin name = self._getpluginname(plugin, name) @@ -41,7 +41,7 @@ class PluginManager(object): self._name2plugin[name] = plugin self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) self.hook.pytest_plugin_registered(manager=self, plugin=plugin) - self.registry.register(plugin) + self.registry.register(plugin, prepend=prepend) return True def unregister(self, plugin): @@ -259,6 +259,8 @@ class MultiCall: return kwargs def varnames(func): + 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: @@ -275,10 +277,13 @@ class Registry: plugins = [] self._plugins = plugins - def register(self, plugin): + def register(self, plugin, prepend=False): assert not isinstance(plugin, str) assert not plugin in self._plugins - self._plugins.append(plugin) + if not prepend: + self._plugins.append(plugin) + else: + self._plugins.insert(0, plugin) def unregister(self, plugin): self._plugins.remove(plugin) diff --git a/py/_test/session.py b/py/_test/session.py index 64d724d13..cd6320eb1 100644 --- a/py/_test/session.py +++ b/py/_test/session.py @@ -6,6 +6,25 @@ """ import py +import os, sys + +# +# main entry point +# + +def main(args=None): + if args is None: + args = sys.argv[1:] + config = py.test.config + config.parse(args) + try: + exitstatus = config.hook.pytest_cmdline_main(config=config) + except config.Error: + e = sys.exc_info()[1] + sys.stderr.write("ERROR: %s\n" %(e.args[0],)) + exitstatus = EXIT_INTERNALERROR + py.test.config = py.test.config.__class__() + return exitstatus # exitcodes for the command line EXIT_OK = 0 @@ -14,10 +33,6 @@ EXIT_INTERRUPTED = 2 EXIT_INTERNALERROR = 3 EXIT_NOHOSTS = 4 -# imports used for genitems() -Item = py.test.collect.Item -Collector = py.test.collect.Collector - class Session(object): nodeid = "" class Interrupted(KeyboardInterrupt): @@ -26,67 +41,10 @@ class Session(object): def __init__(self, config): self.config = config - self.pluginmanager = config.pluginmanager # shortcut - self.pluginmanager.register(self) + self.config.pluginmanager.register(self, name="session", prepend=True) self._testsfailed = 0 - self._nomatch = False self.shouldstop = False - - def genitems(self, colitems, keywordexpr=None): - """ yield Items from iterating over the given colitems. """ - if colitems: - colitems = list(colitems) - while colitems: - next = colitems.pop(0) - if isinstance(next, (tuple, list)): - colitems[:] = list(next) + colitems - continue - assert self.pluginmanager is next.config.pluginmanager - if isinstance(next, Item): - remaining = self.filteritems([next]) - if remaining: - self.config.hook.pytest_itemstart(item=next) - yield next - else: - assert isinstance(next, Collector) - self.config.hook.pytest_collectstart(collector=next) - rep = self.config.hook.pytest_make_collect_report(collector=next) - if rep.passed: - for x in self.genitems(rep.result, keywordexpr): - yield x - self.config.hook.pytest_collectreport(report=rep) - if self.shouldstop: - raise self.Interrupted(self.shouldstop) - - def filteritems(self, colitems): - """ return items to process (some may be deselected)""" - keywordexpr = self.config.option.keyword - if not keywordexpr or self._nomatch: - return colitems - if keywordexpr[-1] == ":": - keywordexpr = keywordexpr[:-1] - remaining = [] - deselected = [] - for colitem in colitems: - if isinstance(colitem, Item): - if colitem._skipbykeyword(keywordexpr): - deselected.append(colitem) - continue - remaining.append(colitem) - if deselected: - self.config.hook.pytest_deselected(items=deselected) - if self.config.option.keyword.endswith(":"): - self._nomatch = True - return remaining - - def collect(self, colitems): - keyword = self.config.option.keyword - for x in self.genitems(colitems, keyword): - yield x - - def sessionstarts(self): - """ setup any neccessary resources ahead of the test run. """ - self.config.hook.pytest_sessionstart(session=self) + self.collection = Collection(config) # XXX move elswehre def pytest_runtest_logreport(self, report): if report.failed: @@ -95,41 +53,195 @@ class Session(object): if maxfail and self._testsfailed >= maxfail: self.shouldstop = "stopping after %d failures" % ( self._testsfailed) + self.collection.shouldstop = self.shouldstop pytest_collectreport = pytest_runtest_logreport - def sessionfinishes(self, exitstatus): - """ teardown any resources after a test run. """ - self.config.hook.pytest_sessionfinish( - session=self, - exitstatus=exitstatus, - ) - - def main(self, colitems): + def main(self): """ main loop for running tests. """ self.shouldstop = False - self.sessionstarts() - exitstatus = EXIT_OK + self.exitstatus = EXIT_OK + config = self.config try: - self._mainloop(colitems) - if self._testsfailed: - exitstatus = EXIT_TESTSFAILED - self.sessionfinishes(exitstatus=exitstatus) + config.pluginmanager.do_configure(config) + config.hook.pytest_sessionstart(session=self) + config.hook.pytest_perform_collection(session=self) + config.hook.pytest_runtest_mainloop(session=self) + except self.config.Error: + raise except KeyboardInterrupt: excinfo = py.code.ExceptionInfo() self.config.hook.pytest_keyboard_interrupt(excinfo=excinfo) - exitstatus = EXIT_INTERRUPTED + self.exitstatus = EXIT_INTERRUPTED except: excinfo = py.code.ExceptionInfo() self.config.pluginmanager.notify_exception(excinfo) - exitstatus = EXIT_INTERNALERROR - if exitstatus in (EXIT_INTERNALERROR, EXIT_INTERRUPTED): - self.sessionfinishes(exitstatus=exitstatus) - return exitstatus + self.exitstatus = EXIT_INTERNALERROR + if excinfo.errisinstance(SystemExit): + sys.stderr.write("mainloop: caught Spurious SystemExit!\n") + + if not self.exitstatus and self._testsfailed: + self.exitstatus = EXIT_TESTSFAILED + self.config.hook.pytest_sessionfinish( + session=self, exitstatus=self.exitstatus, + ) + config.pluginmanager.do_unconfigure(config) + return self.exitstatus + +class Collection: + def __init__(self, config): + self.config = config + self.topdir = gettopdir(self.config.args) + self._argfspaths = [py.path.local(decodearg(x)[0]) + for x in self.config.args] + x = py.test.collect.Directory(fspath=self.topdir, + config=config, collection=self) + self._topcollector = x.consider_dir(self.topdir) + self._topcollector.parent = None + + def _normalizearg(self, arg): + return "::".join(self._parsearg(arg)) + + def _parsearg(self, arg, base=None): + """ return normalized name list for a command line specified id + which might be of the form x/y/z::name1::name2 + and should result into the form x::y::z::name1::name2 + """ + if base is None: + base = py.path.local() + parts = str(arg).split("::") + path = base.join(parts[0], abs=True) + if not path.check(): + raise self.config.Error("file not found: %s" %(path,)) + topdir = self.topdir + if path != topdir and not path.relto(topdir): + raise self.config.Error("path %r is not relative to %r" % + (str(path), str(topdir))) + topparts = path.relto(topdir).split(path.sep) + return topparts + parts[1:] + + def getid(self, node): + """ return id for node, relative to topdir. """ + path = node.fspath + chain = [x for x in node.listchain() if x.fspath == path] + chain = chain[1:] + names = [x.name for x in chain if x.name != "()"] + relpath = path.relto(self.topdir) + if not relpath: + assert path == self.topdir + path = '' + else: + path = relpath + if os.sep != "/": + path = str(path).replace(os.sep, "/") + names.insert(0, path) + return "::".join(names) + + def getbyid(self, id): + """ return one or more nodes matching the id. """ + names = [x for x in id.split("::") if x] + if names and '/' in names[0]: + names[:1] = names[0].split("/") + return self._match([self._topcollector], names) + + def _match(self, matching, names): + while names: + name = names.pop(0) + l = [] + for current in matching: + for x in current._memocollect(): + if x.name == name: + l.append(x) + elif x.name == "()": + names.insert(0, name) + l.append(x) + break + if not l: + raise ValueError("no node named %r below %r" %(name, current)) + matching = l + return matching + + def getinitialnodes(self): + idlist = [self._normalizearg(arg) for arg in self.config.args] + nodes = [] + for id in idlist: + nodes.extend(self.getbyid(id)) + return nodes + + def perform_collect(self): + nodes = [] + for arg in self.config.args: + names = self._parsearg(arg) + try: + self.genitems([self._topcollector], names, nodes) + except NoMatch: + raise self.config.Error("can't collect: %s" % (arg,)) + return nodes + + def genitems(self, matching, names, result): + if not matching: + assert not names + return + if names: + name = names[0] + names = names[1:] + else: + name = None + for node in matching: + if isinstance(node, py.test.collect.Item): + if name is None: + node.ihook.pytest_log_itemcollect(item=node) + result.append(node) + continue + assert isinstance(node, py.test.collect.Collector) + node.ihook.pytest_collectstart(collector=node) + rep = node.ihook.pytest_make_collect_report(collector=node) + #print "matching", rep.result, "against name", name + if rep.passed: + if not name: + self.genitems(rep.result, [], result) + else: + matched = False + for x in rep.result: + try: + if x.name == name or x.fspath.basename == name: + self.genitems([x], names, result) + matched = True + elif x.name == "()": # XXX special Instance() case + self.genitems([x], [name] + names, result) + matched = True + except NoMatch: + pass + if not matched: + node.ihook.pytest_collectreport(report=rep) + raise NoMatch(name) + node.ihook.pytest_collectreport(report=rep) + x = getattr(self, 'shouldstop', None) + if x: + raise Session.Interrupted(x) + +class NoMatch(Exception): + """ raised if genitems cannot locate a matching names. """ + +def gettopdir(args): + """ return the top directory for the given paths. + if the common base dir resides in a python package + parent directory of the root package is returned. + """ + fsargs = [py.path.local(decodearg(arg)[0]) for arg in args] + p = fsargs and fsargs[0] or None + for x in fsargs[1:]: + p = p.common(x) + assert p, "cannot determine common basedir of %s" %(fsargs,) + pkgdir = p.pypkgpath() + if pkgdir is None: + if p.check(file=1): + p = p.dirpath() + return p + else: + return pkgdir.dirpath() + +def decodearg(arg): + arg = str(arg) + return arg.split("::") - def _mainloop(self, colitems): - for item in self.collect(colitems): - if not self.config.option.collectonly: - item.config.hook.pytest_runtest_protocol(item=item) - if self.shouldstop: - raise self.Interrupted(self.shouldstop) diff --git a/setup.py b/setup.py index cf93eeb6a..49c311505 100644 --- a/setup.py +++ b/setup.py @@ -12,8 +12,12 @@ py.test and pylib: rapid testing and development utils - `py.code`_: dynamic code compile and traceback printing support Platforms: Linux, Win32, OSX + Interpreters: Python versions 2.4 through to 3.2, Jython 2.5.1 and PyPy -For questions please check out http://pylib.org/contact.html + +Bugs and issues: http://bitbucket.org/hpk42/py-trunk/issues/ + +Mailing lists and more contact points: http://pylib.org/contact.html .. _`py.test`: http://pytest.org .. _`py.path`: http://pylib.org/path.html @@ -26,14 +30,14 @@ def main(): name='py', description='py.test and pylib: rapid testing and development utils.', long_description = long_description, - version= '1.3.4a1', + version= '1.4.0a1', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], author='holger krekel, Guido Wesdorp, Carl Friedrich Bolz, Armin Rigo, Maciej Fijalkowski & others', author_email='holger at merlinux.eu', entry_points= make_entry_points(), - classifiers=['Development Status :: 5 - Production/Stable', + classifiers=['Development Status :: 6 - Mature', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: POSIX', diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 5b2940d65..0b521eef8 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -72,7 +72,7 @@ class TestGeneralUsage: result = testdir.runpytest(p1, p2) assert result.ret != 0 result.stderr.fnmatch_lines([ - "*ERROR: can't collect: %s" %(p2,) + "*ERROR: can't collect:*%s" %(p2.basename,) ]) @@ -122,7 +122,6 @@ class TestGeneralUsage: ]) - @py.test.mark.xfail def test_issue88_initial_file_multinodes(self, testdir): testdir.makeconftest(""" import py @@ -163,3 +162,52 @@ class TestGeneralUsage: """)) result = testdir.runpython(p, prepend=False) assert not result.ret + + @py.test.mark.xfail(reason="http://bitbucket.org/hpk42/py-trunk/issue/109") + def test_sibling_conftest_issue109(self, testdir): + """ + This test is to make sure that the conftest.py of sibling directories is not loaded + if py.test is run for/in one of the siblings directory and those sibling directories + are not packaged together with an __init__.py. See bitbucket issue #109. + """ + for dirname in ['a', 'b']: + testdir.tmpdir.ensure(dirname, dir=True) + testdir.tmpdir.ensure(dirname, '__init__.py') + + # To create the conftest.py I would like to use testdir.make*-methods + # but as far as I have seen they can only create files in testdir.tempdir + # Maybe there is a way to explicitly specifiy the directory on which those + # methods work or a completely better way to do that? + backupTmpDir = testdir.tmpdir + testdir.tmpdir = testdir.tmpdir.join(dirname) + testdir.makeconftest(""" + _DIR_NAME = '%s' + def pytest_configure(config): + if config.args and config.args[0] != _DIR_NAME: + raise Exception("py.test run for '" + config.args[0] + "', but '" + _DIR_NAME + "/conftest.py' loaded.") + """ % dirname) + testdir.tmpdir = backupTmpDir + + for dirname, other_dirname in [('a', 'b'), ('b', 'a')]: + result = testdir.runpytest(dirname) + assert result.ret == 0, "test_sibling_conftest: py.test run for '%s', but '%s/conftest.py' loaded." % (dirname, other_dirname) + + def test_multiple_items_per_collector_byid(self, testdir): + c = testdir.makeconftest(""" + import py + class MyItem(py.test.collect.Item): + def runtest(self): + pass + class MyCollector(py.test.collect.File): + def collect(self): + return [MyItem(name="xyz", parent=self)] + def pytest_collect_file(path, parent): + if path.basename.startswith("conftest"): + return MyCollector(path, parent) + """) + result = testdir.runpytest(c.basename+"::"+"xyz") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*1 pass*", + ]) + diff --git a/testing/cmdline/test_cmdline.py b/testing/cmdline/test_cmdline.py index cc3ce1399..8438d876b 100644 --- a/testing/cmdline/test_cmdline.py +++ b/testing/cmdline/test_cmdline.py @@ -7,7 +7,7 @@ def test_cmdmain(name, pytestconfig): main = getattr(py.cmdline, name) assert py.builtin.callable(main) assert name[:2] == "py" - if pytestconfig.getvalue("toolsonpath"): + if not pytestconfig.getvalue("notoolsonpath"): scriptname = "py." + name[2:] assert py.path.local.sysfind(scriptname), scriptname diff --git a/testing/code/test_assertion.py b/testing/code/test_assertion.py index d417b0f75..4904f7fb1 100644 --- a/testing/code/test_assertion.py +++ b/testing/code/test_assertion.py @@ -217,3 +217,13 @@ def test_underscore_api(): py.code._AssertionError py.code._reinterpret_old # used by pypy py.code._reinterpret + +@py.test.mark.skipif("sys.version_info < (2,6)") +def test_assert_customizable_reprcompare(monkeypatch): + monkeypatch.setattr(py.code, '_reprcompare', lambda *args: 'hello') + try: + assert 3 == 4 + except AssertionError: + e = exvalue() + s = str(e) + assert "hello" in s diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index e8db92c83..e158944c9 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -700,3 +700,16 @@ raise ValueError() repr = excinfo.getrepr(**reproptions) repr.toterminal(tw) assert tw.stringio.getvalue() + + + def test_native_style(self): + excinfo = self.excinfo_from_exec(""" + assert 0 + """) + repr = excinfo.getrepr(style='native') + assert repr.startswith('Traceback (most recent call last):\n File') + assert repr.endswith('\nAssertionError: assert 0\n') + assert 'exec (source.compile())' in repr + # python 2.4 fails to get the source line for the assert + if py.std.sys.version_info >= (2, 5): + assert repr.count('assert 0') == 2 diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 4be583803..afcd28d7a 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -348,7 +348,7 @@ def test_deindent(): lines = deindent(source.splitlines()) assert lines == ['', 'def f():', ' def g():', ' pass', ' '] -@py.test.mark.xfail("sys.version_info[:2] != (2,7)") +@py.test.mark.xfail("sys.version_info[:2] != (2,7) and sys.version_info[:2]<(3,2)") def test_source_of_class_at_eof_without_newline(tmpdir): # this test fails because the implicit inspect.getsource(A) below # does not return the "x = 1" last line. diff --git a/testing/path/test_local.py b/testing/path/test_local.py index e13a4899c..5b296288b 100644 --- a/testing/path/test_local.py +++ b/testing/path/test_local.py @@ -306,9 +306,11 @@ class TestImport: def test_pyimport_dir(self, tmpdir): p = tmpdir.join("hello_123") - p.ensure("__init__.py") + p_init = p.ensure("__init__.py") m = p.pyimport() assert m.__name__ == "hello_123" + m = p_init.pyimport() + assert m.__name__ == "hello_123" def test_pyimport_execfile_different_name(self, path1): obj = path1.join('execfile.py').pyimport(modname="0x.y.z") diff --git a/testing/plugin/test_pytest_assertion.py b/testing/plugin/test_pytest_assertion.py index c76a93b4d..1b98c8ae8 100644 --- a/testing/plugin/test_pytest_assertion.py +++ b/testing/plugin/test_pytest_assertion.py @@ -1,3 +1,107 @@ +import sys + +import py +import py._plugin.pytest_assertion as plugin + +needsnewassert = py.test.mark.skipif("sys.version_info < (2,6)") + +def interpret(expr): + return py.code._reinterpret(expr, py.code.Frame(sys._getframe(1))) + +class TestBinReprIntegration: + pytestmark = needsnewassert + + def pytest_funcarg__hook(self, request): + class MockHook(object): + def __init__(self): + self.called = False + self.args = tuple() + self.kwargs = dict() + + def __call__(self, op, left, right): + self.called = True + self.op = op + self.left = left + self.right = right + mockhook = MockHook() + monkeypatch = request.getfuncargvalue("monkeypatch") + monkeypatch.setattr(py.code, '_reprcompare', mockhook) + return mockhook + + def test_pytest_assertrepr_compare_called(self, hook): + interpret('assert 0 == 1') + assert hook.called + + + def test_pytest_assertrepr_compare_args(self, hook): + interpret('assert [0, 1] == [0, 2]') + assert hook.op == '==' + assert hook.left == [0, 1] + assert hook.right == [0, 2] + + def test_configure_unconfigure(self, testdir, hook): + assert hook == py.code._reprcompare + config = testdir.parseconfig() + plugin.pytest_configure(config) + assert hook != py.code._reprcompare + plugin.pytest_unconfigure(config) + assert hook == py.code._reprcompare + +class TestAssert_reprcompare: + def test_different_types(self): + assert plugin.pytest_assertrepr_compare('==', [0, 1], 'foo') is None + + def test_summary(self): + summary = plugin.pytest_assertrepr_compare('==', [0, 1], [0, 2])[0] + assert len(summary) < 65 + + def test_text_diff(self): + diff = plugin.pytest_assertrepr_compare('==', 'spam', 'eggs')[1:] + assert '- spam' in diff + assert '+ eggs' in diff + + def test_multiline_text_diff(self): + left = 'foo\nspam\nbar' + right = 'foo\neggs\nbar' + diff = plugin.pytest_assertrepr_compare('==', left, right) + assert '- spam' in diff + assert '+ eggs' in diff + + def test_list(self): + expl = plugin.pytest_assertrepr_compare('==', [0, 1], [0, 2]) + assert len(expl) > 1 + + def test_list_different_lenghts(self): + expl = plugin.pytest_assertrepr_compare('==', [0, 1], [0, 1, 2]) + assert len(expl) > 1 + expl = plugin.pytest_assertrepr_compare('==', [0, 1, 2], [0, 1]) + assert len(expl) > 1 + + def test_dict(self): + expl = plugin.pytest_assertrepr_compare('==', {'a': 0}, {'a': 1}) + assert len(expl) > 1 + + def test_set(self): + expl = plugin.pytest_assertrepr_compare('==', set([0, 1]), set([0, 2])) + assert len(expl) > 1 + +@needsnewassert +def test_pytest_assertrepr_compare_integration(testdir): + testdir.makepyfile(""" + def test_hello(): + x = set(range(100)) + y = x.copy() + y.remove(50) + assert x == y + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*def test_hello():*", + "*assert x == y*", + "*E*Extra items*left*", + "*E*50*", + ]) + def test_functional(testdir): testdir.makepyfile(""" def test_hello(): diff --git a/testing/plugin/test_pytest_doctest.py b/testing/plugin/test_pytest_doctest.py index 161ddf44f..ae13ef69b 100644 --- a/testing/plugin/test_pytest_doctest.py +++ b/testing/plugin/test_pytest_doctest.py @@ -1,4 +1,5 @@ from py._plugin.pytest_doctest import DoctestModule, DoctestTextfile +import py pytest_plugins = ["pytest_doctest"] @@ -73,16 +74,16 @@ class TestDoctests: reprec = testdir.inline_run(p, "--doctest-modules") reprec.assertoutcome(failed=1) - def test_doctestmodule_external(self, testdir): - p = testdir.makepyfile(""" - # + def test_doctestmodule_external_and_issue116(self, testdir): + p = testdir.mkpydir("hello") + p.join("__init__.py").write(py.code.Source(""" def somefunc(): ''' >>> i = 0 >>> i + 1 2 ''' - """) + """)) result = testdir.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines([ '004 *>>> i = 0', diff --git a/testing/plugin/test_pytest_genscript.py b/testing/plugin/test_pytest_genscript.py index c421460fc..10da2e91d 100644 --- a/testing/plugin/test_pytest_genscript.py +++ b/testing/plugin/test_pytest_genscript.py @@ -26,6 +26,7 @@ def test_gen(testdir, anypython, standalone): "*imported from*mypytest" ]) +@py.test.mark.xfail(reason="fix-dist", run=False) def test_rundist(testdir, pytestconfig, standalone): pytestconfig.pluginmanager.skipifmissing("xdist") testdir.makepyfile(""" diff --git a/testing/plugin/test_pytest_hooklog.py b/testing/plugin/test_pytest_hooklog.py index b55359ef7..4b11e69b4 100644 --- a/testing/plugin/test_pytest_hooklog.py +++ b/testing/plugin/test_pytest_hooklog.py @@ -8,5 +8,5 @@ def test_functional(testdir): testdir.runpytest("--hooklog=hook.log") s = testdir.tmpdir.join("hook.log").read() assert s.find("pytest_sessionstart") != -1 - assert s.find("ItemTestReport") != -1 + assert s.find("TestReport") != -1 assert s.find("sessionfinish") != -1 diff --git a/testing/test_genitems.py b/testing/plugin/test_pytest_keyword.py similarity index 97% rename from testing/test_genitems.py rename to testing/plugin/test_pytest_keyword.py index 0ac94e444..2f97c28a3 100644 --- a/testing/test_genitems.py +++ b/testing/plugin/test_pytest_keyword.py @@ -64,7 +64,7 @@ class TestKeywordSelection: reprec = testdir.inline_run("-s", "-k", keyword, file_test) passed, skipped, failed = reprec.listoutcomes() assert len(failed) == 1 - assert failed[0].item.name == name + assert failed[0].nodeid.split("::")[-1] == name assert len(reprec.getcalls('pytest_deselected')) == 1 for keyword in ['test_one', 'est_on']: @@ -92,7 +92,7 @@ class TestKeywordSelection: py.builtin.print_("keyword", repr(keyword)) passed, skipped, failed = reprec.listoutcomes() assert len(passed) == 1 - assert passed[0].item.name == "test_2" + assert passed[0].nodeid.endswith("test_2") dlist = reprec.getcalls("pytest_deselected") assert len(dlist) == 1 assert dlist[0].items[0].name == 'test_1' diff --git a/testing/plugin/test_pytest_pytester.py b/testing/plugin/test_pytest_pytester.py index a6a968dfe..1e1c3bf9b 100644 --- a/testing/plugin/test_pytest_pytester.py +++ b/testing/plugin/test_pytest_pytester.py @@ -5,6 +5,8 @@ def test_reportrecorder(testdir): item = testdir.getitem("def test_func(): pass") recorder = testdir.getreportrecorder(item.config) assert not recorder.getfailures() + + py.test.xfail("internal reportrecorder tests need refactoring") class rep: excinfo = None passed = False diff --git a/testing/test_funcargs.py b/testing/plugin/test_pytest_python.py similarity index 51% rename from testing/test_funcargs.py rename to testing/plugin/test_pytest_python.py index 5b90ffe4f..4b6d717a0 100644 --- a/testing/test_funcargs.py +++ b/testing/plugin/test_pytest_python.py @@ -1,5 +1,521 @@ import py, sys -from py._test import funcargs +from py._plugin import pytest_python as funcargs + +class TestModule: + def test_module_file_not_found(self, testdir): + tmpdir = testdir.tmpdir + fn = tmpdir.join('nada','no') + col = py.test.collect.Module(fn, config=testdir.Config()) + col.config = testdir.parseconfig(tmpdir) + py.test.raises(py.error.ENOENT, col.collect) + + def test_failing_import(self, testdir): + modcol = testdir.getmodulecol("import alksdjalskdjalkjals") + py.test.raises(ImportError, modcol.collect) + py.test.raises(ImportError, modcol.collect) + py.test.raises(ImportError, modcol.run) + + def test_import_duplicate(self, testdir): + a = testdir.mkdir("a") + b = testdir.mkdir("b") + p = a.ensure("test_whatever.py") + p.pyimport() + del py.std.sys.modules['test_whatever'] + b.ensure("test_whatever.py") + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*import*mismatch*", + "*imported*test_whatever*", + "*%s*" % a.join("test_whatever.py"), + "*not the same*", + "*%s*" % b.join("test_whatever.py"), + "*HINT*", + ]) + + def test_syntax_error_in_module(self, testdir): + modcol = testdir.getmodulecol("this is a syntax error") + py.test.raises(modcol.CollectError, modcol.collect) + py.test.raises(modcol.CollectError, modcol.collect) + py.test.raises(modcol.CollectError, modcol.run) + + def test_module_considers_pluginmanager_at_import(self, testdir): + modcol = testdir.getmodulecol("pytest_plugins='xasdlkj',") + py.test.raises(ImportError, "modcol.obj") + +class TestClass: + def test_class_with_init_not_collected(self, testdir): + modcol = testdir.getmodulecol(""" + class TestClass1: + def __init__(self): + pass + class TestClass2(object): + def __init__(self): + pass + """) + l = modcol.collect() + assert len(l) == 0 + +if py.std.sys.version_info > (3, 0): + _func_name_attr = "__name__" +else: + _func_name_attr = "func_name" + +class TestGenerator: + def test_generative_functions(self, testdir): + modcol = testdir.getmodulecol(""" + def func1(arg, arg2): + assert arg == arg2 + + def test_gen(): + yield func1, 17, 3*5 + yield func1, 42, 6*7 + """) + colitems = modcol.collect() + assert len(colitems) == 1 + gencol = colitems[0] + assert isinstance(gencol, py.test.collect.Generator) + gencolitems = gencol.collect() + assert len(gencolitems) == 2 + assert isinstance(gencolitems[0], py.test.collect.Function) + assert isinstance(gencolitems[1], py.test.collect.Function) + assert gencolitems[0].name == '[0]' + assert getattr(gencolitems[0].obj, _func_name_attr) == 'func1' + + def test_generative_methods(self, testdir): + modcol = testdir.getmodulecol(""" + def func1(arg, arg2): + assert arg == arg2 + class TestGenMethods: + def test_gen(self): + yield func1, 17, 3*5 + yield func1, 42, 6*7 + """) + gencol = modcol.collect()[0].collect()[0].collect()[0] + assert isinstance(gencol, py.test.collect.Generator) + gencolitems = gencol.collect() + assert len(gencolitems) == 2 + assert isinstance(gencolitems[0], py.test.collect.Function) + assert isinstance(gencolitems[1], py.test.collect.Function) + assert gencolitems[0].name == '[0]' + assert getattr(gencolitems[0].obj, _func_name_attr) == 'func1' + + def test_generative_functions_with_explicit_names(self, testdir): + modcol = testdir.getmodulecol(""" + def func1(arg, arg2): + assert arg == arg2 + + def test_gen(): + yield "seventeen", func1, 17, 3*5 + yield "fortytwo", func1, 42, 6*7 + """) + colitems = modcol.collect() + assert len(colitems) == 1 + gencol = colitems[0] + assert isinstance(gencol, py.test.collect.Generator) + gencolitems = gencol.collect() + assert len(gencolitems) == 2 + assert isinstance(gencolitems[0], py.test.collect.Function) + assert isinstance(gencolitems[1], py.test.collect.Function) + assert gencolitems[0].name == "['seventeen']" + assert getattr(gencolitems[0].obj, _func_name_attr) == 'func1' + assert gencolitems[1].name == "['fortytwo']" + assert getattr(gencolitems[1].obj, _func_name_attr) == 'func1' + + def test_generative_functions_unique_explicit_names(self, testdir): + # generative + modcol = testdir.getmodulecol(""" + def func(): pass + def test_gen(): + yield "name", func + yield "name", func + """) + colitems = modcol.collect() + assert len(colitems) == 1 + gencol = colitems[0] + assert isinstance(gencol, py.test.collect.Generator) + py.test.raises(ValueError, "gencol.collect()") + + def test_generative_methods_with_explicit_names(self, testdir): + modcol = testdir.getmodulecol(""" + def func1(arg, arg2): + assert arg == arg2 + class TestGenMethods: + def test_gen(self): + yield "m1", func1, 17, 3*5 + yield "m2", func1, 42, 6*7 + """) + gencol = modcol.collect()[0].collect()[0].collect()[0] + assert isinstance(gencol, py.test.collect.Generator) + gencolitems = gencol.collect() + assert len(gencolitems) == 2 + assert isinstance(gencolitems[0], py.test.collect.Function) + assert isinstance(gencolitems[1], py.test.collect.Function) + assert gencolitems[0].name == "['m1']" + assert getattr(gencolitems[0].obj, _func_name_attr) == 'func1' + assert gencolitems[1].name == "['m2']" + assert getattr(gencolitems[1].obj, _func_name_attr) == 'func1' + + def test_order_of_execution_generator_same_codeline(self, testdir, tmpdir): + o = testdir.makepyfile(""" + def test_generative_order_of_execution(): + import py + test_list = [] + expected_list = list(range(6)) + + def list_append(item): + test_list.append(item) + + def assert_order_of_execution(): + py.builtin.print_('expected order', expected_list) + py.builtin.print_('but got ', test_list) + assert test_list == expected_list + + for i in expected_list: + yield list_append, i + yield assert_order_of_execution + """) + reprec = testdir.inline_run(o) + passed, skipped, failed = reprec.countoutcomes() + assert passed == 7 + assert not skipped and not failed + + def test_order_of_execution_generator_different_codeline(self, testdir): + o = testdir.makepyfile(""" + def test_generative_tests_different_codeline(): + import py + test_list = [] + expected_list = list(range(3)) + + def list_append_2(): + test_list.append(2) + + def list_append_1(): + test_list.append(1) + + def list_append_0(): + test_list.append(0) + + def assert_order_of_execution(): + py.builtin.print_('expected order', expected_list) + py.builtin.print_('but got ', test_list) + assert test_list == expected_list + + yield list_append_0 + yield list_append_1 + yield list_append_2 + yield assert_order_of_execution + """) + reprec = testdir.inline_run(o) + passed, skipped, failed = reprec.countoutcomes() + assert passed == 4 + assert not skipped and not failed + +class TestFunction: + def test_getmodulecollector(self, testdir): + item = testdir.getitem("def test_func(): pass") + modcol = item.getparent(py.test.collect.Module) + assert isinstance(modcol, py.test.collect.Module) + assert hasattr(modcol.obj, 'test_func') + + def test_function_equality(self, testdir, tmpdir): + config = testdir.reparseconfig() + f1 = py.test.collect.Function(name="name", config=config, + args=(1,), callobj=isinstance) + f2 = py.test.collect.Function(name="name",config=config, + args=(1,), callobj=py.builtin.callable) + assert not f1 == f2 + assert f1 != f2 + f3 = py.test.collect.Function(name="name", config=config, + args=(1,2), callobj=py.builtin.callable) + assert not f3 == f2 + assert f3 != f2 + + assert not f3 == f1 + assert f3 != f1 + + f1_b = py.test.collect.Function(name="name", config=config, + args=(1,), callobj=isinstance) + assert f1 == f1_b + assert not f1 != f1_b + + def test_function_equality_with_callspec(self, testdir, tmpdir): + config = testdir.reparseconfig() + class callspec1: + param = 1 + funcargs = {} + id = "hello" + class callspec2: + param = 1 + funcargs = {} + id = "world" + collection = object() + f5 = py.test.collect.Function(name="name", config=config, + callspec=callspec1, callobj=isinstance, collection=collection) + f5b = py.test.collect.Function(name="name", config=config, + callspec=callspec2, callobj=isinstance, collection=collection) + assert f5 != f5b + assert not (f5 == f5b) + + def test_pyfunc_call(self, testdir): + item = testdir.getitem("def test_func(): raise ValueError") + config = item.config + class MyPlugin1: + def pytest_pyfunc_call(self, pyfuncitem): + raise ValueError + class MyPlugin2: + def pytest_pyfunc_call(self, pyfuncitem): + return True + config.pluginmanager.register(MyPlugin1()) + config.pluginmanager.register(MyPlugin2()) + config.hook.pytest_pyfunc_call(pyfuncitem=item) + +class TestSorting: + def test_check_equality(self, testdir): + modcol = testdir.getmodulecol(""" + def test_pass(): pass + def test_fail(): assert 0 + """) + fn1 = modcol.collect_by_name("test_pass") + assert isinstance(fn1, py.test.collect.Function) + fn2 = modcol.collect_by_name("test_pass") + assert isinstance(fn2, py.test.collect.Function) + + assert fn1 == fn2 + assert fn1 != modcol + if py.std.sys.version_info < (3, 0): + assert cmp(fn1, fn2) == 0 + assert hash(fn1) == hash(fn2) + + fn3 = modcol.collect_by_name("test_fail") + assert isinstance(fn3, py.test.collect.Function) + assert not (fn1 == fn3) + assert fn1 != fn3 + + for fn in fn1,fn2,fn3: + assert fn != 3 + assert fn != modcol + assert fn != [1,2,3] + assert [1,2,3] != fn + assert modcol != fn + + def test_allow_sane_sorting_for_decorators(self, testdir): + modcol = testdir.getmodulecol(""" + def dec(f): + g = lambda: f(2) + g.place_as = f + return g + + + def test_b(y): + pass + test_b = dec(test_b) + + def test_a(y): + pass + test_a = dec(test_a) + """) + colitems = modcol.collect() + assert len(colitems) == 2 + assert [item.name for item in colitems] == ['test_b', 'test_a'] + + +class TestConftestCustomization: + def test_pytest_pycollect_module(self, testdir): + testdir.makeconftest(""" + import py + class MyModule(py.test.collect.Module): + pass + def pytest_pycollect_makemodule(path, parent): + if path.basename == "test_xyz.py": + return MyModule(path, parent) + """) + testdir.makepyfile("def some(): pass") + testdir.makepyfile(test_xyz="") + result = testdir.runpytest("--collectonly") + result.stdout.fnmatch_lines([ + "*3 + + def test_traceback_error_during_import(self, testdir): + testdir.makepyfile(""" + x = 1 + x = 2 + x = 17 + asd + """) + result = testdir.runpytest() + assert result.ret != 0 + out = result.stdout.str() + assert "x = 1" not in out + assert "x = 2" not in out + result.stdout.fnmatch_lines([ + ">*asd*", + "E*NameError*", + ]) + result = testdir.runpytest("--fulltrace") + out = result.stdout.str() + assert "x = 1" in out + assert "x = 2" in out + result.stdout.fnmatch_lines([ + ">*asd*", + "E*NameError*", + ]) def test_getfuncargnames(): def f(): pass @@ -593,3 +1109,28 @@ def test_funcarg_lookup_error(testdir): "*1 error*", ]) assert "INTERNAL" not in result.stdout.str() + +class TestReportInfo: + def test_itemreport_reportinfo(self, testdir, linecomp): + testdir.makeconftest(""" + import py + class Function(py.test.collect.Function): + def reportinfo(self): + return "ABCDE", 42, "custom" + """) + item = testdir.getitem("def test_func(): pass") + runner = item.config.pluginmanager.getplugin("runner") + nodeinfo = runner.getitemnodeinfo(item) + assert nodeinfo.location == ("ABCDE", 42, "custom") + + def test_itemreport_pytest_report_iteminfo(self, testdir, linecomp): + item = testdir.getitem("def test_func(): pass") + tup = "FGHJ", 42, "custom" + class Plugin: + def pytest_report_iteminfo(self, item): + return tup + item.config.pluginmanager.register(Plugin()) + runner = runner = item.config.pluginmanager.getplugin("runner") + nodeinfo = runner.getitemnodeinfo(item) + location = nodeinfo.location + assert location == tup diff --git a/testing/plugin/test_pytest_resultlog.py b/testing/plugin/test_pytest_resultlog.py index 1ed512140..8171441c6 100644 --- a/testing/plugin/test_pytest_resultlog.py +++ b/testing/plugin/test_pytest_resultlog.py @@ -3,10 +3,12 @@ import os from py._plugin.pytest_resultlog import generic_path, ResultLog, \ pytest_configure, pytest_unconfigure from py._test.collect import Node, Item, FSCollector +from py._test.session import Collection def test_generic_path(testdir): config = testdir.parseconfig() - p1 = Node('a', parent=config._rootcol) + collection = Collection(config) + p1 = Node('a', config=config, collection=collection) #assert p1.fspath is None p2 = Node('B', parent=p1) p3 = Node('()', parent = p2) @@ -15,7 +17,7 @@ def test_generic_path(testdir): res = generic_path(item) assert res == 'a.B().c' - p0 = FSCollector('proj/test', parent=config._rootcol) + p0 = FSCollector('proj/test', config=config, collection=collection) p1 = FSCollector('proj/test/a', parent=p0) p2 = Node('B', parent=p1) p3 = Node('()', parent = p2) diff --git a/testing/plugin/test_pytest_runner.py b/testing/plugin/test_pytest_runner.py index 6716af23e..fe0bc3677 100644 --- a/testing/plugin/test_pytest_runner.py +++ b/testing/plugin/test_pytest_runner.py @@ -53,8 +53,8 @@ class BaseFunctionalTests: rep = reports[1] assert rep.passed assert not rep.failed - assert rep.shortrepr == "." - assert not hasattr(rep, 'longrepr') + assert rep.outcome == "passed" + assert not rep.longrepr def test_failfunction(self, testdir): reports = testdir.runitem(""" @@ -66,23 +66,8 @@ class BaseFunctionalTests: assert not rep.skipped assert rep.failed assert rep.when == "call" - assert isinstance(rep.longrepr, ReprExceptionInfo) - assert str(rep.shortrepr) == "F" - - def test_failfunction_customized_report(self, testdir, LineMatcher): - reports = testdir.runitem(""" - def test_func(): - assert 0 - """) - rep = reports[1] - rep.headerlines += ["hello world"] - tr = py.io.TerminalWriter(stringio=True) - rep.toterminal(tr) - val = tr.stringio.getvalue() - LineMatcher(val.split("\n")).fnmatch_lines([ - "*hello world", - "*def test_func():*" - ]) + assert rep.outcome == "failed" + #assert isinstance(rep.longrepr, ReprExceptionInfo) def test_skipfunction(self, testdir): reports = testdir.runitem(""" @@ -94,6 +79,7 @@ class BaseFunctionalTests: assert not rep.failed assert not rep.passed assert rep.skipped + assert rep.outcome == "skipped" #assert rep.skipped.when == "call" #assert rep.skipped.when == "call" #assert rep.skipped == "%sreason == "hello" @@ -150,8 +136,8 @@ class BaseFunctionalTests: assert not rep.passed assert rep.failed assert rep.when == "teardown" - assert rep.longrepr.reprcrash.lineno == 3 - assert rep.longrepr.reprtraceback.reprentries + #assert rep.longrepr.reprcrash.lineno == 3 + #assert rep.longrepr.reprtraceback.reprentries def test_custom_failure_repr(self, testdir): testdir.makepyfile(conftest=""" @@ -270,6 +256,10 @@ class TestCollectionReports: assert not rep.failed assert not rep.skipped assert rep.passed + locinfo = rep.location + assert locinfo[0] == col.fspath + assert not locinfo[1] + assert locinfo[2] == col.fspath res = rep.result assert len(res) == 2 assert res[0].name == "test_func1" @@ -299,7 +289,7 @@ def test_callinfo(): assert "exc" in repr(ci) # design question: do we want general hooks in python files? -# following passes if withpy defaults to True in pycoll.PyObjMix._getplugins() +# then something like the following functional tests makes sense @py.test.mark.xfail def test_runtest_in_module_ordering(testdir): p1 = testdir.makepyfile(""" diff --git a/testing/plugin/test_pytest_skipping.py b/testing/plugin/test_pytest_skipping.py index 7f8f8adb1..73c855724 100644 --- a/testing/plugin/test_pytest_skipping.py +++ b/testing/plugin/test_pytest_skipping.py @@ -183,8 +183,10 @@ class TestXFail: """) result = testdir.runpytest(p, '--report=xfailed', ) result.stdout.fnmatch_lines([ - "*test_one*test_this*NOTRUN*noway", - "*test_one*test_this_true*NOTRUN*condition:*True*", + "*test_one*test_this*", + "*NOTRUN*noway", + "*test_one*test_this_true*", + "*NOTRUN*condition:*True*", "*1 passed*", ]) @@ -199,7 +201,8 @@ class TestXFail: """) result = testdir.runpytest(p, '--report=xfailed', ) result.stdout.fnmatch_lines([ - "*test_one*test_this*NOTRUN*hello", + "*test_one*test_this*", + "*NOTRUN*hello", "*1 xfailed*", ]) @@ -229,7 +232,8 @@ class TestXFail: ]) result = testdir.runpytest(p, "-rx") result.stdout.fnmatch_lines([ - "*XFAIL*test_this*reason:*hello*", + "*XFAIL*test_this*", + "*reason:*hello*", ]) result = testdir.runpytest(p, "--runxfail") result.stdout.fnmatch_lines([ @@ -252,7 +256,8 @@ class TestXFail: ]) result = testdir.runpytest(p, "-rx") result.stdout.fnmatch_lines([ - "*XFAIL*test_this*reason:*hello*", + "*XFAIL*test_this*", + "*reason:*hello*", ]) result = testdir.runpytest(p, "--runxfail") result.stdout.fnmatch_lines([ @@ -286,7 +291,8 @@ class TestXFail: """) result = testdir.runpytest(p, '-rxX') result.stdout.fnmatch_lines([ - "*XFAIL*test_this*NOTRUN*", + "*XFAIL*test_this*", + "*NOTRUN*", ]) def test_dynamic_xfail_set_during_funcarg_setup(self, testdir): @@ -360,7 +366,6 @@ def test_skipif_class(testdir): def test_skip_reasons_folding(): - from py._plugin import pytest_runner as runner from py._plugin.pytest_skipping import folded_skips class longrepr: class reprcrash: @@ -368,12 +373,15 @@ def test_skip_reasons_folding(): lineno = 3 message = "justso" - ev1 = runner.CollectReport(None, None) + class X: + pass + ev1 = X() ev1.when = "execute" ev1.skipped = True ev1.longrepr = longrepr - ev2 = runner.ItemTestReport(None, excinfo=longrepr) + ev2 = X() + ev2.longrepr = longrepr ev2.skipped = True l = folded_skips([ev1, ev2]) @@ -408,8 +416,8 @@ def test_skipped_reasons_functional(testdir): ) result = testdir.runpytest('--report=skipped') result.stdout.fnmatch_lines([ - "*test_one.py ss", "*test_two.py S", + "*test_one.py ss", "*SKIP*3*conftest.py:3: 'test'", ]) assert result.ret == 0 diff --git a/testing/plugin/test_pytest_terminal.py b/testing/plugin/test_pytest_terminal.py index 28a522a32..c54e5729d 100644 --- a/testing/plugin/test_pytest_terminal.py +++ b/testing/plugin/test_pytest_terminal.py @@ -89,60 +89,28 @@ class TestTerminal: assert lines[1].endswith("xy.py .") assert lines[2] == "hello world" - def test_testid(self, testdir, linecomp): - func,method = testdir.getitems(""" - def test_func(): - pass - class TestClass: - def test_method(self): - pass + def test_show_runtest_logstart(self, testdir, linecomp): + item = testdir.getitem("def test_func(): pass") + tr = TerminalReporter(item.config, file=linecomp.stringio) + item.config.pluginmanager.register(tr) + nodeid = item.collection.getid(item) + location = item.ihook.pytest_report_iteminfo(item=item) + tr.config.hook.pytest_runtest_logstart(nodeid=nodeid, + location=location, fspath=str(item.fspath)) + linecomp.assert_contains_lines([ + "*test_show_runtest_logstart.py*" + ]) + + def test_runtest_location_shown_before_test_starts(self, testdir): + p1 = testdir.makepyfile(""" + def test_1(): + import time + time.sleep(20) """) - tr = TerminalReporter(func.config, file=linecomp.stringio) - id = tr.gettestid(func) - assert id.endswith("test_testid.py::test_func") - fspath = py.path.local(id.split("::")[0]) - assert fspath.check() - id = tr.gettestid(method) - assert id.endswith("test_testid.py::TestClass::test_method") - - def test_show_path_before_running_test(self, testdir, linecomp): - item = testdir.getitem("def test_func(): pass") - tr = TerminalReporter(item.config, file=linecomp.stringio) - item.config.pluginmanager.register(tr) - tr.config.hook.pytest_itemstart(item=item) - linecomp.assert_contains_lines([ - "*test_show_path_before_running_test.py*" - ]) - - def test_itemreport_reportinfo(self, testdir, linecomp): - testdir.makeconftest(""" - import py - class Function(py.test.collect.Function): - def reportinfo(self): - return "ABCDE", 42, "custom" - """) - item = testdir.getitem("def test_func(): pass") - tr = TerminalReporter(item.config, file=linecomp.stringio) - item.config.pluginmanager.register(tr) - tr.config.option.verbose = True - tr.config.hook.pytest_itemstart(item=item) - linecomp.assert_contains_lines([ - "*ABCDE:43: custom*" - ]) - - def test_itemreport_pytest_report_iteminfo(self, testdir, linecomp): - item = testdir.getitem("def test_func(): pass") - class Plugin: - def pytest_report_iteminfo(self, item): - return "FGHJ", 42, "custom" - item.config.pluginmanager.register(Plugin()) - tr = TerminalReporter(item.config, file=linecomp.stringio) - item.config.pluginmanager.register(tr) - tr.config.option.verbose = True - tr.config.hook.pytest_itemstart(item=item) - linecomp.assert_contains_lines([ - "*FGHJ:43: custom*" - ]) + child = testdir.spawn_pytest("") + child.expect(".*test_runtest_location.*py") + child.sendeof() + child.kill(15) def test_itemreport_subclasses_show_subclassed_file(self, testdir): p1 = testdir.makepyfile(test_p1=""" @@ -206,12 +174,12 @@ class TestCollectonly: "" ]) item = modcol.join("test_func") - rep.config.hook.pytest_itemstart(item=item) + rep.config.hook.pytest_log_itemcollect(item=item) linecomp.assert_contains_lines([ " ", ]) - rep.config.hook.pytest_collectreport( - report=runner.CollectReport(modcol, [], excinfo=None)) + report = rep.config.hook.pytest_make_collect_report(collector=modcol) + rep.config.hook.pytest_collectreport(report=report) assert rep.indent == indent def test_collectonly_skipped_module(self, testdir, linecomp): @@ -264,13 +232,13 @@ class TestCollectonly: stderr = result.stderr.str().strip() #assert stderr.startswith("inserting into sys.path") assert result.ret == 0 - extra = result.stdout.fnmatch_lines(py.code.Source(""" - - - - - - """).strip()) + extra = result.stdout.fnmatch_lines([ + "*", + "* ", + "* ", + "* ", + "* ", + ]) def test_collectonly_error(self, testdir): p = testdir.makepyfile("import Errlkjqweqwe") @@ -278,9 +246,9 @@ class TestCollectonly: stderr = result.stderr.str().strip() assert result.ret == 1 extra = result.stdout.fnmatch_lines(py.code.Source(""" - + * *ImportError* - !!!*failures*!!! + *!!!*failures*!!! *test_collectonly_error.py:1* """).strip()) @@ -454,6 +422,7 @@ class TestTerminalFunctional: "*test_verbose_reporting.py:10: test_gen*FAIL*", ]) assert result.ret == 1 + py.test.xfail("fix dist-testing") pytestconfig.pluginmanager.skipifmissing("xdist") result = testdir.runpytest(p1, '-v', '-n 1') result.stdout.fnmatch_lines([ diff --git a/testing/plugin/test_pytest_tmpdir.py b/testing/plugin/test_pytest_tmpdir.py index 692956f45..d19088d91 100644 --- a/testing/plugin/test_pytest_tmpdir.py +++ b/testing/plugin/test_pytest_tmpdir.py @@ -1,7 +1,7 @@ from py._plugin.pytest_tmpdir import pytest_funcarg__tmpdir +from py._plugin.pytest_python import FuncargRequest def test_funcarg(testdir): - from py._test.funcargs import FuncargRequest item = testdir.getitem("def test_func(tmpdir): pass") p = pytest_funcarg__tmpdir(FuncargRequest(item)) assert p.check() diff --git a/testing/test_collect.py b/testing/test_collect.py index 43e611f53..7f67152df 100644 --- a/testing/test_collect.py +++ b/testing/test_collect.py @@ -59,19 +59,18 @@ class TestCollector: import py class CustomFile(py.test.collect.File): pass - class MyDirectory(py.test.collect.Directory): - def collect(self): - return [CustomFile(self.fspath.join("hello.xxx"), parent=self)] - def pytest_collect_directory(path, parent): - return MyDirectory(path, parent=parent) + def pytest_collect_file(path, parent): + if path.ext == ".xxx": + return CustomFile(path, parent=parent) """) config = testdir.parseconfig(hello) - node = config.getnode(hello) + node = testdir.getnode(config, hello) assert isinstance(node, py.test.collect.File) assert node.name == "hello.xxx" - names = config._rootcol.totrail(node) - node = config._rootcol.getbynames(names) - assert isinstance(node, py.test.collect.File) + id = node.collection.getid(node) + nodes = node.collection.getbyid(id) + assert len(nodes) == 1 + assert isinstance(nodes[0], py.test.collect.File) class TestCollectFS: def test_ignored_certain_directories(self, testdir): @@ -84,7 +83,7 @@ class TestCollectFS: tmpdir.ensure("normal", 'test_found.py') tmpdir.ensure('test_found.py') - col = testdir.parseconfig(tmpdir).getnode(tmpdir) + col = testdir.getnode(testdir.parseconfig(tmpdir), tmpdir) items = col.collect() names = [x.name for x in items] assert len(items) == 2 @@ -93,7 +92,7 @@ class TestCollectFS: def test_found_certain_testfiles(self, testdir): p1 = testdir.makepyfile(test_found = "pass", found_test="pass") - col = testdir.parseconfig(p1).getnode(p1.dirpath()) + col = testdir.getnode(testdir.parseconfig(p1), p1.dirpath()) items = col.collect() # Directory collect returns files sorted by name assert len(items) == 2 assert items[1].name == 'test_found.py' @@ -106,7 +105,7 @@ class TestCollectFS: testdir.makepyfile(test_two="hello") p1.dirpath().mkdir("dir2") config = testdir.parseconfig() - col = config.getnode(p1.dirpath()) + col = testdir.getnode(config, p1.dirpath()) names = [x.name for x in col.collect()] assert names == ["dir1", "dir2", "test_one.py", "test_two.py", "x"] @@ -120,7 +119,7 @@ class TestCollectPluginHookRelay: config = testdir.Config() config.pluginmanager.register(Plugin()) config.parse([tmpdir]) - col = config.getnode(tmpdir) + col = testdir.getnode(config, tmpdir) testdir.makefile(".abc", "xyz") res = col.collect() assert len(wascalled) == 1 @@ -141,7 +140,7 @@ class TestCollectPluginHookRelay: assert "world" in wascalled # make sure the directories do not get double-appended colreports = reprec.getreports("pytest_collectreport") - names = [rep.collector.name for rep in colreports] + names = [rep.nodenames[-1] for rep in colreports] assert names.count("hello") == 1 class TestPrunetraceback: @@ -181,6 +180,7 @@ class TestPrunetraceback: "*hello world*", ]) + @py.test.mark.xfail(reason="other mechanism for adding to reporting needed") def test_collect_report_postprocessing(self, testdir): p = testdir.makepyfile(""" import not_exists @@ -227,16 +227,18 @@ class TestCustomConftests: testdir.mkdir("hello") testdir.makepyfile(test_world="#") reprec = testdir.inline_run(testdir.tmpdir) - names = [rep.collector.name for rep in reprec.getreports("pytest_collectreport")] + names = [rep.nodenames[-1] + for rep in reprec.getreports("pytest_collectreport")] assert 'hello' not in names assert 'test_world.py' not in names reprec = testdir.inline_run(testdir.tmpdir, "--XX") - names = [rep.collector.name for rep in reprec.getreports("pytest_collectreport")] + names = [rep.nodenames[-1] + for rep in reprec.getreports("pytest_collectreport")] assert 'hello' in names assert 'test_world.py' in names def test_pytest_fs_collect_hooks_are_seen(self, testdir): - testdir.makeconftest(""" + conf = testdir.makeconftest(""" import py class MyDirectory(py.test.collect.Directory): pass @@ -247,79 +249,11 @@ class TestCustomConftests: def pytest_collect_file(path, parent): return MyModule(path, parent) """) - testdir.makepyfile("def test_x(): pass") + sub = testdir.mkdir("sub") + p = testdir.makepyfile("def test_x(): pass") result = testdir.runpytest("--collectonly") result.stdout.fnmatch_lines([ "*MyDirectory*", "*MyModule*", "*test_x*" ]) - -class TestRootCol: - def test_totrail_and_back(self, testdir, tmpdir): - a = tmpdir.ensure("a", dir=1) - tmpdir.ensure("a", "__init__.py") - x = tmpdir.ensure("a", "trail.py") - config = testdir.reparseconfig([x]) - col = config.getnode(x) - trail = config._rootcol.totrail(col) - col2 = config._rootcol.fromtrail(trail) - assert col2 == col - - @py.test.mark.xfail(reason="http://bitbucket.org/hpk42/py-trunk/issue/109") - def test_sibling_conftest_issue109(self, testdir): - """ - This test is to make sure that the conftest.py of sibling directories is not loaded - if py.test is run for/in one of the siblings directory and those sibling directories - are not packaged together with an __init__.py. See bitbucket issue #109. - """ - for dirname in ['a', 'b']: - testdir.tmpdir.ensure(dirname, dir=True) - testdir.tmpdir.ensure(dirname, '__init__.py') - - # To create the conftest.py I would like to use testdir.make*-methods - # but as far as I have seen they can only create files in testdir.tempdir - # Maybe there is a way to explicitly specifiy the directory on which those - # methods work or a completely better way to do that? - backupTmpDir = testdir.tmpdir - testdir.tmpdir = testdir.tmpdir.join(dirname) - testdir.makeconftest(""" - _DIR_NAME = '%s' - def pytest_configure(config): - if config.args and config.args[0] != _DIR_NAME: - raise Exception("py.test run for '" + config.args[0] + "', but '" + _DIR_NAME + "/conftest.py' loaded.") - """ % dirname) - testdir.tmpdir = backupTmpDir - - for dirname, other_dirname in [('a', 'b'), ('b', 'a')]: - result = testdir.runpytest(dirname) - assert result.ret == 0, "test_sibling_conftest: py.test run for '%s', but '%s/conftest.py' loaded." % (dirname, other_dirname) - - def test_totrail_topdir_and_beyond(self, testdir, tmpdir): - config = testdir.reparseconfig() - col = config.getnode(config.topdir) - trail = config._rootcol.totrail(col) - col2 = config._rootcol.fromtrail(trail) - assert col2.fspath == config.topdir - assert len(col2.listchain()) == 1 - py.test.raises(config.Error, "config.getnode(config.topdir.dirpath())") - #col3 = config.getnode(config.topdir.dirpath()) - #py.test.raises(ValueError, - # "col3._totrail()") - - def test_argid(self, testdir, tmpdir): - cfg = testdir.parseconfig() - p = testdir.makepyfile("def test_func(): pass") - item = cfg.getnode("%s::test_func" % p) - assert item.name == "test_func" - - def test_argid_with_method(self, testdir, tmpdir): - cfg = testdir.parseconfig() - p = testdir.makepyfile(""" - class TestClass: - def test_method(self): pass - """) - item = cfg.getnode("%s::TestClass::()::test_method" % p) - assert item.name == "test_method" - item = cfg.getnode("%s::TestClass::test_method" % p) - assert item.name == "test_method" diff --git a/testing/test_collection.py b/testing/test_collection.py new file mode 100644 index 000000000..a346ad4e0 --- /dev/null +++ b/testing/test_collection.py @@ -0,0 +1,314 @@ +import py + +from py._test.session import Collection, gettopdir + +class TestCollection: + def test_parsearg(self, testdir): + p = testdir.makepyfile("def test_func(): pass") + subdir = testdir.mkdir("sub") + subdir.ensure("__init__.py") + target = subdir.join(p.basename) + p.move(target) + testdir.chdir() + subdir.chdir() + config = testdir.parseconfig(p.basename) + rcol = Collection(config=config) + assert rcol.topdir == testdir.tmpdir + parts = rcol._parsearg(p.basename) + assert parts[0] == "sub" + assert parts[1] == p.basename + assert len(parts) == 2 + parts = rcol._parsearg(p.basename + "::test_func") + assert parts[0] == "sub" + assert parts[1] == p.basename + assert parts[2] == "test_func" + assert len(parts) == 3 + + def test_collect_topdir(self, testdir): + p = testdir.makepyfile("def test_func(): pass") + id = "::".join([p.basename, "test_func"]) + config = testdir.parseconfig(id) + topdir = testdir.tmpdir + rcol = Collection(config) + assert topdir == rcol.topdir + hookrec = testdir.getreportrecorder(config) + items = rcol.perform_collect() + assert len(items) == 1 + root = items[0].listchain()[0] + root_id = rcol.getid(root) + root2 = rcol.getbyid(root_id)[0] + assert root2.fspath == root.fspath + + def test_collect_protocol_single_function(self, testdir): + p = testdir.makepyfile("def test_func(): pass") + id = "::".join([p.basename, "test_func"]) + config = testdir.parseconfig(id) + topdir = testdir.tmpdir + rcol = Collection(config) + assert topdir == rcol.topdir + hookrec = testdir.getreportrecorder(config) + items = rcol.perform_collect() + assert len(items) == 1 + item = items[0] + assert item.name == "test_func" + newid = rcol.getid(item) + assert newid == id + py.std.pprint.pprint(hookrec.hookrecorder.calls) + hookrec.hookrecorder.contains([ + ("pytest_collectstart", "collector.fspath == topdir"), + ("pytest_make_collect_report", "collector.fspath == topdir"), + ("pytest_collectstart", "collector.fspath == p"), + ("pytest_make_collect_report", "collector.fspath == p"), + ("pytest_pycollect_makeitem", "name == 'test_func'"), + ("pytest_collectreport", "report.fspath == p"), + ("pytest_collectreport", "report.fspath == topdir") + ]) + + def test_collect_protocol_method(self, testdir): + p = testdir.makepyfile(""" + class TestClass: + def test_method(self): + pass + """) + normid = p.basename + "::TestClass::test_method" + for id in [p.basename, + p.basename + "::TestClass", + p.basename + "::TestClass::()", + p.basename + "::TestClass::()::test_method", + normid, + ]: + config = testdir.parseconfig(id) + rcol = Collection(config=config) + nodes = rcol.perform_collect() + assert len(nodes) == 1 + assert nodes[0].name == "test_method" + newid = rcol.getid(nodes[0]) + assert newid == normid + + def test_collect_custom_nodes_multi_id(self, testdir): + p = testdir.makepyfile("def test_func(): pass") + testdir.makeconftest(""" + import py + class SpecialItem(py.test.collect.Item): + def runtest(self): + return # ok + class SpecialFile(py.test.collect.File): + def collect(self): + return [SpecialItem(name="check", parent=self)] + def pytest_collect_file(path, parent): + if path.basename == %r: + return SpecialFile(fspath=path, parent=parent) + """ % p.basename) + id = p.basename + + config = testdir.parseconfig(id) + rcol = Collection(config) + hookrec = testdir.getreportrecorder(config) + items = rcol.perform_collect() + py.std.pprint.pprint(hookrec.hookrecorder.calls) + assert len(items) == 2 + hookrec.hookrecorder.contains([ + ("pytest_collectstart", + "collector.fspath == collector.collection.topdir"), + ("pytest_collectstart", + "collector.__class__.__name__ == 'SpecialFile'"), + ("pytest_collectstart", + "collector.__class__.__name__ == 'Module'"), + ("pytest_pycollect_makeitem", "name == 'test_func'"), + ("pytest_collectreport", "report.fspath == p"), + ("pytest_collectreport", + "report.fspath == %r" % str(rcol.topdir)), + ]) + + def test_collect_subdir_event_ordering(self, testdir): + p = testdir.makepyfile("def test_func(): pass") + aaa = testdir.mkpydir("aaa") + test_aaa = aaa.join("test_aaa.py") + p.move(test_aaa) + config = testdir.parseconfig() + rcol = Collection(config) + hookrec = testdir.getreportrecorder(config) + items = rcol.perform_collect() + assert len(items) == 1 + py.std.pprint.pprint(hookrec.hookrecorder.calls) + hookrec.hookrecorder.contains([ + ("pytest_collectstart", "collector.fspath == aaa"), + ("pytest_collectstart", "collector.fspath == test_aaa"), + ("pytest_pycollect_makeitem", "name == 'test_func'"), + ("pytest_collectreport", "report.fspath == test_aaa"), + ("pytest_collectreport", "report.fspath == aaa"), + ]) + + def test_collect_two_commandline_args(self, testdir): + p = testdir.makepyfile("def test_func(): pass") + aaa = testdir.mkpydir("aaa") + bbb = testdir.mkpydir("bbb") + p.copy(aaa.join("test_aaa.py")) + p.move(bbb.join("test_bbb.py")) + + id = "." + config = testdir.parseconfig(id) + rcol = Collection(config) + hookrec = testdir.getreportrecorder(config) + items = rcol.perform_collect() + assert len(items) == 2 + py.std.pprint.pprint(hookrec.hookrecorder.calls) + hookrec.hookrecorder.contains([ + ("pytest_collectstart", "collector.fspath == aaa"), + ("pytest_pycollect_makeitem", "name == 'test_func'"), + ("pytest_collectreport", "report.fspath == aaa"), + ("pytest_collectstart", "collector.fspath == bbb"), + ("pytest_pycollect_makeitem", "name == 'test_func'"), + ("pytest_collectreport", "report.fspath == bbb"), + ]) + + def test_serialization_byid(self, testdir): + p = testdir.makepyfile("def test_func(): pass") + config = testdir.parseconfig() + rcol = Collection(config) + items = rcol.perform_collect() + assert len(items) == 1 + item, = items + id = rcol.getid(item) + newcol = Collection(config) + item2, = newcol.getbyid(id) + assert item2.name == item.name + assert item2.fspath == item.fspath + item2b, = newcol.getbyid(id) + assert item2b is item2 + +class Test_gettopdir: + def test_gettopdir(self, testdir): + tmp = testdir.tmpdir + assert gettopdir([tmp]) == tmp + topdir = gettopdir([tmp.join("hello"), tmp.join("world")]) + assert topdir == tmp + somefile = tmp.ensure("somefile.py") + assert gettopdir([somefile]) == tmp + + def test_gettopdir_pypkg(self, testdir): + tmp = testdir.tmpdir + a = tmp.ensure('a', dir=1) + b = tmp.ensure('a', 'b', '__init__.py') + c = tmp.ensure('a', 'b', 'c.py') + Z = tmp.ensure('Z', dir=1) + assert gettopdir([c]) == a + assert gettopdir([c, Z]) == tmp + assert gettopdir(["%s::xyc" % c]) == a + assert gettopdir(["%s::xyc::abc" % c]) == a + assert gettopdir(["%s::xyc" % c, "%s::abc" % Z]) == tmp + +class Test_getinitialnodes: + def test_onedir(self, testdir): + config = testdir.reparseconfig([testdir.tmpdir]) + colitems = Collection(config).getinitialnodes() + assert len(colitems) == 1 + col = colitems[0] + assert isinstance(col, py.test.collect.Directory) + for col in col.listchain(): + assert col.config is config + + def test_twodirs(self, testdir, tmpdir): + config = testdir.reparseconfig([tmpdir, tmpdir]) + colitems = Collection(config).getinitialnodes() + assert len(colitems) == 2 + col1, col2 = colitems + assert col1.name == col2.name + assert col1.parent == col2.parent + + def test_curdir_and_subdir(self, testdir, tmpdir): + a = tmpdir.ensure("a", dir=1) + config = testdir.reparseconfig([tmpdir, a]) + colitems = Collection(config).getinitialnodes() + assert len(colitems) == 2 + col1, col2 = colitems + assert col1.name == tmpdir.basename + assert col2.name == 'a' + for col in colitems: + for subcol in col.listchain(): + assert col.config is config + + def test_global_file(self, testdir, tmpdir): + x = tmpdir.ensure("x.py") + config = testdir.reparseconfig([x]) + col, = Collection(config).getinitialnodes() + assert isinstance(col, py.test.collect.Module) + assert col.name == 'x.py' + assert col.parent.name == tmpdir.basename + assert col.parent.parent is None + for col in col.listchain(): + assert col.config is config + + def test_global_dir(self, testdir, tmpdir): + x = tmpdir.ensure("a", dir=1) + config = testdir.reparseconfig([x]) + col, = Collection(config).getinitialnodes() + assert isinstance(col, py.test.collect.Directory) + print(col.listchain()) + assert col.name == 'a' + assert col.parent is None + assert col.config is config + + def test_pkgfile(self, testdir, tmpdir): + tmpdir = tmpdir.join("subdir") + x = tmpdir.ensure("x.py") + tmpdir.ensure("__init__.py") + config = testdir.reparseconfig([x]) + col, = Collection(config).getinitialnodes() + assert isinstance(col, py.test.collect.Module) + assert col.name == 'x.py' + assert col.parent.name == x.dirpath().basename + assert col.parent.parent.parent is None + for col in col.listchain(): + assert col.config is config + +class Test_genitems: + def test_check_collect_hashes(self, testdir): + p = testdir.makepyfile(""" + def test_1(): + pass + + def test_2(): + pass + """) + p.copy(p.dirpath(p.purebasename + "2" + ".py")) + items, reprec = testdir.inline_genitems(p.dirpath()) + assert len(items) == 4 + for numi, i in enumerate(items): + for numj, j in enumerate(items): + if numj != numi: + assert hash(i) != hash(j) + assert i != j + + def test_root_conftest_syntax_error(self, testdir): + # do we want to unify behaviour with + # test_subdir_conftest_error? + p = testdir.makepyfile(conftest="raise SyntaxError\n") + py.test.raises(SyntaxError, testdir.inline_genitems, p.dirpath()) + + def test_example_items1(self, testdir): + p = testdir.makepyfile(''' + def testone(): + pass + + class TestX: + def testmethod_one(self): + pass + + class TestY(TestX): + pass + ''') + items, reprec = testdir.inline_genitems(p) + assert len(items) == 3 + assert items[0].name == 'testone' + assert items[1].name == 'testmethod_one' + assert items[2].name == 'testmethod_one' + + # let's also test getmodpath here + assert items[0].getmodpath() == "testone" + assert items[1].getmodpath() == "TestX.testmethod_one" + assert items[2].getmodpath() == "TestY.testmethod_one" + + s = items[0].getmodpath(stopatmodule=False) + assert s.endswith("test_example_items1.testone") + print(s) diff --git a/testing/test_config.py b/testing/test_config.py index c80aae237..2c839866a 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,6 +1,4 @@ import py -from py._test.collect import RootCollector - class TestConfigCmdlineParsing: def test_parser_addoption_default_env(self, testdir, monkeypatch): @@ -106,104 +104,6 @@ class TestConfigAPI: assert pl[0] == tmpdir assert pl[1] == somepath - def test_setsessionclass_and_initsession(self, testdir): - config = testdir.Config() - class Session1: - def __init__(self, config): - self.config = config - config.setsessionclass(Session1) - session = config.initsession() - assert isinstance(session, Session1) - assert session.config is config - py.test.raises(ValueError, "config.setsessionclass(Session1)") - - -class TestConfigApi_getinitialnodes: - def test_onedir(self, testdir): - config = testdir.reparseconfig([testdir.tmpdir]) - colitems = config.getinitialnodes() - assert len(colitems) == 1 - col = colitems[0] - assert isinstance(col, py.test.collect.Directory) - for col in col.listchain(): - assert col.config is config - - def test_twodirs(self, testdir, tmpdir): - config = testdir.reparseconfig([tmpdir, tmpdir]) - colitems = config.getinitialnodes() - assert len(colitems) == 2 - col1, col2 = colitems - assert col1.name == col2.name - assert col1.parent == col2.parent - - def test_curdir_and_subdir(self, testdir, tmpdir): - a = tmpdir.ensure("a", dir=1) - config = testdir.reparseconfig([tmpdir, a]) - colitems = config.getinitialnodes() - assert len(colitems) == 2 - col1, col2 = colitems - assert col1.name == tmpdir.basename - assert col2.name == 'a' - for col in colitems: - for subcol in col.listchain(): - assert col.config is config - - def test_global_file(self, testdir, tmpdir): - x = tmpdir.ensure("x.py") - config = testdir.reparseconfig([x]) - col, = config.getinitialnodes() - assert isinstance(col, py.test.collect.Module) - assert col.name == 'x.py' - assert col.parent.name == tmpdir.basename - assert isinstance(col.parent.parent, RootCollector) - for col in col.listchain(): - assert col.config is config - - def test_global_dir(self, testdir, tmpdir): - x = tmpdir.ensure("a", dir=1) - config = testdir.reparseconfig([x]) - col, = config.getinitialnodes() - assert isinstance(col, py.test.collect.Directory) - print(col.listchain()) - assert col.name == 'a' - assert isinstance(col.parent, RootCollector) - assert col.config is config - - def test_pkgfile(self, testdir, tmpdir): - tmpdir = tmpdir.join("subdir") - x = tmpdir.ensure("x.py") - tmpdir.ensure("__init__.py") - config = testdir.reparseconfig([x]) - col, = config.getinitialnodes() - assert isinstance(col, py.test.collect.Module) - assert col.name == 'x.py' - assert col.parent.name == x.dirpath().basename - assert isinstance(col.parent.parent.parent, RootCollector) - for col in col.listchain(): - assert col.config is config - -class TestConfig_gettopdir: - def test_gettopdir(self, testdir): - from py._test.config import gettopdir - tmp = testdir.tmpdir - assert gettopdir([tmp]) == tmp - topdir = gettopdir([tmp.join("hello"), tmp.join("world")]) - assert topdir == tmp - somefile = tmp.ensure("somefile.py") - assert gettopdir([somefile]) == tmp - - def test_gettopdir_pypkg(self, testdir): - from py._test.config import gettopdir - tmp = testdir.tmpdir - a = tmp.ensure('a', dir=1) - b = tmp.ensure('a', 'b', '__init__.py') - c = tmp.ensure('a', 'b', 'c.py') - Z = tmp.ensure('Z', dir=1) - assert gettopdir([c]) == a - assert gettopdir([c, Z]) == tmp - assert gettopdir(["%s::xyc" % c]) == a - assert gettopdir(["%s::xyc::abc" % c]) == a - assert gettopdir(["%s::xyc" % c, "%s::abc" % Z]) == tmp def test_options_on_small_file_do_not_blow_up(testdir): def runfiletest(opts): @@ -247,133 +147,3 @@ def test_preparse_ordering(testdir, monkeypatch): config = testdir.parseconfig() plugin = config.pluginmanager.getplugin("mytestplugin") assert plugin.x == 42 - - -import pickle -class TestConfigPickling: - def pytest_funcarg__testdir(self, request): - oldconfig = py.test.config - print("setting py.test.config to None") - py.test.config = None - def resetglobals(): - py.builtin.print_("setting py.test.config to", oldconfig) - py.test.config = oldconfig - request.addfinalizer(resetglobals) - return request.getfuncargvalue("testdir") - - def test_config_getstate_setstate(self, testdir): - from py._test.config import Config - testdir.makepyfile(__init__="", conftest="x=1; y=2") - hello = testdir.makepyfile(hello="") - tmp = testdir.tmpdir - testdir.chdir() - config1 = testdir.parseconfig(hello) - config2 = Config() - config2.__setstate__(config1.__getstate__()) - assert config2.topdir == py.path.local() - config2_relpaths = [py.path.local(x).relto(config2.topdir) - for x in config2.args] - config1_relpaths = [py.path.local(x).relto(config1.topdir) - for x in config1.args] - - assert config2_relpaths == config1_relpaths - for name, value in config1.option.__dict__.items(): - assert getattr(config2.option, name) == value - assert config2.getvalue("x") == 1 - - def test_config_pickling_customoption(self, testdir): - testdir.makeconftest(""" - def pytest_addoption(parser): - group = parser.getgroup("testing group") - group.addoption('-G', '--glong', action="store", default=42, - type="int", dest="gdest", help="g value.") - """) - config = testdir.parseconfig("-G", "11") - assert config.option.gdest == 11 - repr = config.__getstate__() - - config = testdir.Config() - py.test.raises(AttributeError, "config.option.gdest") - - config2 = testdir.Config() - config2.__setstate__(repr) - assert config2.option.gdest == 11 - - def test_config_pickling_and_conftest_deprecated(self, testdir): - tmp = testdir.tmpdir.ensure("w1", "w2", dir=1) - tmp.ensure("__init__.py") - tmp.join("conftest.py").write(py.code.Source(""" - def pytest_addoption(parser): - group = parser.getgroup("testing group") - group.addoption('-G', '--glong', action="store", default=42, - type="int", dest="gdest", help="g value.") - """)) - config = testdir.parseconfig(tmp, "-G", "11") - assert config.option.gdest == 11 - repr = config.__getstate__() - - config = testdir.Config() - py.test.raises(AttributeError, "config.option.gdest") - - config2 = testdir.Config() - config2.__setstate__(repr) - assert config2.option.gdest == 11 - - option = config2.addoptions("testing group", - config2.Option('-G', '--glong', action="store", default=42, - type="int", dest="gdest", help="g value.")) - assert option.gdest == 11 - - def test_config_picklability(self, testdir): - config = testdir.parseconfig() - s = pickle.dumps(config) - newconfig = pickle.loads(s) - assert hasattr(newconfig, "topdir") - assert newconfig.topdir == py.path.local() - - def test_collector_implicit_config_pickling(self, testdir): - tmpdir = testdir.tmpdir - testdir.chdir() - testdir.makepyfile(hello="def test_x(): pass") - config = testdir.parseconfig(tmpdir) - col = config.getnode(config.topdir) - io = py.io.BytesIO() - pickler = pickle.Pickler(io) - pickler.dump(col) - io.seek(0) - unpickler = pickle.Unpickler(io) - col2 = unpickler.load() - assert col2.name == col.name - assert col2.listnames() == col.listnames() - - def test_config_and_collector_pickling(self, testdir): - tmpdir = testdir.tmpdir - dir1 = tmpdir.ensure("sourcedir", "somedir", dir=1) - config = testdir.parseconfig() - assert config.topdir == tmpdir - col = config.getnode(dir1.dirpath()) - col1 = config.getnode(dir1) - assert col1.parent == col - io = py.io.BytesIO() - pickler = pickle.Pickler(io) - pickler.dump(col) - pickler.dump(col1) - pickler.dump(col) - io.seek(0) - unpickler = pickle.Unpickler(io) - newtopdir = tmpdir.ensure("newtopdir", dir=1) - newtopdir.mkdir("sourcedir").mkdir("somedir") - old = newtopdir.chdir() - try: - newcol = unpickler.load() - newcol2 = unpickler.load() - newcol3 = unpickler.load() - assert newcol2.config is newcol.config - assert newcol2.parent == newcol - assert newcol2.config.topdir.realpath() == newtopdir.realpath() - newsourcedir = newtopdir.join("sourcedir") - assert newcol.fspath.realpath() == newsourcedir.realpath() - assert newcol2.fspath.basename == dir1.basename - assert newcol2.fspath.relto(newcol2.config.topdir) - finally: - old.chdir() diff --git a/testing/test_conftesthandle.py b/testing/test_conftesthandle.py index afa59d572..b6a35902e 100644 --- a/testing/test_conftesthandle.py +++ b/testing/test_conftesthandle.py @@ -82,10 +82,10 @@ class TestConftestValueAccessGlobal: #conftest.lget("b") == 1 def test_value_access_with_confmod(self, basedir): - topdir = basedir.join("adir", "b") - topdir.ensure("xx", dir=True) - conftest = ConftestWithSetinitial(topdir) - mod, value = conftest.rget_with_confmod("a", topdir) + startdir = basedir.join("adir", "b") + startdir.ensure("xx", dir=True) + conftest = ConftestWithSetinitial(startdir) + mod, value = conftest.rget_with_confmod("a", startdir) assert value == 1.5 path = py.path.local(mod.__file__) assert path.dirpath() == basedir.join("adir", "b") diff --git a/testing/test_deprecated_api.py b/testing/test_deprecated_api.py index 4f4757132..fb3d738d5 100644 --- a/testing/test_deprecated_api.py +++ b/testing/test_deprecated_api.py @@ -49,7 +49,7 @@ class TestCollectDeprecated: def check2(self): pass """)) config = testdir.parseconfig(somefile) - dirnode = config.getnode(somefile.dirpath()) + dirnode = testdir.getnode(config, somefile.dirpath()) colitems = dirnode.collect() w = recwarn.pop(DeprecationWarning) assert w.filename.find("conftest.py") != -1 @@ -171,9 +171,12 @@ class TestCollectDeprecated: return Module(path, parent=self) return super(Directory, self).consider_file(path) """) + #def pytest_collect_file(path, parent): + # if path.basename == "testme.xxx": + # return Module(path, parent=parent) testme = testdir.makefile('xxx', testme="hello") config = testdir.parseconfig(testme) - col = config.getnode(testme) + col = testdir.getnode(config, testme) assert col.collect() == [] @@ -219,7 +222,7 @@ class TestDisabled: """) reprec.assertoutcome(skipped=2) - @py.test.mark.multi(name="Directory Module Class Function".split()) + @py.test.mark.multi(name="Module Class Function".split()) def test_function_deprecated_run_execute(self, name, testdir, recwarn): testdir.makeconftest(""" import py @@ -235,11 +238,11 @@ class TestDisabled: """) config = testdir.parseconfig() if name == "Directory": - config.getnode(testdir.tmpdir) + testdir.getnode(config, testdir.tmpdir) elif name in ("Module", "File"): - config.getnode(p) + testdir.getnode(config, p) else: - fnode = config.getnode(p) + fnode = testdir.getnode(config, p) recwarn.clear() fnode.collect() w = recwarn.pop(DeprecationWarning) @@ -278,9 +281,10 @@ def test_conftest_non_python_items(recwarn, testdir): checkfile = testdir.makefile(ext="xxx", hello="world") testdir.makepyfile(x="") testdir.maketxtfile(x="") - config = testdir.parseconfig() recwarn.clear() - dircol = config.getnode(checkfile.dirpath()) + config = testdir.parseconfig() + dircol = testdir.getnode(config, checkfile.dirpath()) + w = recwarn.pop(DeprecationWarning) assert str(w.message).find("conftest.py") != -1 colitems = dircol.collect() @@ -288,7 +292,7 @@ def test_conftest_non_python_items(recwarn, testdir): assert colitems[0].name == "hello.xxx" assert colitems[0].__class__.__name__ == "CustomItem" - item = config.getnode(checkfile) + item = testdir.getnode(config, checkfile) assert item.name == "hello.xxx" assert item.__class__.__name__ == "CustomItem" @@ -321,14 +325,14 @@ def test_extra_python_files_and_functions(testdir, recwarn): """) # check that directory collects "check_" files config = testdir.parseconfig() - col = config.getnode(checkfile.dirpath()) + col = testdir.getnode(config, checkfile.dirpath()) colitems = col.collect() assert len(colitems) == 1 assert isinstance(colitems[0], py.test.collect.Module) # check that module collects "check_" functions and methods config = testdir.parseconfig(checkfile) - col = config.getnode(checkfile) + col = testdir.getnode(config, checkfile) assert isinstance(col, py.test.collect.Module) colitems = col.collect() assert len(colitems) == 2 diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index ee919d95c..dddf99d0b 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -160,6 +160,19 @@ class TestBootstrapping: pp.unregister(a2) assert not pp.isregistered(a2) + def test_registry_ordering(self): + pp = PluginManager() + class A: pass + a1, a2 = A(), A() + pp.register(a1) + pp.register(a2, "hello") + l = pp.getplugins() + assert l.index(a1) < l.index(a2) + a3 = A() + pp.register(a3, prepend=True) + l = pp.getplugins() + assert l.index(a3) == 0 + def test_register_imported_modules(self): pp = PluginManager() mod = py.std.types.ModuleType("x.y.pytest_hello") @@ -340,8 +353,12 @@ def test_varnames(): class A: def f(self, y): pass + class B(object): + def __call__(self, z): + pass assert varnames(f) == ("x",) assert varnames(A().f) == ('y',) + assert varnames(B()) == ('z',) class TestMultiCall: def test_uses_copy_of_methods(self): diff --git a/testing/test_pycollect.py b/testing/test_pycollect.py deleted file mode 100644 index feb3f418f..000000000 --- a/testing/test_pycollect.py +++ /dev/null @@ -1,516 +0,0 @@ -import py - -class TestModule: - def test_module_file_not_found(self, testdir): - tmpdir = testdir.tmpdir - fn = tmpdir.join('nada','no') - col = py.test.collect.Module(fn, config=testdir.Config()) - col.config = testdir.parseconfig(tmpdir) - py.test.raises(py.error.ENOENT, col.collect) - - def test_failing_import(self, testdir): - modcol = testdir.getmodulecol("import alksdjalskdjalkjals") - py.test.raises(ImportError, modcol.collect) - py.test.raises(ImportError, modcol.collect) - py.test.raises(ImportError, modcol.run) - - def test_import_duplicate(self, testdir): - a = testdir.mkdir("a") - b = testdir.mkdir("b") - p = a.ensure("test_whatever.py") - p.pyimport() - del py.std.sys.modules['test_whatever'] - b.ensure("test_whatever.py") - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*import*mismatch*", - "*imported*test_whatever*", - "*%s*" % a.join("test_whatever.py"), - "*not the same*", - "*%s*" % b.join("test_whatever.py"), - "*HINT*", - ]) - - def test_syntax_error_in_module(self, testdir): - modcol = testdir.getmodulecol("this is a syntax error") - py.test.raises(modcol.CollectError, modcol.collect) - py.test.raises(modcol.CollectError, modcol.collect) - py.test.raises(modcol.CollectError, modcol.run) - - def test_module_considers_pluginmanager_at_import(self, testdir): - modcol = testdir.getmodulecol("pytest_plugins='xasdlkj',") - py.test.raises(ImportError, "modcol.obj") - -class TestClass: - def test_class_with_init_not_collected(self, testdir): - modcol = testdir.getmodulecol(""" - class TestClass1: - def __init__(self): - pass - class TestClass2(object): - def __init__(self): - pass - """) - l = modcol.collect() - assert len(l) == 0 - -if py.std.sys.version_info > (3, 0): - _func_name_attr = "__name__" -else: - _func_name_attr = "func_name" - -class TestGenerator: - def test_generative_functions(self, testdir): - modcol = testdir.getmodulecol(""" - def func1(arg, arg2): - assert arg == arg2 - - def test_gen(): - yield func1, 17, 3*5 - yield func1, 42, 6*7 - """) - colitems = modcol.collect() - assert len(colitems) == 1 - gencol = colitems[0] - assert isinstance(gencol, py.test.collect.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) - assert gencolitems[0].name == '[0]' - assert getattr(gencolitems[0].obj, _func_name_attr) == 'func1' - - def test_generative_methods(self, testdir): - modcol = testdir.getmodulecol(""" - def func1(arg, arg2): - assert arg == arg2 - class TestGenMethods: - def test_gen(self): - yield func1, 17, 3*5 - yield func1, 42, 6*7 - """) - gencol = modcol.collect()[0].collect()[0].collect()[0] - assert isinstance(gencol, py.test.collect.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) - assert gencolitems[0].name == '[0]' - assert getattr(gencolitems[0].obj, _func_name_attr) == 'func1' - - def test_generative_functions_with_explicit_names(self, testdir): - modcol = testdir.getmodulecol(""" - def func1(arg, arg2): - assert arg == arg2 - - def test_gen(): - yield "seventeen", func1, 17, 3*5 - yield "fortytwo", func1, 42, 6*7 - """) - colitems = modcol.collect() - assert len(colitems) == 1 - gencol = colitems[0] - assert isinstance(gencol, py.test.collect.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) - assert gencolitems[0].name == "['seventeen']" - assert getattr(gencolitems[0].obj, _func_name_attr) == 'func1' - assert gencolitems[1].name == "['fortytwo']" - assert getattr(gencolitems[1].obj, _func_name_attr) == 'func1' - - def test_generative_functions_unique_explicit_names(self, testdir): - # generative - modcol = testdir.getmodulecol(""" - def func(): pass - def test_gen(): - yield "name", func - yield "name", func - """) - colitems = modcol.collect() - assert len(colitems) == 1 - gencol = colitems[0] - assert isinstance(gencol, py.test.collect.Generator) - py.test.raises(ValueError, "gencol.collect()") - - def test_generative_methods_with_explicit_names(self, testdir): - modcol = testdir.getmodulecol(""" - def func1(arg, arg2): - assert arg == arg2 - class TestGenMethods: - def test_gen(self): - yield "m1", func1, 17, 3*5 - yield "m2", func1, 42, 6*7 - """) - gencol = modcol.collect()[0].collect()[0].collect()[0] - assert isinstance(gencol, py.test.collect.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) - assert gencolitems[0].name == "['m1']" - assert getattr(gencolitems[0].obj, _func_name_attr) == 'func1' - assert gencolitems[1].name == "['m2']" - assert getattr(gencolitems[1].obj, _func_name_attr) == 'func1' - - def test_order_of_execution_generator_same_codeline(self, testdir, tmpdir): - o = testdir.makepyfile(""" - def test_generative_order_of_execution(): - import py - test_list = [] - expected_list = list(range(6)) - - def list_append(item): - test_list.append(item) - - def assert_order_of_execution(): - py.builtin.print_('expected order', expected_list) - py.builtin.print_('but got ', test_list) - assert test_list == expected_list - - for i in expected_list: - yield list_append, i - yield assert_order_of_execution - """) - reprec = testdir.inline_run(o) - passed, skipped, failed = reprec.countoutcomes() - assert passed == 7 - assert not skipped and not failed - - def test_order_of_execution_generator_different_codeline(self, testdir): - o = testdir.makepyfile(""" - def test_generative_tests_different_codeline(): - import py - test_list = [] - expected_list = list(range(3)) - - def list_append_2(): - test_list.append(2) - - def list_append_1(): - test_list.append(1) - - def list_append_0(): - test_list.append(0) - - def assert_order_of_execution(): - py.builtin.print_('expected order', expected_list) - py.builtin.print_('but got ', test_list) - assert test_list == expected_list - - yield list_append_0 - yield list_append_1 - yield list_append_2 - yield assert_order_of_execution - """) - reprec = testdir.inline_run(o) - passed, skipped, failed = reprec.countoutcomes() - assert passed == 4 - assert not skipped and not failed - -class TestFunction: - def test_getmodulecollector(self, testdir): - item = testdir.getitem("def test_func(): pass") - modcol = item.getparent(py.test.collect.Module) - assert isinstance(modcol, py.test.collect.Module) - assert hasattr(modcol.obj, 'test_func') - - def test_function_equality(self, testdir, tmpdir): - config = testdir.reparseconfig() - f1 = py.test.collect.Function(name="name", config=config, - args=(1,), callobj=isinstance) - f2 = py.test.collect.Function(name="name",config=config, - args=(1,), callobj=py.builtin.callable) - assert not f1 == f2 - assert f1 != f2 - f3 = py.test.collect.Function(name="name", config=config, - args=(1,2), callobj=py.builtin.callable) - assert not f3 == f2 - assert f3 != f2 - - assert not f3 == f1 - assert f3 != f1 - - f1_b = py.test.collect.Function(name="name", config=config, - args=(1,), callobj=isinstance) - assert f1 == f1_b - assert not f1 != f1_b - - def test_function_equality_with_callspec(self, testdir, tmpdir): - config = testdir.reparseconfig() - class callspec1: - param = 1 - funcargs = {} - id = "hello" - class callspec2: - param = 1 - funcargs = {} - id = "world" - f5 = py.test.collect.Function(name="name", config=config, - callspec=callspec1, callobj=isinstance) - f5b = py.test.collect.Function(name="name", config=config, - callspec=callspec2, callobj=isinstance) - assert f5 != f5b - assert not (f5 == f5b) - - def test_pyfunc_call(self, testdir): - item = testdir.getitem("def test_func(): raise ValueError") - config = item.config - class MyPlugin1: - def pytest_pyfunc_call(self, pyfuncitem): - raise ValueError - class MyPlugin2: - def pytest_pyfunc_call(self, pyfuncitem): - return True - config.pluginmanager.register(MyPlugin1()) - config.pluginmanager.register(MyPlugin2()) - config.hook.pytest_pyfunc_call(pyfuncitem=item) - -class TestSorting: - def test_check_equality(self, testdir): - modcol = testdir.getmodulecol(""" - def test_pass(): pass - def test_fail(): assert 0 - """) - fn1 = modcol.collect_by_name("test_pass") - assert isinstance(fn1, py.test.collect.Function) - fn2 = modcol.collect_by_name("test_pass") - assert isinstance(fn2, py.test.collect.Function) - - assert fn1 == fn2 - assert fn1 != modcol - if py.std.sys.version_info < (3, 0): - assert cmp(fn1, fn2) == 0 - assert hash(fn1) == hash(fn2) - - fn3 = modcol.collect_by_name("test_fail") - assert isinstance(fn3, py.test.collect.Function) - assert not (fn1 == fn3) - assert fn1 != fn3 - - for fn in fn1,fn2,fn3: - assert fn != 3 - assert fn != modcol - assert fn != [1,2,3] - assert [1,2,3] != fn - assert modcol != fn - - def test_allow_sane_sorting_for_decorators(self, testdir): - modcol = testdir.getmodulecol(""" - def dec(f): - g = lambda: f(2) - g.place_as = f - return g - - - def test_b(y): - pass - test_b = dec(test_b) - - def test_a(y): - pass - test_a = dec(test_a) - """) - colitems = modcol.collect() - assert len(colitems) == 2 - assert [item.name for item in colitems] == ['test_b', 'test_a'] - - -class TestConftestCustomization: - def test_pytest_pycollect_module(self, testdir): - testdir.makeconftest(""" - import py - class MyModule(py.test.collect.Module): - pass - def pytest_pycollect_makemodule(path, parent): - if path.basename == "test_xyz.py": - return MyModule(path, parent) - """) - testdir.makepyfile("def some(): pass") - testdir.makepyfile(test_xyz="") - result = testdir.runpytest("--collectonly") - result.stdout.fnmatch_lines([ - "*3 - - def test_traceback_error_during_import(self, testdir): - testdir.makepyfile(""" - x = 1 - x = 2 - x = 17 - asd - """) - result = testdir.runpytest() - assert result.ret != 0 - out = result.stdout.str() - assert "x = 1" not in out - assert "x = 2" not in out - result.stdout.fnmatch_lines([ - ">*asd*", - "E*NameError*", - ]) - result = testdir.runpytest("--fulltrace") - out = result.stdout.str() - assert "x = 1" in out - assert "x = 2" in out - result.stdout.fnmatch_lines([ - ">*asd*", - "E*NameError*", - ]) diff --git a/testing/test_session.py b/testing/test_session.py index a09d3bc30..64d84b252 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -1,11 +1,6 @@ import py class SessionTests: - def test_initsession(self, testdir, tmpdir): - config = testdir.reparseconfig() - session = config.initsession() - assert session.config is config - def test_basic_testitem_events(self, testdir): tfile = testdir.makepyfile(""" def test_one(): @@ -22,14 +17,14 @@ class SessionTests: assert len(skipped) == 0 assert len(passed) == 1 assert len(failed) == 3 - assert failed[0].item.name == "test_one_one" - assert failed[1].item.name == "test_other" - assert failed[2].item.name == "test_two" - itemstarted = reprec.getcalls("pytest_itemstart") + assert failed[0].nodenames[-1] == "test_one_one" + assert failed[1].nodenames[-1] == "test_other" + assert failed[2].nodenames[-1] == "test_two" + itemstarted = reprec.getcalls("pytest_log_itemcollect") assert len(itemstarted) == 4 colstarted = reprec.getcalls("pytest_collectstart") - assert len(colstarted) == 1 - col = colstarted[0].collector + assert len(colstarted) == 1 + 1 # XXX ExtraTopCollector + col = colstarted[1].collector assert isinstance(col, py.test.collect.Module) def test_nested_import_error(self, testdir): @@ -183,13 +178,13 @@ class TestNewSession(SessionTests): ) reprec = testdir.inline_run('--collectonly', p.dirpath()) - itemstarted = reprec.getcalls("pytest_itemstart") + itemstarted = reprec.getcalls("pytest_log_itemcollect") assert len(itemstarted) == 3 assert not reprec.getreports("pytest_runtest_logreport") started = reprec.getcalls("pytest_collectstart") finished = reprec.getreports("pytest_collectreport") assert len(started) == len(finished) - assert len(started) == 8 + assert len(started) == 8 + 1 # XXX extra TopCollector colfail = [x for x in finished if x.failed] colskipped = [x for x in finished if x.skipped] assert len(colfail) == 1 diff --git a/tox.ini b/tox.ini index e791b84c8..770e998d9 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ sdistsrc={distshare}/py-* [testenv] changedir=testing commands= - py.test -rfsxX --junitxml={envlogdir}/junit-{envname}.xml --tools-on-path [] + py.test -rfsxX --junitxml={envlogdir}/junit-{envname}.xml [] deps= pexpect [testenv:py27] @@ -21,7 +21,7 @@ deps= {distshare}/pytest-xdist-* commands= py.test -n3 -rfsxX \ - --junitxml={envlogdir}/junit-{envname}.xml --tools-on-path [] + --junitxml={envlogdir}/junit-{envname}.xml [] [testenv:py26] basepython=python2.6 @@ -47,6 +47,10 @@ basepython=python2.4 [testenv:py31] basepython=python3.1 deps= +[testenv:py32] +basepython=python3.2 +deps= +#{distshare}/pytest-xdist-* #[testenv:pypy] #python=pypy-c [testenv:jython]