Merge branch 'master' into typo_for_parametrize
This commit is contained in:
		
						commit
						e12a588c39
					
				|  | @ -33,6 +33,7 @@ env/ | ||||||
| 3rdparty/ | 3rdparty/ | ||||||
| .tox | .tox | ||||||
| .cache | .cache | ||||||
|  | .pytest_cache | ||||||
| .coverage | .coverage | ||||||
| .ropeproject | .ropeproject | ||||||
| .idea | .idea | ||||||
|  |  | ||||||
							
								
								
									
										5
									
								
								AUTHORS
								
								
								
								
							
							
						
						
									
										5
									
								
								AUTHORS
								
								
								
								
							|  | @ -3,12 +3,15 @@ merlinux GmbH, Germany, office at merlinux eu | ||||||
| 
 | 
 | ||||||
| Contributors include:: | Contributors include:: | ||||||
| 
 | 
 | ||||||
|  | Aaron Coleman | ||||||
| Abdeali JK | Abdeali JK | ||||||
| Abhijeet Kasurde | Abhijeet Kasurde | ||||||
| Ahn Ki-Wook | Ahn Ki-Wook | ||||||
|  | Alan Velasco | ||||||
| Alexander Johnson | Alexander Johnson | ||||||
| Alexei Kozlenok | Alexei Kozlenok | ||||||
| Anatoly Bubenkoff | Anatoly Bubenkoff | ||||||
|  | Anders Hovmöller | ||||||
| Andras Tim | Andras Tim | ||||||
| Andreas Zeidler | Andreas Zeidler | ||||||
| Andrzej Ostrowski | Andrzej Ostrowski | ||||||
|  | @ -17,6 +20,7 @@ Anthon van der Neut | ||||||
| Anthony Sottile | Anthony Sottile | ||||||
| Antony Lee | Antony Lee | ||||||
| Armin Rigo | Armin Rigo | ||||||
|  | Aron Coyle | ||||||
| Aron Curzon | Aron Curzon | ||||||
| Aviv Palivoda | Aviv Palivoda | ||||||
| Barney Gale | Barney Gale | ||||||
|  | @ -150,6 +154,7 @@ Punyashloka Biswal | ||||||
| Quentin Pradet | Quentin Pradet | ||||||
| Ralf Schmitt | Ralf Schmitt | ||||||
| Ran Benita | Ran Benita | ||||||
|  | Raphael Castaneda | ||||||
| Raphael Pierzina | Raphael Pierzina | ||||||
| Raquel Alegre | Raquel Alegre | ||||||
| Ravi Chandra | Ravi Chandra | ||||||
|  |  | ||||||
							
								
								
									
										132
									
								
								CHANGELOG.rst
								
								
								
								
							
							
						
						
									
										132
									
								
								CHANGELOG.rst
								
								
								
								
							|  | @ -8,6 +8,138 @@ | ||||||
| 
 | 
 | ||||||
| .. towncrier release notes start | .. towncrier release notes start | ||||||
| 
 | 
 | ||||||
|  | Pytest 3.4.0 (2018-01-30) | ||||||
|  | ========================= | ||||||
|  | 
 | ||||||
|  | Deprecations and Removals | ||||||
|  | ------------------------- | ||||||
|  | 
 | ||||||
|  | - All pytest classes now subclass ``object`` for better Python 2/3 compatibility. | ||||||
|  |   This should not affect user code except in very rare edge cases. (`#2147 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/2147>`_) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Features | ||||||
|  | -------- | ||||||
|  | 
 | ||||||
|  | - Introduce ``empty_parameter_set_mark`` ini option to select which mark to | ||||||
|  |   apply when ``@pytest.mark.parametrize`` is given an empty set of parameters. | ||||||
|  |   Valid options are ``skip`` (default) and ``xfail``. Note that it is planned | ||||||
|  |   to change the default to ``xfail`` in future releases as this is considered | ||||||
|  |   less error prone. (`#2527 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/2527>`_) | ||||||
|  | 
 | ||||||
|  | - **Incompatible change**: after community feedback the `logging | ||||||
|  |   <https://docs.pytest.org/en/latest/logging.html>`_ functionality has | ||||||
|  |   undergone some changes. Please consult the `logging documentation | ||||||
|  |   <https://docs.pytest.org/en/latest/logging.html#incompatible-changes-in-pytest-3-4>`_ | ||||||
|  |   for details. (`#3013 <https://github.com/pytest-dev/pytest/issues/3013>`_) | ||||||
|  | 
 | ||||||
|  | - Console output falls back to "classic" mode when capturing is disabled (``-s``), | ||||||
|  |   otherwise the output gets garbled to the point of being useless. (`#3038 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/3038>`_) | ||||||
|  | 
 | ||||||
|  | - New `pytest_runtest_logfinish | ||||||
|  |   <https://docs.pytest.org/en/latest/writing_plugins.html#_pytest.hookspec.pytest_runtest_logfinish>`_ | ||||||
|  |   hook which is called when a test item has finished executing, analogous to | ||||||
|  |   `pytest_runtest_logstart | ||||||
|  |   <https://docs.pytest.org/en/latest/writing_plugins.html#_pytest.hookspec.pytest_runtest_start>`_. | ||||||
|  |   (`#3101 <https://github.com/pytest-dev/pytest/issues/3101>`_) | ||||||
|  | 
 | ||||||
|  | - Improve performance when collecting tests using many fixtures. (`#3107 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/3107>`_) | ||||||
|  | 
 | ||||||
|  | - New ``caplog.get_records(when)`` method which provides access to the captured | ||||||
|  |   records for the ``"setup"``, ``"call"`` and ``"teardown"`` | ||||||
|  |   testing stages. (`#3117 <https://github.com/pytest-dev/pytest/issues/3117>`_) | ||||||
|  | 
 | ||||||
|  | - New fixture ``record_xml_attribute`` that allows modifying and inserting | ||||||
|  |   attributes on the ``<testcase>`` xml node in JUnit reports. (`#3130 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/3130>`_) | ||||||
|  | 
 | ||||||
|  | - The default cache directory has been renamed from ``.cache`` to | ||||||
|  |   ``.pytest_cache`` after community feedback that the name ``.cache`` did not | ||||||
|  |   make it clear that it was used by pytest. (`#3138 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/3138>`_) | ||||||
|  | 
 | ||||||
|  | - Colorize the levelname column in the live-log output. (`#3142 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/3142>`_) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Bug Fixes | ||||||
|  | --------- | ||||||
|  | 
 | ||||||
|  | - Fix hanging pexpect test on MacOS by using flush() instead of wait(). | ||||||
|  |   (`#2022 <https://github.com/pytest-dev/pytest/issues/2022>`_) | ||||||
|  | 
 | ||||||
|  | - Fix restoring Python state after in-process pytest runs with the | ||||||
|  |   ``pytester`` plugin; this may break tests using multiple inprocess | ||||||
|  |   pytest runs if later ones depend on earlier ones leaking global interpreter | ||||||
|  |   changes. (`#3016 <https://github.com/pytest-dev/pytest/issues/3016>`_) | ||||||
|  | 
 | ||||||
|  | - Fix skipping plugin reporting hook when test aborted before plugin setup | ||||||
|  |   hook. (`#3074 <https://github.com/pytest-dev/pytest/issues/3074>`_) | ||||||
|  | 
 | ||||||
|  | - Fix progress percentage reported when tests fail during teardown. (`#3088 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/3088>`_) | ||||||
|  | 
 | ||||||
|  | - **Incompatible change**: ``-o/--override`` option no longer eats all the | ||||||
|  |   remaining options, which can lead to surprising behavior: for example, | ||||||
|  |   ``pytest -o foo=1 /path/to/test.py`` would fail because ``/path/to/test.py`` | ||||||
|  |   would be considered as part of the ``-o`` command-line argument. One | ||||||
|  |   consequence of this is that now multiple configuration overrides need | ||||||
|  |   multiple ``-o`` flags: ``pytest -o foo=1 -o bar=2``. (`#3103 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/3103>`_) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Improved Documentation | ||||||
|  | ---------------------- | ||||||
|  | 
 | ||||||
|  | - Document hooks (defined with ``historic=True``) which cannot be used with | ||||||
|  |   ``hookwrapper=True``. (`#2423 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/2423>`_) | ||||||
|  | 
 | ||||||
|  | - Clarify that warning capturing doesn't change the warning filter by default. | ||||||
|  |   (`#2457 <https://github.com/pytest-dev/pytest/issues/2457>`_) | ||||||
|  | 
 | ||||||
|  | - Clarify a possible confusion when using pytest_fixture_setup with fixture | ||||||
|  |   functions that return None. (`#2698 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/2698>`_) | ||||||
|  | 
 | ||||||
|  | - Fix the wording of a sentence on doctest flags used in pytest. (`#3076 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/3076>`_) | ||||||
|  | 
 | ||||||
|  | - Prefer ``https://*.readthedocs.io`` over ``http://*.rtfd.org`` for links in | ||||||
|  |   the documentation. (`#3092 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/3092>`_) | ||||||
|  | 
 | ||||||
|  | - Improve readability (wording, grammar) of Getting Started guide (`#3131 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/3131>`_) | ||||||
|  | 
 | ||||||
|  | - Added note that calling pytest.main multiple times from the same process is | ||||||
|  |   not recommended because of import caching. (`#3143 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/3143>`_) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Trivial/Internal Changes | ||||||
|  | ------------------------ | ||||||
|  | 
 | ||||||
|  | - Show a simple and easy error when keyword expressions trigger a syntax error | ||||||
|  |   (for example, ``"-k foo and import"`` will show an error that you can not use | ||||||
|  |   the ``import`` keyword in expressions). (`#2953 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/2953>`_) | ||||||
|  | 
 | ||||||
|  | - Change parametrized automatic test id generation to use the ``__name__`` | ||||||
|  |   attribute of functions instead of the fallback argument name plus counter. | ||||||
|  |   (`#2976 <https://github.com/pytest-dev/pytest/issues/2976>`_) | ||||||
|  | 
 | ||||||
|  | - Replace py.std with stdlib imports. (`#3067 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/3067>`_) | ||||||
|  | 
 | ||||||
|  | - Corrected 'you' to 'your' in logging docs. (`#3129 | ||||||
|  |   <https://github.com/pytest-dev/pytest/issues/3129>`_) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| Pytest 3.3.2 (2017-12-25) | Pytest 3.3.2 (2017-12-25) | ||||||
| ========================= | ========================= | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ taking a lot of time to make a new one. | ||||||
| 
 | 
 | ||||||
| #. Install development dependencies in a virtual environment with:: | #. Install development dependencies in a virtual environment with:: | ||||||
| 
 | 
 | ||||||
|     pip3 install -r tasks/requirements.txt |     pip3 install -U -r tasks/requirements.txt | ||||||
| 
 | 
 | ||||||
| #. Create a branch ``release-X.Y.Z`` with the version for the release. | #. Create a branch ``release-X.Y.Z`` with the version for the release. | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -60,7 +60,7 @@ import os | ||||||
| from glob import glob | from glob import glob | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class FastFilesCompleter: | class FastFilesCompleter(object): | ||||||
|     'Fast file completer class' |     'Fast file completer class' | ||||||
| 
 | 
 | ||||||
|     def __init__(self, directories=True): |     def __init__(self, directories=True): | ||||||
|  |  | ||||||
|  | @ -56,7 +56,7 @@ class DummyRewriteHook(object): | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class AssertionState: | class AssertionState(object): | ||||||
|     """State for the assertion plugin.""" |     """State for the assertion plugin.""" | ||||||
| 
 | 
 | ||||||
|     def __init__(self, config, mode): |     def __init__(self, config, mode): | ||||||
|  |  | ||||||
|  | @ -17,7 +17,7 @@ class Cache(object): | ||||||
|         self.config = config |         self.config = config | ||||||
|         self._cachedir = Cache.cache_dir_from_config(config) |         self._cachedir = Cache.cache_dir_from_config(config) | ||||||
|         self.trace = config.trace.root.get("cache") |         self.trace = config.trace.root.get("cache") | ||||||
|         if config.getvalue("cacheclear"): |         if config.getoption("cacheclear"): | ||||||
|             self.trace("clearing cachedir") |             self.trace("clearing cachedir") | ||||||
|             if self._cachedir.check(): |             if self._cachedir.check(): | ||||||
|                 self._cachedir.remove() |                 self._cachedir.remove() | ||||||
|  | @ -98,13 +98,13 @@ class Cache(object): | ||||||
|                 json.dump(value, f, indent=2, sort_keys=True) |                 json.dump(value, f, indent=2, sort_keys=True) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class LFPlugin: | class LFPlugin(object): | ||||||
|     """ Plugin which implements the --lf (run last-failing) option """ |     """ Plugin which implements the --lf (run last-failing) option """ | ||||||
| 
 | 
 | ||||||
|     def __init__(self, config): |     def __init__(self, config): | ||||||
|         self.config = config |         self.config = config | ||||||
|         active_keys = 'lf', 'failedfirst' |         active_keys = 'lf', 'failedfirst' | ||||||
|         self.active = any(config.getvalue(key) for key in active_keys) |         self.active = any(config.getoption(key) for key in active_keys) | ||||||
|         self.lastfailed = config.cache.get("cache/lastfailed", {}) |         self.lastfailed = config.cache.get("cache/lastfailed", {}) | ||||||
|         self._previously_failed_count = None |         self._previously_failed_count = None | ||||||
| 
 | 
 | ||||||
|  | @ -114,7 +114,8 @@ class LFPlugin: | ||||||
|                 mode = "run all (no recorded failures)" |                 mode = "run all (no recorded failures)" | ||||||
|             else: |             else: | ||||||
|                 noun = 'failure' if self._previously_failed_count == 1 else 'failures' |                 noun = 'failure' if self._previously_failed_count == 1 else 'failures' | ||||||
|                 suffix = " first" if self.config.getvalue("failedfirst") else "" |                 suffix = " first" if self.config.getoption( | ||||||
|  |                     "failedfirst") else "" | ||||||
|                 mode = "rerun previous {count} {noun}{suffix}".format( |                 mode = "rerun previous {count} {noun}{suffix}".format( | ||||||
|                     count=self._previously_failed_count, suffix=suffix, noun=noun |                     count=self._previously_failed_count, suffix=suffix, noun=noun | ||||||
|                 ) |                 ) | ||||||
|  | @ -151,7 +152,7 @@ class LFPlugin: | ||||||
|                 # running a subset of all tests with recorded failures outside |                 # running a subset of all tests with recorded failures outside | ||||||
|                 # of the set of tests currently executing |                 # of the set of tests currently executing | ||||||
|                 return |                 return | ||||||
|             if self.config.getvalue("lf"): |             if self.config.getoption("lf"): | ||||||
|                 items[:] = previously_failed |                 items[:] = previously_failed | ||||||
|                 config.hook.pytest_deselected(items=previously_passed) |                 config.hook.pytest_deselected(items=previously_passed) | ||||||
|             else: |             else: | ||||||
|  | @ -159,7 +160,7 @@ class LFPlugin: | ||||||
| 
 | 
 | ||||||
|     def pytest_sessionfinish(self, session): |     def pytest_sessionfinish(self, session): | ||||||
|         config = self.config |         config = self.config | ||||||
|         if config.getvalue("cacheshow") or hasattr(config, "slaveinput"): |         if config.getoption("cacheshow") or hasattr(config, "slaveinput"): | ||||||
|             return |             return | ||||||
| 
 | 
 | ||||||
|         saved_lastfailed = config.cache.get("cache/lastfailed", {}) |         saved_lastfailed = config.cache.get("cache/lastfailed", {}) | ||||||
|  | @ -185,7 +186,7 @@ def pytest_addoption(parser): | ||||||
|         '--cache-clear', action='store_true', dest="cacheclear", |         '--cache-clear', action='store_true', dest="cacheclear", | ||||||
|         help="remove all cache contents at start of test run.") |         help="remove all cache contents at start of test run.") | ||||||
|     parser.addini( |     parser.addini( | ||||||
|         "cache_dir", default='.cache', |         "cache_dir", default='.pytest_cache', | ||||||
|         help="cache directory path.") |         help="cache directory path.") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -61,7 +61,7 @@ def pytest_load_initial_conftests(early_config, parser, args): | ||||||
|         sys.stderr.write(err) |         sys.stderr.write(err) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class CaptureManager: | class CaptureManager(object): | ||||||
|     """ |     """ | ||||||
|     Capture plugin, manages that the appropriate capture method is enabled/disabled during collection and each |     Capture plugin, manages that the appropriate capture method is enabled/disabled during collection and each | ||||||
|     test phase (setup, call, teardown). After each of those points, the captured output is obtained and |     test phase (setup, call, teardown). After each of those points, the captured output is obtained and | ||||||
|  | @ -271,7 +271,7 @@ def _install_capture_fixture_on_item(request, capture_class): | ||||||
|     del request.node._capture_fixture |     del request.node._capture_fixture | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class CaptureFixture: | class CaptureFixture(object): | ||||||
|     def __init__(self, captureclass, request): |     def __init__(self, captureclass, request): | ||||||
|         self.captureclass = captureclass |         self.captureclass = captureclass | ||||||
|         self.request = request |         self.request = request | ||||||
|  | @ -416,11 +416,11 @@ class MultiCapture(object): | ||||||
|                              self.err.snap() if self.err is not None else "") |                              self.err.snap() if self.err is not None else "") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class NoCapture: | class NoCapture(object): | ||||||
|     __init__ = start = done = suspend = resume = lambda *args: None |     __init__ = start = done = suspend = resume = lambda *args: None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class FDCaptureBinary: | class FDCaptureBinary(object): | ||||||
|     """Capture IO to/from a given os-level filedescriptor. |     """Capture IO to/from a given os-level filedescriptor. | ||||||
| 
 | 
 | ||||||
|     snap() produces `bytes` |     snap() produces `bytes` | ||||||
|  | @ -506,7 +506,7 @@ class FDCapture(FDCaptureBinary): | ||||||
|         return res |         return res | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class SysCapture: | class SysCapture(object): | ||||||
|     def __init__(self, fd, tmpfile=None): |     def __init__(self, fd, tmpfile=None): | ||||||
|         name = patchsysdict[fd] |         name = patchsysdict[fd] | ||||||
|         self._old = getattr(sys, name) |         self._old = getattr(sys, name) | ||||||
|  | @ -551,7 +551,7 @@ class SysCaptureBinary(SysCapture): | ||||||
|         return res |         return res | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class DontReadFromInput: | class DontReadFromInput(object): | ||||||
|     """Temporary stub class.  Ideally when stdin is accessed, the |     """Temporary stub class.  Ideally when stdin is accessed, the | ||||||
|     capturing should be turned off, with possibly all data captured |     capturing should be turned off, with possibly all data captured | ||||||
|     so far sent to the screen.  This should be configurable, though, |     so far sent to the screen.  This should be configurable, though, | ||||||
|  |  | ||||||
|  | @ -60,12 +60,13 @@ def main(args=None, plugins=None): | ||||||
|             finally: |             finally: | ||||||
|                 config._ensure_unconfigure() |                 config._ensure_unconfigure() | ||||||
|     except UsageError as e: |     except UsageError as e: | ||||||
|  |         tw = py.io.TerminalWriter(sys.stderr) | ||||||
|         for msg in e.args: |         for msg in e.args: | ||||||
|             sys.stderr.write("ERROR: %s\n" % (msg,)) |             tw.line("ERROR: {}\n".format(msg), red=True) | ||||||
|         return 4 |         return 4 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class cmdline:  # compatibility namespace | class cmdline(object):  # compatibility namespace | ||||||
|     main = staticmethod(main) |     main = staticmethod(main) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -462,7 +463,7 @@ def _get_plugin_specs_as_list(specs): | ||||||
|     return [] |     return [] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Parser: | class Parser(object): | ||||||
|     """ Parser for command line arguments and ini-file values. |     """ Parser for command line arguments and ini-file values. | ||||||
| 
 | 
 | ||||||
|     :ivar extra_info: dict of generic param -> value to display in case |     :ivar extra_info: dict of generic param -> value to display in case | ||||||
|  | @ -597,7 +598,7 @@ class ArgumentError(Exception): | ||||||
|             return self.msg |             return self.msg | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Argument: | class Argument(object): | ||||||
|     """class that mimics the necessary behaviour of optparse.Option |     """class that mimics the necessary behaviour of optparse.Option | ||||||
| 
 | 
 | ||||||
|     its currently a least effort implementation |     its currently a least effort implementation | ||||||
|  | @ -727,7 +728,7 @@ class Argument: | ||||||
|         return 'Argument({0})'.format(', '.join(args)) |         return 'Argument({0})'.format(', '.join(args)) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class OptionGroup: | class OptionGroup(object): | ||||||
|     def __init__(self, name, description="", parser=None): |     def __init__(self, name, description="", parser=None): | ||||||
|         self.name = name |         self.name = name | ||||||
|         self.description = description |         self.description = description | ||||||
|  | @ -858,7 +859,7 @@ class CmdOptions(object): | ||||||
|         return CmdOptions(self.__dict__) |         return CmdOptions(self.__dict__) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Notset: | class Notset(object): | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         return "<NOTSET>" |         return "<NOTSET>" | ||||||
| 
 | 
 | ||||||
|  | @ -1187,16 +1188,15 @@ class Config(object): | ||||||
| 
 | 
 | ||||||
|     def _get_override_ini_value(self, name): |     def _get_override_ini_value(self, name): | ||||||
|         value = None |         value = None | ||||||
|         # override_ini is a list of list, to support both -o foo1=bar1 foo2=bar2 and |         # override_ini is a list of "ini=value" options | ||||||
|         # and -o foo1=bar1 -o foo2=bar2 options |         # always use the last item if multiple values are set for same ini-name, | ||||||
|         # always use the last item if multiple value set for same ini-name, |  | ||||||
|         # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2 |         # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2 | ||||||
|         for ini_config_list in self._override_ini: |         for ini_config in self._override_ini: | ||||||
|             for ini_config in ini_config_list: |             try: | ||||||
|                 try: |                 key, user_ini_value = ini_config.split("=", 1) | ||||||
|                     (key, user_ini_value) = ini_config.split("=", 1) |             except ValueError: | ||||||
|                 except ValueError: |                 raise UsageError("-o/--override-ini expects option=value style.") | ||||||
|                     raise UsageError("-o/--override-ini expects option=value style.") |             else: | ||||||
|                 if key == name: |                 if key == name: | ||||||
|                     value = user_ini_value |                     value = user_ini_value | ||||||
|         return value |         return value | ||||||
|  |  | ||||||
|  | @ -40,7 +40,7 @@ def pytest_configure(config): | ||||||
|     config._cleanup.append(fin) |     config._cleanup.append(fin) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class pytestPDB: | class pytestPDB(object): | ||||||
|     """ Pseudo PDB that defers to the real pdb. """ |     """ Pseudo PDB that defers to the real pdb. """ | ||||||
|     _pluginmanager = None |     _pluginmanager = None | ||||||
|     _config = None |     _config = None | ||||||
|  | @ -62,7 +62,7 @@ class pytestPDB: | ||||||
|         cls._pdb_cls().set_trace(frame) |         cls._pdb_cls().set_trace(frame) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class PdbInvoke: | class PdbInvoke(object): | ||||||
|     def pytest_exception_interact(self, node, call, report): |     def pytest_exception_interact(self, node, call, report): | ||||||
|         capman = node.config.pluginmanager.getplugin("capturemanager") |         capman = node.config.pluginmanager.getplugin("capturemanager") | ||||||
|         if capman: |         if capman: | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ import functools | ||||||
| import inspect | import inspect | ||||||
| import sys | import sys | ||||||
| import warnings | import warnings | ||||||
| from collections import OrderedDict | from collections import OrderedDict, deque, defaultdict | ||||||
| 
 | 
 | ||||||
| import attr | import attr | ||||||
| import py | import py | ||||||
|  | @ -26,11 +26,12 @@ from _pytest.outcomes import fail, TEST_OUTCOME | ||||||
| 
 | 
 | ||||||
| def pytest_sessionstart(session): | def pytest_sessionstart(session): | ||||||
|     import _pytest.python |     import _pytest.python | ||||||
|  |     import _pytest.nodes | ||||||
| 
 | 
 | ||||||
|     scopename2class.update({ |     scopename2class.update({ | ||||||
|         'class': _pytest.python.Class, |         'class': _pytest.python.Class, | ||||||
|         'module': _pytest.python.Module, |         'module': _pytest.python.Module, | ||||||
|         'function': _pytest.main.Item, |         'function': _pytest.nodes.Item, | ||||||
|         'session': _pytest.main.Session, |         'session': _pytest.main.Session, | ||||||
|     }) |     }) | ||||||
|     session._fixturemanager = FixtureManager(session) |     session._fixturemanager = FixtureManager(session) | ||||||
|  | @ -162,62 +163,51 @@ def get_parametrized_fixture_keys(item, scopenum): | ||||||
| 
 | 
 | ||||||
| def reorder_items(items): | def reorder_items(items): | ||||||
|     argkeys_cache = {} |     argkeys_cache = {} | ||||||
|  |     items_by_argkey = {} | ||||||
|     for scopenum in range(0, scopenum_function): |     for scopenum in range(0, scopenum_function): | ||||||
|         argkeys_cache[scopenum] = d = {} |         argkeys_cache[scopenum] = d = {} | ||||||
|  |         items_by_argkey[scopenum] = item_d = defaultdict(list) | ||||||
|         for item in items: |         for item in items: | ||||||
|             keys = OrderedDict.fromkeys(get_parametrized_fixture_keys(item, scopenum)) |             keys = OrderedDict.fromkeys(get_parametrized_fixture_keys(item, scopenum)) | ||||||
|             if keys: |             if keys: | ||||||
|                 d[item] = keys |                 d[item] = keys | ||||||
|     return reorder_items_atscope(items, set(), argkeys_cache, 0) |                 for key in keys: | ||||||
|  |                     item_d[key].append(item) | ||||||
|  |     items = OrderedDict.fromkeys(items) | ||||||
|  |     return list(reorder_items_atscope(items, set(), argkeys_cache, items_by_argkey, 0)) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def reorder_items_atscope(items, ignore, argkeys_cache, scopenum): | def reorder_items_atscope(items, ignore, argkeys_cache, items_by_argkey, scopenum): | ||||||
|     if scopenum >= scopenum_function or len(items) < 3: |     if scopenum >= scopenum_function or len(items) < 3: | ||||||
|         return items |         return items | ||||||
|     items_done = [] |     items_deque = deque(items) | ||||||
|     while 1: |     items_done = OrderedDict() | ||||||
|         items_before, items_same, items_other, newignore = \ |     scoped_items_by_argkey = items_by_argkey[scopenum] | ||||||
|             slice_items(items, ignore, argkeys_cache[scopenum]) |     scoped_argkeys_cache = argkeys_cache[scopenum] | ||||||
|         items_before = reorder_items_atscope( |     while items_deque: | ||||||
|             items_before, ignore, argkeys_cache, scopenum + 1) |         no_argkey_group = OrderedDict() | ||||||
|         if items_same is None: |         slicing_argkey = None | ||||||
|             # nothing to reorder in this scope |         while items_deque: | ||||||
|             assert items_other is None |             item = items_deque.popleft() | ||||||
|             return items_done + items_before |             if item in items_done or item in no_argkey_group: | ||||||
|         items_done.extend(items_before) |                 continue | ||||||
|         items = items_same + items_other |             argkeys = OrderedDict.fromkeys(k for k in scoped_argkeys_cache.get(item, []) if k not in ignore) | ||||||
|         ignore = newignore |             if not argkeys: | ||||||
| 
 |                 no_argkey_group[item] = None | ||||||
| 
 |             else: | ||||||
| def slice_items(items, ignore, scoped_argkeys_cache): |                 slicing_argkey, _ = argkeys.popitem() | ||||||
|     # we pick the first item which uses a fixture instance in the |                 # we don't have to remove relevant items from later in the deque because they'll just be ignored | ||||||
|     # requested scope and which we haven't seen yet.  We slice the input |                 for i in reversed(scoped_items_by_argkey[slicing_argkey]): | ||||||
|     # items list into a list of items_nomatch, items_same and |                     if i in items: | ||||||
|     # items_other |                         items_deque.appendleft(i) | ||||||
|     if scoped_argkeys_cache:  # do we need to do work at all? |                 break | ||||||
|         it = iter(items) |         if no_argkey_group: | ||||||
|         # first find a slicing key |             no_argkey_group = reorder_items_atscope( | ||||||
|         for i, item in enumerate(it): |                                 no_argkey_group, set(), argkeys_cache, items_by_argkey, scopenum + 1) | ||||||
|             argkeys = scoped_argkeys_cache.get(item) |             for item in no_argkey_group: | ||||||
|             if argkeys is not None: |                 items_done[item] = None | ||||||
|                 newargkeys = OrderedDict.fromkeys(k for k in argkeys if k not in ignore) |         ignore.add(slicing_argkey) | ||||||
|                 if newargkeys:  # found a slicing key |     return items_done | ||||||
|                     slicing_argkey, _ = newargkeys.popitem() |  | ||||||
|                     items_before = items[:i] |  | ||||||
|                     items_same = [item] |  | ||||||
|                     items_other = [] |  | ||||||
|                     # now slice the remainder of the list |  | ||||||
|                     for item in it: |  | ||||||
|                         argkeys = scoped_argkeys_cache.get(item) |  | ||||||
|                         if argkeys and slicing_argkey in argkeys and \ |  | ||||||
|                                 slicing_argkey not in ignore: |  | ||||||
|                             items_same.append(item) |  | ||||||
|                         else: |  | ||||||
|                             items_other.append(item) |  | ||||||
|                     newignore = ignore.copy() |  | ||||||
|                     newignore.add(slicing_argkey) |  | ||||||
|                     return (items_before, items_same, items_other, newignore) |  | ||||||
|     return items, None, None, None |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def fillfixtures(function): | def fillfixtures(function): | ||||||
|  | @ -246,7 +236,7 @@ def get_direct_param_fixture_func(request): | ||||||
|     return request.param |     return request.param | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class FuncFixtureInfo: | class FuncFixtureInfo(object): | ||||||
|     def __init__(self, argnames, names_closure, name2fixturedefs): |     def __init__(self, argnames, names_closure, name2fixturedefs): | ||||||
|         self.argnames = argnames |         self.argnames = argnames | ||||||
|         self.names_closure = names_closure |         self.names_closure = names_closure | ||||||
|  | @ -442,7 +432,7 @@ class FixtureRequest(FuncargnamesCompatAttr): | ||||||
|                 fixturedef = self._getnextfixturedef(argname) |                 fixturedef = self._getnextfixturedef(argname) | ||||||
|             except FixtureLookupError: |             except FixtureLookupError: | ||||||
|                 if argname == "request": |                 if argname == "request": | ||||||
|                     class PseudoFixtureDef: |                     class PseudoFixtureDef(object): | ||||||
|                         cached_result = (self, [0], None) |                         cached_result = (self, [0], None) | ||||||
|                         scope = "function" |                         scope = "function" | ||||||
|                     return PseudoFixtureDef |                     return PseudoFixtureDef | ||||||
|  | @ -718,7 +708,7 @@ def call_fixture_func(fixturefunc, request, kwargs): | ||||||
|     return res |     return res | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class FixtureDef: | class FixtureDef(object): | ||||||
|     """ A container for a factory definition. """ |     """ A container for a factory definition. """ | ||||||
| 
 | 
 | ||||||
|     def __init__(self, fixturemanager, baseid, argname, func, scope, params, |     def __init__(self, fixturemanager, baseid, argname, func, scope, params, | ||||||
|  | @ -924,7 +914,7 @@ def pytestconfig(request): | ||||||
|     return request.config |     return request.config | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class FixtureManager: | class FixtureManager(object): | ||||||
|     """ |     """ | ||||||
|     pytest fixtures definitions and information is stored and managed |     pytest fixtures definitions and information is stored and managed | ||||||
|     from this class. |     from this class. | ||||||
|  |  | ||||||
|  | @ -57,9 +57,9 @@ def pytest_addoption(parser): | ||||||
|                     action="store_true", dest="debug", default=False, |                     action="store_true", dest="debug", default=False, | ||||||
|                     help="store internal tracing debug information in 'pytestdebug.log'.") |                     help="store internal tracing debug information in 'pytestdebug.log'.") | ||||||
|     group._addoption( |     group._addoption( | ||||||
|         '-o', '--override-ini', nargs='*', dest="override_ini", |         '-o', '--override-ini', dest="override_ini", | ||||||
|         action="append", |         action="append", | ||||||
|         help="override config option with option=value style, e.g. `-o xfail_strict=True`.") |         help='override ini option with "option=value" style, e.g. `-o xfail_strict=True -o cache_dir=cache`.') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.hookimpl(hookwrapper=True) | @pytest.hookimpl(hookwrapper=True) | ||||||
|  |  | ||||||
|  | @ -179,7 +179,7 @@ def pytest_collection_modifyitems(session, config, items): | ||||||
| 
 | 
 | ||||||
|     :param _pytest.main.Session session: the pytest session object |     :param _pytest.main.Session session: the pytest session object | ||||||
|     :param _pytest.config.Config config: pytest config object |     :param _pytest.config.Config config: pytest config object | ||||||
|     :param List[_pytest.main.Item] items: list of item objects |     :param List[_pytest.nodes.Item] items: list of item objects | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -330,7 +330,25 @@ def pytest_runtest_protocol(item, nextitem): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def pytest_runtest_logstart(nodeid, location): | def pytest_runtest_logstart(nodeid, location): | ||||||
|     """ signal the start of running a single test item. """ |     """ signal the start of running a single test item. | ||||||
|  | 
 | ||||||
|  |     This hook will be called **before** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and | ||||||
|  |     :func:`pytest_runtest_teardown` hooks. | ||||||
|  | 
 | ||||||
|  |     :param str nodeid: full id of the item | ||||||
|  |     :param location: a triple of ``(filename, linenum, testname)`` | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def pytest_runtest_logfinish(nodeid, location): | ||||||
|  |     """ signal the complete finish of running a single test item. | ||||||
|  | 
 | ||||||
|  |     This hook will be called **after** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and | ||||||
|  |     :func:`pytest_runtest_teardown` hooks. | ||||||
|  | 
 | ||||||
|  |     :param str nodeid: full id of the item | ||||||
|  |     :param location: a triple of ``(filename, linenum, testname)`` | ||||||
|  |     """ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def pytest_runtest_setup(item): | def pytest_runtest_setup(item): | ||||||
|  | @ -479,7 +497,7 @@ def pytest_terminal_summary(terminalreporter, exitstatus): | ||||||
| def pytest_logwarning(message, code, nodeid, fslocation): | def pytest_logwarning(message, code, nodeid, fslocation): | ||||||
|     """ process a warning specified by a message, a code string, |     """ process a warning specified by a message, a code string, | ||||||
|     a nodeid and fslocation (both of which may be None |     a nodeid and fslocation (both of which may be None | ||||||
|     if the warning is not tied to a partilar node/location). |     if the warning is not tied to a particular node/location). | ||||||
| 
 | 
 | ||||||
|     .. note:: |     .. note:: | ||||||
|         This hook is incompatible with ``hookwrapper=True``. |         This hook is incompatible with ``hookwrapper=True``. | ||||||
|  |  | ||||||
|  | @ -85,6 +85,9 @@ class _NodeReporter(object): | ||||||
|     def add_property(self, name, value): |     def add_property(self, name, value): | ||||||
|         self.properties.append((str(name), bin_xml_escape(value))) |         self.properties.append((str(name), bin_xml_escape(value))) | ||||||
| 
 | 
 | ||||||
|  |     def add_attribute(self, name, value): | ||||||
|  |         self.attrs[str(name)] = bin_xml_escape(value) | ||||||
|  | 
 | ||||||
|     def make_properties_node(self): |     def make_properties_node(self): | ||||||
|         """Return a Junit node containing custom properties, if any. |         """Return a Junit node containing custom properties, if any. | ||||||
|         """ |         """ | ||||||
|  | @ -98,6 +101,7 @@ class _NodeReporter(object): | ||||||
|     def record_testreport(self, testreport): |     def record_testreport(self, testreport): | ||||||
|         assert not self.testcase |         assert not self.testcase | ||||||
|         names = mangle_test_address(testreport.nodeid) |         names = mangle_test_address(testreport.nodeid) | ||||||
|  |         existing_attrs = self.attrs | ||||||
|         classnames = names[:-1] |         classnames = names[:-1] | ||||||
|         if self.xml.prefix: |         if self.xml.prefix: | ||||||
|             classnames.insert(0, self.xml.prefix) |             classnames.insert(0, self.xml.prefix) | ||||||
|  | @ -111,6 +115,7 @@ class _NodeReporter(object): | ||||||
|         if hasattr(testreport, "url"): |         if hasattr(testreport, "url"): | ||||||
|             attrs["url"] = testreport.url |             attrs["url"] = testreport.url | ||||||
|         self.attrs = attrs |         self.attrs = attrs | ||||||
|  |         self.attrs.update(existing_attrs)  # restore any user-defined attributes | ||||||
| 
 | 
 | ||||||
|     def to_xml(self): |     def to_xml(self): | ||||||
|         testcase = Junit.testcase(time=self.duration, **self.attrs) |         testcase = Junit.testcase(time=self.duration, **self.attrs) | ||||||
|  | @ -211,6 +216,27 @@ def record_xml_property(request): | ||||||
|         return add_property_noop |         return add_property_noop | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @pytest.fixture | ||||||
|  | def record_xml_attribute(request): | ||||||
|  |     """Add extra xml attributes to the tag for the calling test. | ||||||
|  |     The fixture is callable with ``(name, value)``, with value being automatically | ||||||
|  |     xml-encoded | ||||||
|  |     """ | ||||||
|  |     request.node.warn( | ||||||
|  |         code='C3', | ||||||
|  |         message='record_xml_attribute is an experimental feature', | ||||||
|  |     ) | ||||||
|  |     xml = getattr(request.config, "_xml", None) | ||||||
|  |     if xml is not None: | ||||||
|  |         node_reporter = xml.node_reporter(request.node.nodeid) | ||||||
|  |         return node_reporter.add_attribute | ||||||
|  |     else: | ||||||
|  |         def add_attr_noop(name, value): | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  |         return add_attr_noop | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def pytest_addoption(parser): | def pytest_addoption(parser): | ||||||
|     group = parser.getgroup("terminal reporting") |     group = parser.getgroup("terminal reporting") | ||||||
|     group.addoption( |     group.addoption( | ||||||
|  |  | ||||||
|  | @ -2,9 +2,10 @@ from __future__ import absolute_import, division, print_function | ||||||
| 
 | 
 | ||||||
| import logging | import logging | ||||||
| from contextlib import closing, contextmanager | from contextlib import closing, contextmanager | ||||||
| import sys | import re | ||||||
| import six | import six | ||||||
| 
 | 
 | ||||||
|  | from _pytest.config import create_terminal_writer | ||||||
| import pytest | import pytest | ||||||
| import py | import py | ||||||
| 
 | 
 | ||||||
|  | @ -13,6 +14,58 @@ DEFAULT_LOG_FORMAT = '%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s' | ||||||
| DEFAULT_LOG_DATE_FORMAT = '%H:%M:%S' | DEFAULT_LOG_DATE_FORMAT = '%H:%M:%S' | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class ColoredLevelFormatter(logging.Formatter): | ||||||
|  |     """ | ||||||
|  |     Colorize the %(levelname)..s part of the log format passed to __init__. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     LOGLEVEL_COLOROPTS = { | ||||||
|  |         logging.CRITICAL: {'red'}, | ||||||
|  |         logging.ERROR: {'red', 'bold'}, | ||||||
|  |         logging.WARNING: {'yellow'}, | ||||||
|  |         logging.WARN: {'yellow'}, | ||||||
|  |         logging.INFO: {'green'}, | ||||||
|  |         logging.DEBUG: {'purple'}, | ||||||
|  |         logging.NOTSET: set(), | ||||||
|  |     } | ||||||
|  |     LEVELNAME_FMT_REGEX = re.compile(r'%\(levelname\)([+-]?\d*s)') | ||||||
|  | 
 | ||||||
|  |     def __init__(self, terminalwriter, *args, **kwargs): | ||||||
|  |         super(ColoredLevelFormatter, self).__init__( | ||||||
|  |             *args, **kwargs) | ||||||
|  |         if six.PY2: | ||||||
|  |             self._original_fmt = self._fmt | ||||||
|  |         else: | ||||||
|  |             self._original_fmt = self._style._fmt | ||||||
|  |         self._level_to_fmt_mapping = {} | ||||||
|  | 
 | ||||||
|  |         levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt) | ||||||
|  |         if not levelname_fmt_match: | ||||||
|  |             return | ||||||
|  |         levelname_fmt = levelname_fmt_match.group() | ||||||
|  | 
 | ||||||
|  |         for level, color_opts in self.LOGLEVEL_COLOROPTS.items(): | ||||||
|  |             formatted_levelname = levelname_fmt % { | ||||||
|  |                 'levelname': logging.getLevelName(level)} | ||||||
|  | 
 | ||||||
|  |             # add ANSI escape sequences around the formatted levelname | ||||||
|  |             color_kwargs = {name: True for name in color_opts} | ||||||
|  |             colorized_formatted_levelname = terminalwriter.markup( | ||||||
|  |                 formatted_levelname, **color_kwargs) | ||||||
|  |             self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub( | ||||||
|  |                 colorized_formatted_levelname, | ||||||
|  |                 self._fmt) | ||||||
|  | 
 | ||||||
|  |     def format(self, record): | ||||||
|  |         fmt = self._level_to_fmt_mapping.get( | ||||||
|  |             record.levelno, self._original_fmt) | ||||||
|  |         if six.PY2: | ||||||
|  |             self._fmt = fmt | ||||||
|  |         else: | ||||||
|  |             self._style._fmt = fmt | ||||||
|  |         return super(ColoredLevelFormatter, self).format(record) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def get_option_ini(config, *names): | def get_option_ini(config, *names): | ||||||
|     for name in names: |     for name in names: | ||||||
|         ret = config.getoption(name)  # 'default' arg won't work as expected |         ret = config.getoption(name)  # 'default' arg won't work as expected | ||||||
|  | @ -48,6 +101,9 @@ def pytest_addoption(parser): | ||||||
|         '--log-date-format', |         '--log-date-format', | ||||||
|         dest='log_date_format', default=DEFAULT_LOG_DATE_FORMAT, |         dest='log_date_format', default=DEFAULT_LOG_DATE_FORMAT, | ||||||
|         help='log date format as used by the logging module.') |         help='log date format as used by the logging module.') | ||||||
|  |     parser.addini( | ||||||
|  |         'log_cli', default=False, type='bool', | ||||||
|  |         help='enable log display during test run (also known as "live logging").') | ||||||
|     add_option_ini( |     add_option_ini( | ||||||
|         '--log-cli-level', |         '--log-cli-level', | ||||||
|         dest='log_cli_level', default=None, |         dest='log_cli_level', default=None, | ||||||
|  | @ -79,13 +135,14 @@ def pytest_addoption(parser): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @contextmanager | @contextmanager | ||||||
| def catching_logs(handler, formatter=None, level=logging.NOTSET): | def catching_logs(handler, formatter=None, level=None): | ||||||
|     """Context manager that prepares the whole logging machinery properly.""" |     """Context manager that prepares the whole logging machinery properly.""" | ||||||
|     root_logger = logging.getLogger() |     root_logger = logging.getLogger() | ||||||
| 
 | 
 | ||||||
|     if formatter is not None: |     if formatter is not None: | ||||||
|         handler.setFormatter(formatter) |         handler.setFormatter(formatter) | ||||||
|     handler.setLevel(level) |     if level is not None: | ||||||
|  |         handler.setLevel(level) | ||||||
| 
 | 
 | ||||||
|     # Adding the same handler twice would confuse logging system. |     # Adding the same handler twice would confuse logging system. | ||||||
|     # Just don't do that. |     # Just don't do that. | ||||||
|  | @ -93,12 +150,14 @@ def catching_logs(handler, formatter=None, level=logging.NOTSET): | ||||||
| 
 | 
 | ||||||
|     if add_new_handler: |     if add_new_handler: | ||||||
|         root_logger.addHandler(handler) |         root_logger.addHandler(handler) | ||||||
|     orig_level = root_logger.level |     if level is not None: | ||||||
|     root_logger.setLevel(min(orig_level, level)) |         orig_level = root_logger.level | ||||||
|  |         root_logger.setLevel(level) | ||||||
|     try: |     try: | ||||||
|         yield handler |         yield handler | ||||||
|     finally: |     finally: | ||||||
|         root_logger.setLevel(orig_level) |         if level is not None: | ||||||
|  |             root_logger.setLevel(orig_level) | ||||||
|         if add_new_handler: |         if add_new_handler: | ||||||
|             root_logger.removeHandler(handler) |             root_logger.removeHandler(handler) | ||||||
| 
 | 
 | ||||||
|  | @ -123,11 +182,40 @@ class LogCaptureFixture(object): | ||||||
|     def __init__(self, item): |     def __init__(self, item): | ||||||
|         """Creates a new funcarg.""" |         """Creates a new funcarg.""" | ||||||
|         self._item = item |         self._item = item | ||||||
|  |         self._initial_log_levels = {}  # type: Dict[str, int] # dict of log name -> log level | ||||||
|  | 
 | ||||||
|  |     def _finalize(self): | ||||||
|  |         """Finalizes the fixture. | ||||||
|  | 
 | ||||||
|  |         This restores the log levels changed by :meth:`set_level`. | ||||||
|  |         """ | ||||||
|  |         # restore log levels | ||||||
|  |         for logger_name, level in self._initial_log_levels.items(): | ||||||
|  |             logger = logging.getLogger(logger_name) | ||||||
|  |             logger.setLevel(level) | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def handler(self): |     def handler(self): | ||||||
|         return self._item.catch_log_handler |         return self._item.catch_log_handler | ||||||
| 
 | 
 | ||||||
|  |     def get_records(self, when): | ||||||
|  |         """ | ||||||
|  |         Get the logging records for one of the possible test phases. | ||||||
|  | 
 | ||||||
|  |         :param str when: | ||||||
|  |             Which test phase to obtain the records from. Valid values are: "setup", "call" and "teardown". | ||||||
|  | 
 | ||||||
|  |         :rtype: List[logging.LogRecord] | ||||||
|  |         :return: the list of captured records at the given stage | ||||||
|  | 
 | ||||||
|  |         .. versionadded:: 3.4 | ||||||
|  |         """ | ||||||
|  |         handler = self._item.catch_log_handlers.get(when) | ||||||
|  |         if handler: | ||||||
|  |             return handler.records | ||||||
|  |         else: | ||||||
|  |             return [] | ||||||
|  | 
 | ||||||
|     @property |     @property | ||||||
|     def text(self): |     def text(self): | ||||||
|         """Returns the log text.""" |         """Returns the log text.""" | ||||||
|  | @ -154,31 +242,31 @@ class LogCaptureFixture(object): | ||||||
|         self.handler.records = [] |         self.handler.records = [] | ||||||
| 
 | 
 | ||||||
|     def set_level(self, level, logger=None): |     def set_level(self, level, logger=None): | ||||||
|         """Sets the level for capturing of logs. |         """Sets the level for capturing of logs. The level will be restored to its previous value at the end of | ||||||
|  |         the test. | ||||||
| 
 | 
 | ||||||
|         By default, the level is set on the handler used to capture |         :param int level: the logger to level. | ||||||
|         logs. Specify a logger name to instead set the level of any |         :param str logger: the logger to update the level. If not given, the root logger level is updated. | ||||||
|         logger. | 
 | ||||||
|  |         .. versionchanged:: 3.4 | ||||||
|  |             The levels of the loggers changed by this function will be restored to their initial values at the | ||||||
|  |             end of the test. | ||||||
|         """ |         """ | ||||||
|         if logger is None: |         logger_name = logger | ||||||
|             logger = self.handler |         logger = logging.getLogger(logger_name) | ||||||
|         else: |         # save the original log-level to restore it during teardown | ||||||
|             logger = logging.getLogger(logger) |         self._initial_log_levels.setdefault(logger_name, logger.level) | ||||||
|         logger.setLevel(level) |         logger.setLevel(level) | ||||||
| 
 | 
 | ||||||
|     @contextmanager |     @contextmanager | ||||||
|     def at_level(self, level, logger=None): |     def at_level(self, level, logger=None): | ||||||
|         """Context manager that sets the level for capturing of logs. |         """Context manager that sets the level for capturing of logs. After the end of the 'with' statement the | ||||||
|  |         level is restored to its original value. | ||||||
| 
 | 
 | ||||||
|         By default, the level is set on the handler used to capture |         :param int level: the logger to level. | ||||||
|         logs. Specify a logger name to instead set the level of any |         :param str logger: the logger to update the level. If not given, the root logger level is updated. | ||||||
|         logger. |  | ||||||
|         """ |         """ | ||||||
|         if logger is None: |         logger = logging.getLogger(logger) | ||||||
|             logger = self.handler |  | ||||||
|         else: |  | ||||||
|             logger = logging.getLogger(logger) |  | ||||||
| 
 |  | ||||||
|         orig_level = logger.level |         orig_level = logger.level | ||||||
|         logger.setLevel(level) |         logger.setLevel(level) | ||||||
|         try: |         try: | ||||||
|  | @ -197,7 +285,9 @@ def caplog(request): | ||||||
|     * caplog.records()       -> list of logging.LogRecord instances |     * caplog.records()       -> list of logging.LogRecord instances | ||||||
|     * caplog.record_tuples() -> list of (logger_name, level, message) tuples |     * caplog.record_tuples() -> list of (logger_name, level, message) tuples | ||||||
|     """ |     """ | ||||||
|     return LogCaptureFixture(request.node) |     result = LogCaptureFixture(request.node) | ||||||
|  |     yield result | ||||||
|  |     result._finalize() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_actual_log_level(config, *setting_names): | def get_actual_log_level(config, *setting_names): | ||||||
|  | @ -227,8 +317,12 @@ def get_actual_log_level(config, *setting_names): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def pytest_configure(config): | def pytest_configure(config): | ||||||
|     config.pluginmanager.register(LoggingPlugin(config), |     config.pluginmanager.register(LoggingPlugin(config), 'logging-plugin') | ||||||
|                                   'logging-plugin') | 
 | ||||||
|  | 
 | ||||||
|  | @contextmanager | ||||||
|  | def _dummy_context_manager(): | ||||||
|  |     yield | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class LoggingPlugin(object): | class LoggingPlugin(object): | ||||||
|  | @ -241,57 +335,52 @@ class LoggingPlugin(object): | ||||||
|         The formatter can be safely shared across all handlers so |         The formatter can be safely shared across all handlers so | ||||||
|         create a single one for the entire test session here. |         create a single one for the entire test session here. | ||||||
|         """ |         """ | ||||||
|         self.log_cli_level = get_actual_log_level( |         self._config = config | ||||||
|             config, 'log_cli_level', 'log_level') or logging.WARNING | 
 | ||||||
|  |         # enable verbose output automatically if live logging is enabled | ||||||
|  |         if self._config.getini('log_cli') and not config.getoption('verbose'): | ||||||
|  |             # sanity check: terminal reporter should not have been loaded at this point | ||||||
|  |             assert self._config.pluginmanager.get_plugin('terminalreporter') is None | ||||||
|  |             config.option.verbose = 1 | ||||||
| 
 | 
 | ||||||
|         self.print_logs = get_option_ini(config, 'log_print') |         self.print_logs = get_option_ini(config, 'log_print') | ||||||
|         self.formatter = logging.Formatter( |         self.formatter = logging.Formatter(get_option_ini(config, 'log_format'), | ||||||
|             get_option_ini(config, 'log_format'), |                                            get_option_ini(config, 'log_date_format')) | ||||||
|             get_option_ini(config, 'log_date_format')) |         self.log_level = get_actual_log_level(config, 'log_level') | ||||||
| 
 |  | ||||||
|         log_cli_handler = logging.StreamHandler(sys.stderr) |  | ||||||
|         log_cli_format = get_option_ini( |  | ||||||
|             config, 'log_cli_format', 'log_format') |  | ||||||
|         log_cli_date_format = get_option_ini( |  | ||||||
|             config, 'log_cli_date_format', 'log_date_format') |  | ||||||
|         log_cli_formatter = logging.Formatter( |  | ||||||
|             log_cli_format, |  | ||||||
|             datefmt=log_cli_date_format) |  | ||||||
|         self.log_cli_handler = log_cli_handler  # needed for a single unittest |  | ||||||
|         self.live_logs = catching_logs(log_cli_handler, |  | ||||||
|                                        formatter=log_cli_formatter, |  | ||||||
|                                        level=self.log_cli_level) |  | ||||||
| 
 | 
 | ||||||
|         log_file = get_option_ini(config, 'log_file') |         log_file = get_option_ini(config, 'log_file') | ||||||
|         if log_file: |         if log_file: | ||||||
|             self.log_file_level = get_actual_log_level( |             self.log_file_level = get_actual_log_level(config, 'log_file_level') | ||||||
|                 config, 'log_file_level') or logging.WARNING |  | ||||||
| 
 | 
 | ||||||
|             log_file_format = get_option_ini( |             log_file_format = get_option_ini(config, 'log_file_format', 'log_format') | ||||||
|                 config, 'log_file_format', 'log_format') |             log_file_date_format = get_option_ini(config, 'log_file_date_format', 'log_date_format') | ||||||
|             log_file_date_format = get_option_ini( |             # Each pytest runtests session will write to a clean logfile | ||||||
|                 config, 'log_file_date_format', 'log_date_format') |             self.log_file_handler = logging.FileHandler(log_file, mode='w') | ||||||
|             self.log_file_handler = logging.FileHandler( |             log_file_formatter = logging.Formatter(log_file_format, datefmt=log_file_date_format) | ||||||
|                 log_file, |  | ||||||
|                 # Each pytest runtests session will write to a clean logfile |  | ||||||
|                 mode='w') |  | ||||||
|             log_file_formatter = logging.Formatter( |  | ||||||
|                 log_file_format, |  | ||||||
|                 datefmt=log_file_date_format) |  | ||||||
|             self.log_file_handler.setFormatter(log_file_formatter) |             self.log_file_handler.setFormatter(log_file_formatter) | ||||||
|         else: |         else: | ||||||
|             self.log_file_handler = None |             self.log_file_handler = None | ||||||
| 
 | 
 | ||||||
|  |         # initialized during pytest_runtestloop | ||||||
|  |         self.log_cli_handler = None | ||||||
|  | 
 | ||||||
|     @contextmanager |     @contextmanager | ||||||
|     def _runtest_for(self, item, when): |     def _runtest_for(self, item, when): | ||||||
|         """Implements the internals of pytest_runtest_xxx() hook.""" |         """Implements the internals of pytest_runtest_xxx() hook.""" | ||||||
|         with catching_logs(LogCaptureHandler(), |         with catching_logs(LogCaptureHandler(), | ||||||
|                            formatter=self.formatter) as log_handler: |                            formatter=self.formatter, level=self.log_level) as log_handler: | ||||||
|  |             if self.log_cli_handler: | ||||||
|  |                 self.log_cli_handler.set_when(when) | ||||||
|  |             if not hasattr(item, 'catch_log_handlers'): | ||||||
|  |                 item.catch_log_handlers = {} | ||||||
|  |             item.catch_log_handlers[when] = log_handler | ||||||
|             item.catch_log_handler = log_handler |             item.catch_log_handler = log_handler | ||||||
|             try: |             try: | ||||||
|                 yield  # run test |                 yield  # run test | ||||||
|             finally: |             finally: | ||||||
|                 del item.catch_log_handler |                 del item.catch_log_handler | ||||||
|  |                 if when == 'teardown': | ||||||
|  |                     del item.catch_log_handlers | ||||||
| 
 | 
 | ||||||
|             if self.print_logs: |             if self.print_logs: | ||||||
|                 # Add a captured log section to the report. |                 # Add a captured log section to the report. | ||||||
|  | @ -313,10 +402,15 @@ class LoggingPlugin(object): | ||||||
|         with self._runtest_for(item, 'teardown'): |         with self._runtest_for(item, 'teardown'): | ||||||
|             yield |             yield | ||||||
| 
 | 
 | ||||||
|  |     def pytest_runtest_logstart(self): | ||||||
|  |         if self.log_cli_handler: | ||||||
|  |             self.log_cli_handler.reset() | ||||||
|  | 
 | ||||||
|     @pytest.hookimpl(hookwrapper=True) |     @pytest.hookimpl(hookwrapper=True) | ||||||
|     def pytest_runtestloop(self, session): |     def pytest_runtestloop(self, session): | ||||||
|         """Runs all collected test items.""" |         """Runs all collected test items.""" | ||||||
|         with self.live_logs: |         self._setup_cli_logging() | ||||||
|  |         with self.live_logs_context: | ||||||
|             if self.log_file_handler is not None: |             if self.log_file_handler is not None: | ||||||
|                 with closing(self.log_file_handler): |                 with closing(self.log_file_handler): | ||||||
|                     with catching_logs(self.log_file_handler, |                     with catching_logs(self.log_file_handler, | ||||||
|  | @ -324,3 +418,69 @@ class LoggingPlugin(object): | ||||||
|                         yield  # run all the tests |                         yield  # run all the tests | ||||||
|             else: |             else: | ||||||
|                 yield  # run all the tests |                 yield  # run all the tests | ||||||
|  | 
 | ||||||
|  |     def _setup_cli_logging(self): | ||||||
|  |         """Sets up the handler and logger for the Live Logs feature, if enabled. | ||||||
|  | 
 | ||||||
|  |         This must be done right before starting the loop so we can access the terminal reporter plugin. | ||||||
|  |         """ | ||||||
|  |         terminal_reporter = self._config.pluginmanager.get_plugin('terminalreporter') | ||||||
|  |         if self._config.getini('log_cli') and terminal_reporter is not None: | ||||||
|  |             capture_manager = self._config.pluginmanager.get_plugin('capturemanager') | ||||||
|  |             log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager) | ||||||
|  |             log_cli_format = get_option_ini(self._config, 'log_cli_format', 'log_format') | ||||||
|  |             log_cli_date_format = get_option_ini(self._config, 'log_cli_date_format', 'log_date_format') | ||||||
|  |             if self._config.option.color != 'no' and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format): | ||||||
|  |                 log_cli_formatter = ColoredLevelFormatter(create_terminal_writer(self._config), | ||||||
|  |                                                           log_cli_format, datefmt=log_cli_date_format) | ||||||
|  |             else: | ||||||
|  |                 log_cli_formatter = logging.Formatter(log_cli_format, datefmt=log_cli_date_format) | ||||||
|  |             log_cli_level = get_actual_log_level(self._config, 'log_cli_level', 'log_level') | ||||||
|  |             self.log_cli_handler = log_cli_handler | ||||||
|  |             self.live_logs_context = catching_logs(log_cli_handler, formatter=log_cli_formatter, level=log_cli_level) | ||||||
|  |         else: | ||||||
|  |             self.live_logs_context = _dummy_context_manager() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class _LiveLoggingStreamHandler(logging.StreamHandler): | ||||||
|  |     """ | ||||||
|  |     Custom StreamHandler used by the live logging feature: it will write a newline before the first log message | ||||||
|  |     in each test. | ||||||
|  | 
 | ||||||
|  |     During live logging we must also explicitly disable stdout/stderr capturing otherwise it will get captured | ||||||
|  |     and won't appear in the terminal. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self, terminal_reporter, capture_manager): | ||||||
|  |         """ | ||||||
|  |         :param _pytest.terminal.TerminalReporter terminal_reporter: | ||||||
|  |         :param _pytest.capture.CaptureManager capture_manager: | ||||||
|  |         """ | ||||||
|  |         logging.StreamHandler.__init__(self, stream=terminal_reporter) | ||||||
|  |         self.capture_manager = capture_manager | ||||||
|  |         self.reset() | ||||||
|  |         self.set_when(None) | ||||||
|  | 
 | ||||||
|  |     def reset(self): | ||||||
|  |         """Reset the handler; should be called before the start of each test""" | ||||||
|  |         self._first_record_emitted = False | ||||||
|  | 
 | ||||||
|  |     def set_when(self, when): | ||||||
|  |         """Prepares for the given test phase (setup/call/teardown)""" | ||||||
|  |         self._when = when | ||||||
|  |         self._section_name_shown = False | ||||||
|  | 
 | ||||||
|  |     def emit(self, record): | ||||||
|  |         if self.capture_manager is not None: | ||||||
|  |             self.capture_manager.suspend_global_capture() | ||||||
|  |         try: | ||||||
|  |             if not self._first_record_emitted or self._when == 'teardown': | ||||||
|  |                 self.stream.write('\n') | ||||||
|  |                 self._first_record_emitted = True | ||||||
|  |             if not self._section_name_shown: | ||||||
|  |                 self.stream.section('live log ' + self._when, sep='-', bold=True) | ||||||
|  |                 self._section_name_shown = True | ||||||
|  |             logging.StreamHandler.emit(self, record) | ||||||
|  |         finally: | ||||||
|  |             if self.capture_manager is not None: | ||||||
|  |                 self.capture_manager.resume_global_capture() | ||||||
|  |  | ||||||
							
								
								
									
										372
									
								
								_pytest/main.py
								
								
								
								
							
							
						
						
									
										372
									
								
								_pytest/main.py
								
								
								
								
							|  | @ -12,16 +12,11 @@ import _pytest | ||||||
| from _pytest import nodes | from _pytest import nodes | ||||||
| import _pytest._code | import _pytest._code | ||||||
| import py | import py | ||||||
| try: |  | ||||||
|     from collections import MutableMapping as MappingMixin |  | ||||||
| except ImportError: |  | ||||||
|     from UserDict import DictMixin as MappingMixin |  | ||||||
| 
 | 
 | ||||||
| from _pytest.config import directory_arg, UsageError, hookimpl | from _pytest.config import directory_arg, UsageError, hookimpl | ||||||
| from _pytest.outcomes import exit | from _pytest.outcomes import exit | ||||||
| from _pytest.runner import collect_one_node | from _pytest.runner import collect_one_node | ||||||
| 
 | 
 | ||||||
| tracebackcutdir = py.path.local(_pytest.__file__).dirpath() |  | ||||||
| 
 | 
 | ||||||
| # exitcodes for the command line | # exitcodes for the command line | ||||||
| EXIT_OK = 0 | EXIT_OK = 0 | ||||||
|  | @ -248,7 +243,7 @@ def _patched_find_module(): | ||||||
|         yield |         yield | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class FSHookProxy: | class FSHookProxy(object): | ||||||
|     def __init__(self, fspath, pm, remove_mods): |     def __init__(self, fspath, pm, remove_mods): | ||||||
|         self.fspath = fspath |         self.fspath = fspath | ||||||
|         self.pm = pm |         self.pm = pm | ||||||
|  | @ -260,356 +255,6 @@ class FSHookProxy: | ||||||
|         return x |         return x | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class _CompatProperty(object): |  | ||||||
|     def __init__(self, name): |  | ||||||
|         self.name = name |  | ||||||
| 
 |  | ||||||
|     def __get__(self, obj, owner): |  | ||||||
|         if obj is None: |  | ||||||
|             return self |  | ||||||
| 
 |  | ||||||
|         # TODO: reenable in the features branch |  | ||||||
|         # warnings.warn( |  | ||||||
|         #     "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format( |  | ||||||
|         #         name=self.name, owner=type(owner).__name__), |  | ||||||
|         #     PendingDeprecationWarning, stacklevel=2) |  | ||||||
|         return getattr(__import__('pytest'), self.name) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class NodeKeywords(MappingMixin): |  | ||||||
|     def __init__(self, node): |  | ||||||
|         self.node = node |  | ||||||
|         self.parent = node.parent |  | ||||||
|         self._markers = {node.name: True} |  | ||||||
| 
 |  | ||||||
|     def __getitem__(self, key): |  | ||||||
|         try: |  | ||||||
|             return self._markers[key] |  | ||||||
|         except KeyError: |  | ||||||
|             if self.parent is None: |  | ||||||
|                 raise |  | ||||||
|             return self.parent.keywords[key] |  | ||||||
| 
 |  | ||||||
|     def __setitem__(self, key, value): |  | ||||||
|         self._markers[key] = value |  | ||||||
| 
 |  | ||||||
|     def __delitem__(self, key): |  | ||||||
|         raise ValueError("cannot delete key in keywords dict") |  | ||||||
| 
 |  | ||||||
|     def __iter__(self): |  | ||||||
|         seen = set(self._markers) |  | ||||||
|         if self.parent is not None: |  | ||||||
|             seen.update(self.parent.keywords) |  | ||||||
|         return iter(seen) |  | ||||||
| 
 |  | ||||||
|     def __len__(self): |  | ||||||
|         return len(self.__iter__()) |  | ||||||
| 
 |  | ||||||
|     def keys(self): |  | ||||||
|         return list(self) |  | ||||||
| 
 |  | ||||||
|     def __repr__(self): |  | ||||||
|         return "<NodeKeywords for node %s>" % (self.node, ) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Node(object): |  | ||||||
|     """ base class for Collector and Item the test collection tree. |  | ||||||
|     Collector subclasses have children, Items are terminal nodes.""" |  | ||||||
| 
 |  | ||||||
|     def __init__(self, name, parent=None, config=None, session=None): |  | ||||||
|         #: a unique name within the scope of the parent node |  | ||||||
|         self.name = name |  | ||||||
| 
 |  | ||||||
|         #: the parent collector node. |  | ||||||
|         self.parent = parent |  | ||||||
| 
 |  | ||||||
|         #: the pytest config object |  | ||||||
|         self.config = config or parent.config |  | ||||||
| 
 |  | ||||||
|         #: the session this node is part of |  | ||||||
|         self.session = session or parent.session |  | ||||||
| 
 |  | ||||||
|         #: filesystem path where this node was collected from (can be None) |  | ||||||
|         self.fspath = getattr(parent, 'fspath', None) |  | ||||||
| 
 |  | ||||||
|         #: keywords/markers collected from all scopes |  | ||||||
|         self.keywords = NodeKeywords(self) |  | ||||||
| 
 |  | ||||||
|         #: allow adding of extra keywords to use for matching |  | ||||||
|         self.extra_keyword_matches = set() |  | ||||||
| 
 |  | ||||||
|         # used for storing artificial fixturedefs for direct parametrization |  | ||||||
|         self._name2pseudofixturedef = {} |  | ||||||
| 
 |  | ||||||
|     @property |  | ||||||
|     def ihook(self): |  | ||||||
|         """ fspath sensitive hook proxy used to call pytest hooks""" |  | ||||||
|         return self.session.gethookproxy(self.fspath) |  | ||||||
| 
 |  | ||||||
|     Module = _CompatProperty("Module") |  | ||||||
|     Class = _CompatProperty("Class") |  | ||||||
|     Instance = _CompatProperty("Instance") |  | ||||||
|     Function = _CompatProperty("Function") |  | ||||||
|     File = _CompatProperty("File") |  | ||||||
|     Item = _CompatProperty("Item") |  | ||||||
| 
 |  | ||||||
|     def _getcustomclass(self, name): |  | ||||||
|         maybe_compatprop = getattr(type(self), name) |  | ||||||
|         if isinstance(maybe_compatprop, _CompatProperty): |  | ||||||
|             return getattr(__import__('pytest'), name) |  | ||||||
|         else: |  | ||||||
|             cls = getattr(self, name) |  | ||||||
|             # TODO: reenable in the features branch |  | ||||||
|             # warnings.warn("use of node.%s is deprecated, " |  | ||||||
|             #    "use pytest_pycollect_makeitem(...) to create custom " |  | ||||||
|             #    "collection nodes" % name, category=DeprecationWarning) |  | ||||||
|         return cls |  | ||||||
| 
 |  | ||||||
|     def __repr__(self): |  | ||||||
|         return "<%s %r>" % (self.__class__.__name__, |  | ||||||
|                             getattr(self, 'name', None)) |  | ||||||
| 
 |  | ||||||
|     def warn(self, code, message): |  | ||||||
|         """ generate a warning with the given code and message for this |  | ||||||
|         item. """ |  | ||||||
|         assert isinstance(code, str) |  | ||||||
|         fslocation = getattr(self, "location", None) |  | ||||||
|         if fslocation is None: |  | ||||||
|             fslocation = getattr(self, "fspath", None) |  | ||||||
|         self.ihook.pytest_logwarning.call_historic(kwargs=dict( |  | ||||||
|             code=code, message=message, |  | ||||||
|             nodeid=self.nodeid, fslocation=fslocation)) |  | ||||||
| 
 |  | ||||||
|     # methods for ordering nodes |  | ||||||
|     @property |  | ||||||
|     def nodeid(self): |  | ||||||
|         """ a ::-separated string denoting its collection tree address. """ |  | ||||||
|         try: |  | ||||||
|             return self._nodeid |  | ||||||
|         except AttributeError: |  | ||||||
|             self._nodeid = x = self._makeid() |  | ||||||
|             return x |  | ||||||
| 
 |  | ||||||
|     def _makeid(self): |  | ||||||
|         return self.parent.nodeid + "::" + self.name |  | ||||||
| 
 |  | ||||||
|     def __hash__(self): |  | ||||||
|         return hash(self.nodeid) |  | ||||||
| 
 |  | ||||||
|     def setup(self): |  | ||||||
|         pass |  | ||||||
| 
 |  | ||||||
|     def teardown(self): |  | ||||||
|         pass |  | ||||||
| 
 |  | ||||||
|     def listchain(self): |  | ||||||
|         """ return list of all parent collectors up to self, |  | ||||||
|             starting from root of collection tree. """ |  | ||||||
|         chain = [] |  | ||||||
|         item = self |  | ||||||
|         while item is not None: |  | ||||||
|             chain.append(item) |  | ||||||
|             item = item.parent |  | ||||||
|         chain.reverse() |  | ||||||
|         return chain |  | ||||||
| 
 |  | ||||||
|     def add_marker(self, marker): |  | ||||||
|         """ dynamically add a marker object to the node. |  | ||||||
| 
 |  | ||||||
|         ``marker`` can be a string or pytest.mark.* instance. |  | ||||||
|         """ |  | ||||||
|         from _pytest.mark import MarkDecorator, MARK_GEN |  | ||||||
|         if isinstance(marker, six.string_types): |  | ||||||
|             marker = getattr(MARK_GEN, marker) |  | ||||||
|         elif not isinstance(marker, MarkDecorator): |  | ||||||
|             raise ValueError("is not a string or pytest.mark.* Marker") |  | ||||||
|         self.keywords[marker.name] = marker |  | ||||||
| 
 |  | ||||||
|     def get_marker(self, name): |  | ||||||
|         """ get a marker object from this node or None if |  | ||||||
|         the node doesn't have a marker with that name. """ |  | ||||||
|         val = self.keywords.get(name, None) |  | ||||||
|         if val is not None: |  | ||||||
|             from _pytest.mark import MarkInfo, MarkDecorator |  | ||||||
|             if isinstance(val, (MarkDecorator, MarkInfo)): |  | ||||||
|                 return val |  | ||||||
| 
 |  | ||||||
|     def listextrakeywords(self): |  | ||||||
|         """ Return a set of all extra keywords in self and any parents.""" |  | ||||||
|         extra_keywords = set() |  | ||||||
|         item = self |  | ||||||
|         for item in self.listchain(): |  | ||||||
|             extra_keywords.update(item.extra_keyword_matches) |  | ||||||
|         return extra_keywords |  | ||||||
| 
 |  | ||||||
|     def listnames(self): |  | ||||||
|         return [x.name for x in self.listchain()] |  | ||||||
| 
 |  | ||||||
|     def addfinalizer(self, fin): |  | ||||||
|         """ register a function to be called when this node is finalized. |  | ||||||
| 
 |  | ||||||
|         This method can only be called when this node is active |  | ||||||
|         in a setup chain, for example during self.setup(). |  | ||||||
|         """ |  | ||||||
|         self.session._setupstate.addfinalizer(fin, self) |  | ||||||
| 
 |  | ||||||
|     def getparent(self, cls): |  | ||||||
|         """ get the next parent node (including ourself) |  | ||||||
|         which is an instance of the given class""" |  | ||||||
|         current = self |  | ||||||
|         while current and not isinstance(current, cls): |  | ||||||
|             current = current.parent |  | ||||||
|         return current |  | ||||||
| 
 |  | ||||||
|     def _prunetraceback(self, excinfo): |  | ||||||
|         pass |  | ||||||
| 
 |  | ||||||
|     def _repr_failure_py(self, excinfo, style=None): |  | ||||||
|         fm = self.session._fixturemanager |  | ||||||
|         if excinfo.errisinstance(fm.FixtureLookupError): |  | ||||||
|             return excinfo.value.formatrepr() |  | ||||||
|         tbfilter = True |  | ||||||
|         if self.config.option.fulltrace: |  | ||||||
|             style = "long" |  | ||||||
|         else: |  | ||||||
|             tb = _pytest._code.Traceback([excinfo.traceback[-1]]) |  | ||||||
|             self._prunetraceback(excinfo) |  | ||||||
|             if len(excinfo.traceback) == 0: |  | ||||||
|                 excinfo.traceback = tb |  | ||||||
|             tbfilter = False  # prunetraceback already does it |  | ||||||
|             if style == "auto": |  | ||||||
|                 style = "long" |  | ||||||
|         # XXX should excinfo.getrepr record all data and toterminal() process it? |  | ||||||
|         if style is None: |  | ||||||
|             if self.config.option.tbstyle == "short": |  | ||||||
|                 style = "short" |  | ||||||
|             else: |  | ||||||
|                 style = "long" |  | ||||||
| 
 |  | ||||||
|         try: |  | ||||||
|             os.getcwd() |  | ||||||
|             abspath = False |  | ||||||
|         except OSError: |  | ||||||
|             abspath = True |  | ||||||
| 
 |  | ||||||
|         return excinfo.getrepr(funcargs=True, abspath=abspath, |  | ||||||
|                                showlocals=self.config.option.showlocals, |  | ||||||
|                                style=style, tbfilter=tbfilter) |  | ||||||
| 
 |  | ||||||
|     repr_failure = _repr_failure_py |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Collector(Node): |  | ||||||
|     """ Collector instances create children through collect() |  | ||||||
|         and thus iteratively build a tree. |  | ||||||
|     """ |  | ||||||
| 
 |  | ||||||
|     class CollectError(Exception): |  | ||||||
|         """ an error during collection, contains a custom message. """ |  | ||||||
| 
 |  | ||||||
|     def collect(self): |  | ||||||
|         """ returns a list of children (items and collectors) |  | ||||||
|             for this collection node. |  | ||||||
|         """ |  | ||||||
|         raise NotImplementedError("abstract") |  | ||||||
| 
 |  | ||||||
|     def repr_failure(self, excinfo): |  | ||||||
|         """ represent a collection failure. """ |  | ||||||
|         if excinfo.errisinstance(self.CollectError): |  | ||||||
|             exc = excinfo.value |  | ||||||
|             return str(exc.args[0]) |  | ||||||
|         return self._repr_failure_py(excinfo, style="short") |  | ||||||
| 
 |  | ||||||
|     def _prunetraceback(self, excinfo): |  | ||||||
|         if hasattr(self, 'fspath'): |  | ||||||
|             traceback = excinfo.traceback |  | ||||||
|             ntraceback = traceback.cut(path=self.fspath) |  | ||||||
|             if ntraceback == traceback: |  | ||||||
|                 ntraceback = ntraceback.cut(excludepath=tracebackcutdir) |  | ||||||
|             excinfo.traceback = ntraceback.filter() |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class FSCollector(Collector): |  | ||||||
|     def __init__(self, fspath, parent=None, config=None, session=None): |  | ||||||
|         fspath = py.path.local(fspath)  # xxx only for test_resultlog.py? |  | ||||||
|         name = fspath.basename |  | ||||||
|         if parent is not None: |  | ||||||
|             rel = fspath.relto(parent.fspath) |  | ||||||
|             if rel: |  | ||||||
|                 name = rel |  | ||||||
|             name = name.replace(os.sep, nodes.SEP) |  | ||||||
|         super(FSCollector, self).__init__(name, parent, config, session) |  | ||||||
|         self.fspath = fspath |  | ||||||
| 
 |  | ||||||
|     def _check_initialpaths_for_relpath(self): |  | ||||||
|         for initialpath in self.session._initialpaths: |  | ||||||
|             if self.fspath.common(initialpath) == initialpath: |  | ||||||
|                 return self.fspath.relto(initialpath.dirname) |  | ||||||
| 
 |  | ||||||
|     def _makeid(self): |  | ||||||
|         relpath = self.fspath.relto(self.config.rootdir) |  | ||||||
| 
 |  | ||||||
|         if not relpath: |  | ||||||
|             relpath = self._check_initialpaths_for_relpath() |  | ||||||
|         if os.sep != nodes.SEP: |  | ||||||
|             relpath = relpath.replace(os.sep, nodes.SEP) |  | ||||||
|         return relpath |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class File(FSCollector): |  | ||||||
|     """ base class for collecting tests from a file. """ |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Item(Node): |  | ||||||
|     """ a basic test invocation item. Note that for a single function |  | ||||||
|     there might be multiple test invocation items. |  | ||||||
|     """ |  | ||||||
|     nextitem = None |  | ||||||
| 
 |  | ||||||
|     def __init__(self, name, parent=None, config=None, session=None): |  | ||||||
|         super(Item, self).__init__(name, parent, config, session) |  | ||||||
|         self._report_sections = [] |  | ||||||
| 
 |  | ||||||
|     def add_report_section(self, when, key, content): |  | ||||||
|         """ |  | ||||||
|         Adds a new report section, similar to what's done internally to add stdout and |  | ||||||
|         stderr captured output:: |  | ||||||
| 
 |  | ||||||
|             item.add_report_section("call", "stdout", "report section contents") |  | ||||||
| 
 |  | ||||||
|         :param str when: |  | ||||||
|             One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``. |  | ||||||
|         :param str key: |  | ||||||
|             Name of the section, can be customized at will. Pytest uses ``"stdout"`` and |  | ||||||
|             ``"stderr"`` internally. |  | ||||||
| 
 |  | ||||||
|         :param str content: |  | ||||||
|             The full contents as a string. |  | ||||||
|         """ |  | ||||||
|         if content: |  | ||||||
|             self._report_sections.append((when, key, content)) |  | ||||||
| 
 |  | ||||||
|     def reportinfo(self): |  | ||||||
|         return self.fspath, None, "" |  | ||||||
| 
 |  | ||||||
|     @property |  | ||||||
|     def location(self): |  | ||||||
|         try: |  | ||||||
|             return self._location |  | ||||||
|         except AttributeError: |  | ||||||
|             location = self.reportinfo() |  | ||||||
|             # bestrelpath is a quite slow function |  | ||||||
|             cache = self.config.__dict__.setdefault("_bestrelpathcache", {}) |  | ||||||
|             try: |  | ||||||
|                 fspath = cache[location[0]] |  | ||||||
|             except KeyError: |  | ||||||
|                 fspath = self.session.fspath.bestrelpath(location[0]) |  | ||||||
|                 cache[location[0]] = fspath |  | ||||||
|             location = (fspath, location[1], str(location[2])) |  | ||||||
|             self._location = location |  | ||||||
|             return location |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class NoMatch(Exception): | class NoMatch(Exception): | ||||||
|     """ raised if matching cannot locate a matching names. """ |     """ raised if matching cannot locate a matching names. """ | ||||||
| 
 | 
 | ||||||
|  | @ -623,13 +268,14 @@ class Failed(Exception): | ||||||
|     """ signals an stop as failed test run. """ |     """ signals an stop as failed test run. """ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Session(FSCollector): | class Session(nodes.FSCollector): | ||||||
|     Interrupted = Interrupted |     Interrupted = Interrupted | ||||||
|     Failed = Failed |     Failed = Failed | ||||||
| 
 | 
 | ||||||
|     def __init__(self, config): |     def __init__(self, config): | ||||||
|         FSCollector.__init__(self, config.rootdir, parent=None, |         nodes.FSCollector.__init__( | ||||||
|                              config=config, session=self) |             self, config.rootdir, parent=None, | ||||||
|  |             config=config, session=self) | ||||||
|         self.testsfailed = 0 |         self.testsfailed = 0 | ||||||
|         self.testscollected = 0 |         self.testscollected = 0 | ||||||
|         self.shouldstop = False |         self.shouldstop = False | ||||||
|  | @ -826,11 +472,11 @@ class Session(FSCollector): | ||||||
|         nextnames = names[1:] |         nextnames = names[1:] | ||||||
|         resultnodes = [] |         resultnodes = [] | ||||||
|         for node in matching: |         for node in matching: | ||||||
|             if isinstance(node, Item): |             if isinstance(node, nodes.Item): | ||||||
|                 if not names: |                 if not names: | ||||||
|                     resultnodes.append(node) |                     resultnodes.append(node) | ||||||
|                 continue |                 continue | ||||||
|             assert isinstance(node, Collector) |             assert isinstance(node, nodes.Collector) | ||||||
|             rep = collect_one_node(node) |             rep = collect_one_node(node) | ||||||
|             if rep.passed: |             if rep.passed: | ||||||
|                 has_matched = False |                 has_matched = False | ||||||
|  | @ -852,11 +498,11 @@ class Session(FSCollector): | ||||||
| 
 | 
 | ||||||
|     def genitems(self, node): |     def genitems(self, node): | ||||||
|         self.trace("genitems", node) |         self.trace("genitems", node) | ||||||
|         if isinstance(node, Item): |         if isinstance(node, nodes.Item): | ||||||
|             node.ihook.pytest_itemcollected(item=node) |             node.ihook.pytest_itemcollected(item=node) | ||||||
|             yield node |             yield node | ||||||
|         else: |         else: | ||||||
|             assert isinstance(node, Collector) |             assert isinstance(node, nodes.Collector) | ||||||
|             rep = collect_one_node(node) |             rep = collect_one_node(node) | ||||||
|             if rep.passed: |             if rep.passed: | ||||||
|                 for subnode in rep.result: |                 for subnode in rep.result: | ||||||
|  |  | ||||||
|  | @ -2,14 +2,19 @@ | ||||||
| from __future__ import absolute_import, division, print_function | from __future__ import absolute_import, division, print_function | ||||||
| 
 | 
 | ||||||
| import inspect | import inspect | ||||||
|  | import keyword | ||||||
| import warnings | import warnings | ||||||
| import attr | import attr | ||||||
| from collections import namedtuple | from collections import namedtuple | ||||||
| from operator import attrgetter | from operator import attrgetter | ||||||
| from six.moves import map | from six.moves import map | ||||||
|  | 
 | ||||||
|  | from _pytest.config import UsageError | ||||||
| from .deprecated import MARK_PARAMETERSET_UNPACKING | from .deprecated import MARK_PARAMETERSET_UNPACKING | ||||||
| from .compat import NOTSET, getfslineno | from .compat import NOTSET, getfslineno | ||||||
| 
 | 
 | ||||||
|  | EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| def alias(name, warning=None): | def alias(name, warning=None): | ||||||
|     getter = attrgetter(name) |     getter = attrgetter(name) | ||||||
|  | @ -82,10 +87,7 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')): | ||||||
|         del argvalues |         del argvalues | ||||||
| 
 | 
 | ||||||
|         if not parameters: |         if not parameters: | ||||||
|             fs, lineno = getfslineno(function) |             mark = get_empty_parameterset_mark(config, argnames, function) | ||||||
|             reason = "got empty parameter set %r, function %s at %s:%d" % ( |  | ||||||
|                 argnames, function.__name__, fs, lineno) |  | ||||||
|             mark = MARK_GEN.skip(reason=reason) |  | ||||||
|             parameters.append(ParameterSet( |             parameters.append(ParameterSet( | ||||||
|                 values=(NOTSET,) * len(argnames), |                 values=(NOTSET,) * len(argnames), | ||||||
|                 marks=[mark], |                 marks=[mark], | ||||||
|  | @ -94,6 +96,20 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')): | ||||||
|         return argnames, parameters |         return argnames, parameters | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def get_empty_parameterset_mark(config, argnames, function): | ||||||
|  |     requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) | ||||||
|  |     if requested_mark in ('', None, 'skip'): | ||||||
|  |         mark = MARK_GEN.skip | ||||||
|  |     elif requested_mark == 'xfail': | ||||||
|  |         mark = MARK_GEN.xfail(run=False) | ||||||
|  |     else: | ||||||
|  |         raise LookupError(requested_mark) | ||||||
|  |     fs, lineno = getfslineno(function) | ||||||
|  |     reason = "got empty parameter set %r, function %s at %s:%d" % ( | ||||||
|  |         argnames, function.__name__, fs, lineno) | ||||||
|  |     return mark(reason=reason) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class MarkerError(Exception): | class MarkerError(Exception): | ||||||
| 
 | 
 | ||||||
|     """Error in use of a pytest marker/attribute.""" |     """Error in use of a pytest marker/attribute.""" | ||||||
|  | @ -133,6 +149,9 @@ def pytest_addoption(parser): | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     parser.addini("markers", "markers for test functions", 'linelist') |     parser.addini("markers", "markers for test functions", 'linelist') | ||||||
|  |     parser.addini( | ||||||
|  |         EMPTY_PARAMETERSET_OPTION, | ||||||
|  |         "default marker for empty parametersets") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def pytest_cmdline_main(config): | def pytest_cmdline_main(config): | ||||||
|  | @ -222,6 +241,9 @@ class KeywordMapping(object): | ||||||
|         return False |         return False | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | python_keywords_allowed_list = ["or", "and", "not"] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def matchmark(colitem, markexpr): | def matchmark(colitem, markexpr): | ||||||
|     """Tries to match on any marker names, attached to the given colitem.""" |     """Tries to match on any marker names, attached to the given colitem.""" | ||||||
|     return eval(markexpr, {}, MarkMapping.from_keywords(colitem.keywords)) |     return eval(markexpr, {}, MarkMapping.from_keywords(colitem.keywords)) | ||||||
|  | @ -259,7 +281,13 @@ def matchkeyword(colitem, keywordexpr): | ||||||
|         return mapping[keywordexpr] |         return mapping[keywordexpr] | ||||||
|     elif keywordexpr.startswith("not ") and " " not in keywordexpr[4:]: |     elif keywordexpr.startswith("not ") and " " not in keywordexpr[4:]: | ||||||
|         return not mapping[keywordexpr[4:]] |         return not mapping[keywordexpr[4:]] | ||||||
|     return eval(keywordexpr, {}, mapping) |     for kwd in keywordexpr.split(): | ||||||
|  |         if keyword.iskeyword(kwd) and kwd not in python_keywords_allowed_list: | ||||||
|  |             raise UsageError("Python keyword '{}' not accepted in expressions passed to '-k'".format(kwd)) | ||||||
|  |     try: | ||||||
|  |         return eval(keywordexpr, {}, mapping) | ||||||
|  |     except SyntaxError: | ||||||
|  |         raise UsageError("Wrong expression passed to '-k': {}".format(keywordexpr)) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def pytest_configure(config): | def pytest_configure(config): | ||||||
|  | @ -267,12 +295,19 @@ def pytest_configure(config): | ||||||
|     if config.option.strict: |     if config.option.strict: | ||||||
|         MARK_GEN._config = config |         MARK_GEN._config = config | ||||||
| 
 | 
 | ||||||
|  |     empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION) | ||||||
|  | 
 | ||||||
|  |     if empty_parameterset not in ('skip', 'xfail', None, ''): | ||||||
|  |         raise UsageError( | ||||||
|  |             "{!s} must be one of skip and xfail," | ||||||
|  |             " but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset)) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| def pytest_unconfigure(config): | def pytest_unconfigure(config): | ||||||
|     MARK_GEN._config = getattr(config, '_old_mark_config', None) |     MARK_GEN._config = getattr(config, '_old_mark_config', None) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class MarkGenerator: | class MarkGenerator(object): | ||||||
|     """ Factory for :class:`MarkDecorator` objects - exposed as |     """ Factory for :class:`MarkDecorator` objects - exposed as | ||||||
|     a ``pytest.mark`` singleton instance.  Example:: |     a ``pytest.mark`` singleton instance.  Example:: | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -88,7 +88,7 @@ def derive_importpath(import_path, raising): | ||||||
|     return attr, target |     return attr, target | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Notset: | class Notset(object): | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         return "<notset>" |         return "<notset>" | ||||||
| 
 | 
 | ||||||
|  | @ -96,7 +96,7 @@ class Notset: | ||||||
| notset = Notset() | notset = Notset() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class MonkeyPatch: | class MonkeyPatch(object): | ||||||
|     """ Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes. |     """ Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes. | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										363
									
								
								_pytest/nodes.py
								
								
								
								
							
							
						
						
									
										363
									
								
								_pytest/nodes.py
								
								
								
								
							|  | @ -1,5 +1,18 @@ | ||||||
|  | from __future__ import absolute_import, division, print_function | ||||||
|  | from collections import MutableMapping as MappingMixin | ||||||
|  | import os | ||||||
|  | 
 | ||||||
|  | import six | ||||||
|  | import py | ||||||
|  | import attr | ||||||
|  | 
 | ||||||
|  | import _pytest | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| SEP = "/" | SEP = "/" | ||||||
| 
 | 
 | ||||||
|  | tracebackcutdir = py.path.local(_pytest.__file__).dirpath() | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| def _splitnode(nodeid): | def _splitnode(nodeid): | ||||||
|     """Split a nodeid into constituent 'parts'. |     """Split a nodeid into constituent 'parts'. | ||||||
|  | @ -35,3 +48,353 @@ def ischildnode(baseid, nodeid): | ||||||
|     if len(node_parts) < len(base_parts): |     if len(node_parts) < len(base_parts): | ||||||
|         return False |         return False | ||||||
|     return node_parts[:len(base_parts)] == base_parts |     return node_parts[:len(base_parts)] == base_parts | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @attr.s | ||||||
|  | class _CompatProperty(object): | ||||||
|  |     name = attr.ib() | ||||||
|  | 
 | ||||||
|  |     def __get__(self, obj, owner): | ||||||
|  |         if obj is None: | ||||||
|  |             return self | ||||||
|  | 
 | ||||||
|  |         # TODO: reenable in the features branch | ||||||
|  |         # warnings.warn( | ||||||
|  |         #     "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format( | ||||||
|  |         #         name=self.name, owner=type(owner).__name__), | ||||||
|  |         #     PendingDeprecationWarning, stacklevel=2) | ||||||
|  |         return getattr(__import__('pytest'), self.name) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class NodeKeywords(MappingMixin): | ||||||
|  |     def __init__(self, node): | ||||||
|  |         self.node = node | ||||||
|  |         self.parent = node.parent | ||||||
|  |         self._markers = {node.name: True} | ||||||
|  | 
 | ||||||
|  |     def __getitem__(self, key): | ||||||
|  |         try: | ||||||
|  |             return self._markers[key] | ||||||
|  |         except KeyError: | ||||||
|  |             if self.parent is None: | ||||||
|  |                 raise | ||||||
|  |             return self.parent.keywords[key] | ||||||
|  | 
 | ||||||
|  |     def __setitem__(self, key, value): | ||||||
|  |         self._markers[key] = value | ||||||
|  | 
 | ||||||
|  |     def __delitem__(self, key): | ||||||
|  |         raise ValueError("cannot delete key in keywords dict") | ||||||
|  | 
 | ||||||
|  |     def __iter__(self): | ||||||
|  |         seen = set(self._markers) | ||||||
|  |         if self.parent is not None: | ||||||
|  |             seen.update(self.parent.keywords) | ||||||
|  |         return iter(seen) | ||||||
|  | 
 | ||||||
|  |     def __len__(self): | ||||||
|  |         return len(self.__iter__()) | ||||||
|  | 
 | ||||||
|  |     def keys(self): | ||||||
|  |         return list(self) | ||||||
|  | 
 | ||||||
|  |     def __repr__(self): | ||||||
|  |         return "<NodeKeywords for node %s>" % (self.node, ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Node(object): | ||||||
|  |     """ base class for Collector and Item the test collection tree. | ||||||
|  |     Collector subclasses have children, Items are terminal nodes.""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, name, parent=None, config=None, session=None): | ||||||
|  |         #: a unique name within the scope of the parent node | ||||||
|  |         self.name = name | ||||||
|  | 
 | ||||||
|  |         #: the parent collector node. | ||||||
|  |         self.parent = parent | ||||||
|  | 
 | ||||||
|  |         #: the pytest config object | ||||||
|  |         self.config = config or parent.config | ||||||
|  | 
 | ||||||
|  |         #: the session this node is part of | ||||||
|  |         self.session = session or parent.session | ||||||
|  | 
 | ||||||
|  |         #: filesystem path where this node was collected from (can be None) | ||||||
|  |         self.fspath = getattr(parent, 'fspath', None) | ||||||
|  | 
 | ||||||
|  |         #: keywords/markers collected from all scopes | ||||||
|  |         self.keywords = NodeKeywords(self) | ||||||
|  | 
 | ||||||
|  |         #: allow adding of extra keywords to use for matching | ||||||
|  |         self.extra_keyword_matches = set() | ||||||
|  | 
 | ||||||
|  |         # used for storing artificial fixturedefs for direct parametrization | ||||||
|  |         self._name2pseudofixturedef = {} | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def ihook(self): | ||||||
|  |         """ fspath sensitive hook proxy used to call pytest hooks""" | ||||||
|  |         return self.session.gethookproxy(self.fspath) | ||||||
|  | 
 | ||||||
|  |     Module = _CompatProperty("Module") | ||||||
|  |     Class = _CompatProperty("Class") | ||||||
|  |     Instance = _CompatProperty("Instance") | ||||||
|  |     Function = _CompatProperty("Function") | ||||||
|  |     File = _CompatProperty("File") | ||||||
|  |     Item = _CompatProperty("Item") | ||||||
|  | 
 | ||||||
|  |     def _getcustomclass(self, name): | ||||||
|  |         maybe_compatprop = getattr(type(self), name) | ||||||
|  |         if isinstance(maybe_compatprop, _CompatProperty): | ||||||
|  |             return getattr(__import__('pytest'), name) | ||||||
|  |         else: | ||||||
|  |             cls = getattr(self, name) | ||||||
|  |             # TODO: reenable in the features branch | ||||||
|  |             # warnings.warn("use of node.%s is deprecated, " | ||||||
|  |             #    "use pytest_pycollect_makeitem(...) to create custom " | ||||||
|  |             #    "collection nodes" % name, category=DeprecationWarning) | ||||||
|  |         return cls | ||||||
|  | 
 | ||||||
|  |     def __repr__(self): | ||||||
|  |         return "<%s %r>" % (self.__class__.__name__, | ||||||
|  |                             getattr(self, 'name', None)) | ||||||
|  | 
 | ||||||
|  |     def warn(self, code, message): | ||||||
|  |         """ generate a warning with the given code and message for this | ||||||
|  |         item. """ | ||||||
|  |         assert isinstance(code, str) | ||||||
|  |         fslocation = getattr(self, "location", None) | ||||||
|  |         if fslocation is None: | ||||||
|  |             fslocation = getattr(self, "fspath", None) | ||||||
|  |         self.ihook.pytest_logwarning.call_historic(kwargs=dict( | ||||||
|  |             code=code, message=message, | ||||||
|  |             nodeid=self.nodeid, fslocation=fslocation)) | ||||||
|  | 
 | ||||||
|  |     # methods for ordering nodes | ||||||
|  |     @property | ||||||
|  |     def nodeid(self): | ||||||
|  |         """ a ::-separated string denoting its collection tree address. """ | ||||||
|  |         try: | ||||||
|  |             return self._nodeid | ||||||
|  |         except AttributeError: | ||||||
|  |             self._nodeid = x = self._makeid() | ||||||
|  |             return x | ||||||
|  | 
 | ||||||
|  |     def _makeid(self): | ||||||
|  |         return self.parent.nodeid + "::" + self.name | ||||||
|  | 
 | ||||||
|  |     def __hash__(self): | ||||||
|  |         return hash(self.nodeid) | ||||||
|  | 
 | ||||||
|  |     def setup(self): | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     def teardown(self): | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     def listchain(self): | ||||||
|  |         """ return list of all parent collectors up to self, | ||||||
|  |             starting from root of collection tree. """ | ||||||
|  |         chain = [] | ||||||
|  |         item = self | ||||||
|  |         while item is not None: | ||||||
|  |             chain.append(item) | ||||||
|  |             item = item.parent | ||||||
|  |         chain.reverse() | ||||||
|  |         return chain | ||||||
|  | 
 | ||||||
|  |     def add_marker(self, marker): | ||||||
|  |         """ dynamically add a marker object to the node. | ||||||
|  | 
 | ||||||
|  |         ``marker`` can be a string or pytest.mark.* instance. | ||||||
|  |         """ | ||||||
|  |         from _pytest.mark import MarkDecorator, MARK_GEN | ||||||
|  |         if isinstance(marker, six.string_types): | ||||||
|  |             marker = getattr(MARK_GEN, marker) | ||||||
|  |         elif not isinstance(marker, MarkDecorator): | ||||||
|  |             raise ValueError("is not a string or pytest.mark.* Marker") | ||||||
|  |         self.keywords[marker.name] = marker | ||||||
|  | 
 | ||||||
|  |     def get_marker(self, name): | ||||||
|  |         """ get a marker object from this node or None if | ||||||
|  |         the node doesn't have a marker with that name. """ | ||||||
|  |         val = self.keywords.get(name, None) | ||||||
|  |         if val is not None: | ||||||
|  |             from _pytest.mark import MarkInfo, MarkDecorator | ||||||
|  |             if isinstance(val, (MarkDecorator, MarkInfo)): | ||||||
|  |                 return val | ||||||
|  | 
 | ||||||
|  |     def listextrakeywords(self): | ||||||
|  |         """ Return a set of all extra keywords in self and any parents.""" | ||||||
|  |         extra_keywords = set() | ||||||
|  |         item = self | ||||||
|  |         for item in self.listchain(): | ||||||
|  |             extra_keywords.update(item.extra_keyword_matches) | ||||||
|  |         return extra_keywords | ||||||
|  | 
 | ||||||
|  |     def listnames(self): | ||||||
|  |         return [x.name for x in self.listchain()] | ||||||
|  | 
 | ||||||
|  |     def addfinalizer(self, fin): | ||||||
|  |         """ register a function to be called when this node is finalized. | ||||||
|  | 
 | ||||||
|  |         This method can only be called when this node is active | ||||||
|  |         in a setup chain, for example during self.setup(). | ||||||
|  |         """ | ||||||
|  |         self.session._setupstate.addfinalizer(fin, self) | ||||||
|  | 
 | ||||||
|  |     def getparent(self, cls): | ||||||
|  |         """ get the next parent node (including ourself) | ||||||
|  |         which is an instance of the given class""" | ||||||
|  |         current = self | ||||||
|  |         while current and not isinstance(current, cls): | ||||||
|  |             current = current.parent | ||||||
|  |         return current | ||||||
|  | 
 | ||||||
|  |     def _prunetraceback(self, excinfo): | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     def _repr_failure_py(self, excinfo, style=None): | ||||||
|  |         fm = self.session._fixturemanager | ||||||
|  |         if excinfo.errisinstance(fm.FixtureLookupError): | ||||||
|  |             return excinfo.value.formatrepr() | ||||||
|  |         tbfilter = True | ||||||
|  |         if self.config.option.fulltrace: | ||||||
|  |             style = "long" | ||||||
|  |         else: | ||||||
|  |             tb = _pytest._code.Traceback([excinfo.traceback[-1]]) | ||||||
|  |             self._prunetraceback(excinfo) | ||||||
|  |             if len(excinfo.traceback) == 0: | ||||||
|  |                 excinfo.traceback = tb | ||||||
|  |             tbfilter = False  # prunetraceback already does it | ||||||
|  |             if style == "auto": | ||||||
|  |                 style = "long" | ||||||
|  |         # XXX should excinfo.getrepr record all data and toterminal() process it? | ||||||
|  |         if style is None: | ||||||
|  |             if self.config.option.tbstyle == "short": | ||||||
|  |                 style = "short" | ||||||
|  |             else: | ||||||
|  |                 style = "long" | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             os.getcwd() | ||||||
|  |             abspath = False | ||||||
|  |         except OSError: | ||||||
|  |             abspath = True | ||||||
|  | 
 | ||||||
|  |         return excinfo.getrepr(funcargs=True, abspath=abspath, | ||||||
|  |                                showlocals=self.config.option.showlocals, | ||||||
|  |                                style=style, tbfilter=tbfilter) | ||||||
|  | 
 | ||||||
|  |     repr_failure = _repr_failure_py | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Collector(Node): | ||||||
|  |     """ Collector instances create children through collect() | ||||||
|  |         and thus iteratively build a tree. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     class CollectError(Exception): | ||||||
|  |         """ an error during collection, contains a custom message. """ | ||||||
|  | 
 | ||||||
|  |     def collect(self): | ||||||
|  |         """ returns a list of children (items and collectors) | ||||||
|  |             for this collection node. | ||||||
|  |         """ | ||||||
|  |         raise NotImplementedError("abstract") | ||||||
|  | 
 | ||||||
|  |     def repr_failure(self, excinfo): | ||||||
|  |         """ represent a collection failure. """ | ||||||
|  |         if excinfo.errisinstance(self.CollectError): | ||||||
|  |             exc = excinfo.value | ||||||
|  |             return str(exc.args[0]) | ||||||
|  |         return self._repr_failure_py(excinfo, style="short") | ||||||
|  | 
 | ||||||
|  |     def _prunetraceback(self, excinfo): | ||||||
|  |         if hasattr(self, 'fspath'): | ||||||
|  |             traceback = excinfo.traceback | ||||||
|  |             ntraceback = traceback.cut(path=self.fspath) | ||||||
|  |             if ntraceback == traceback: | ||||||
|  |                 ntraceback = ntraceback.cut(excludepath=tracebackcutdir) | ||||||
|  |             excinfo.traceback = ntraceback.filter() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class FSCollector(Collector): | ||||||
|  |     def __init__(self, fspath, parent=None, config=None, session=None): | ||||||
|  |         fspath = py.path.local(fspath)  # xxx only for test_resultlog.py? | ||||||
|  |         name = fspath.basename | ||||||
|  |         if parent is not None: | ||||||
|  |             rel = fspath.relto(parent.fspath) | ||||||
|  |             if rel: | ||||||
|  |                 name = rel | ||||||
|  |             name = name.replace(os.sep, SEP) | ||||||
|  |         super(FSCollector, self).__init__(name, parent, config, session) | ||||||
|  |         self.fspath = fspath | ||||||
|  | 
 | ||||||
|  |     def _check_initialpaths_for_relpath(self): | ||||||
|  |         for initialpath in self.session._initialpaths: | ||||||
|  |             if self.fspath.common(initialpath) == initialpath: | ||||||
|  |                 return self.fspath.relto(initialpath.dirname) | ||||||
|  | 
 | ||||||
|  |     def _makeid(self): | ||||||
|  |         relpath = self.fspath.relto(self.config.rootdir) | ||||||
|  | 
 | ||||||
|  |         if not relpath: | ||||||
|  |             relpath = self._check_initialpaths_for_relpath() | ||||||
|  |         if os.sep != SEP: | ||||||
|  |             relpath = relpath.replace(os.sep, SEP) | ||||||
|  |         return relpath | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class File(FSCollector): | ||||||
|  |     """ base class for collecting tests from a file. """ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Item(Node): | ||||||
|  |     """ a basic test invocation item. Note that for a single function | ||||||
|  |     there might be multiple test invocation items. | ||||||
|  |     """ | ||||||
|  |     nextitem = None | ||||||
|  | 
 | ||||||
|  |     def __init__(self, name, parent=None, config=None, session=None): | ||||||
|  |         super(Item, self).__init__(name, parent, config, session) | ||||||
|  |         self._report_sections = [] | ||||||
|  | 
 | ||||||
|  |     def add_report_section(self, when, key, content): | ||||||
|  |         """ | ||||||
|  |         Adds a new report section, similar to what's done internally to add stdout and | ||||||
|  |         stderr captured output:: | ||||||
|  | 
 | ||||||
|  |             item.add_report_section("call", "stdout", "report section contents") | ||||||
|  | 
 | ||||||
|  |         :param str when: | ||||||
|  |             One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``. | ||||||
|  |         :param str key: | ||||||
|  |             Name of the section, can be customized at will. Pytest uses ``"stdout"`` and | ||||||
|  |             ``"stderr"`` internally. | ||||||
|  | 
 | ||||||
|  |         :param str content: | ||||||
|  |             The full contents as a string. | ||||||
|  |         """ | ||||||
|  |         if content: | ||||||
|  |             self._report_sections.append((when, key, content)) | ||||||
|  | 
 | ||||||
|  |     def reportinfo(self): | ||||||
|  |         return self.fspath, None, "" | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def location(self): | ||||||
|  |         try: | ||||||
|  |             return self._location | ||||||
|  |         except AttributeError: | ||||||
|  |             location = self.reportinfo() | ||||||
|  |             # bestrelpath is a quite slow function | ||||||
|  |             cache = self.config.__dict__.setdefault("_bestrelpathcache", {}) | ||||||
|  |             try: | ||||||
|  |                 fspath = cache[location[0]] | ||||||
|  |             except KeyError: | ||||||
|  |                 fspath = self.session.fspath.bestrelpath(location[0]) | ||||||
|  |                 cache[location[0]] = fspath | ||||||
|  |             location = (fspath, location[1], str(location[2])) | ||||||
|  |             self._location = location | ||||||
|  |             return location | ||||||
|  |  | ||||||
|  | @ -171,7 +171,7 @@ def _pytest(request): | ||||||
|     return PytestArg(request) |     return PytestArg(request) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class PytestArg: | class PytestArg(object): | ||||||
|     def __init__(self, request): |     def __init__(self, request): | ||||||
|         self.request = request |         self.request = request | ||||||
| 
 | 
 | ||||||
|  | @ -186,7 +186,7 @@ def get_public_names(values): | ||||||
|     return [x for x in values if x[0] != "_"] |     return [x for x in values if x[0] != "_"] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ParsedCall: | class ParsedCall(object): | ||||||
|     def __init__(self, name, kwargs): |     def __init__(self, name, kwargs): | ||||||
|         self.__dict__.update(kwargs) |         self.__dict__.update(kwargs) | ||||||
|         self._name = name |         self._name = name | ||||||
|  | @ -197,7 +197,7 @@ class ParsedCall: | ||||||
|         return "<ParsedCall %r(**%r)>" % (self._name, d) |         return "<ParsedCall %r(**%r)>" % (self._name, d) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class HookRecorder: | class HookRecorder(object): | ||||||
|     """Record all hooks called in a plugin manager. |     """Record all hooks called in a plugin manager. | ||||||
| 
 | 
 | ||||||
|     This wraps all the hook calls in the plugin manager, recording each call |     This wraps all the hook calls in the plugin manager, recording each call | ||||||
|  | @ -343,7 +343,7 @@ def testdir(request, tmpdir_factory): | ||||||
| rex_outcome = re.compile(r"(\d+) ([\w-]+)") | rex_outcome = re.compile(r"(\d+) ([\w-]+)") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class RunResult: | class RunResult(object): | ||||||
|     """The result of running a command. |     """The result of running a command. | ||||||
| 
 | 
 | ||||||
|     Attributes: |     Attributes: | ||||||
|  | @ -397,7 +397,36 @@ class RunResult: | ||||||
|         assert obtained == dict(passed=passed, skipped=skipped, failed=failed, error=error) |         assert obtained == dict(passed=passed, skipped=skipped, failed=failed, error=error) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Testdir: | class CwdSnapshot(object): | ||||||
|  |     def __init__(self): | ||||||
|  |         self.__saved = os.getcwd() | ||||||
|  | 
 | ||||||
|  |     def restore(self): | ||||||
|  |         os.chdir(self.__saved) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class SysModulesSnapshot(object): | ||||||
|  |     def __init__(self, preserve=None): | ||||||
|  |         self.__preserve = preserve | ||||||
|  |         self.__saved = dict(sys.modules) | ||||||
|  | 
 | ||||||
|  |     def restore(self): | ||||||
|  |         if self.__preserve: | ||||||
|  |             self.__saved.update( | ||||||
|  |                 (k, m) for k, m in sys.modules.items() if self.__preserve(k)) | ||||||
|  |         sys.modules.clear() | ||||||
|  |         sys.modules.update(self.__saved) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class SysPathsSnapshot(object): | ||||||
|  |     def __init__(self): | ||||||
|  |         self.__saved = list(sys.path), list(sys.meta_path) | ||||||
|  | 
 | ||||||
|  |     def restore(self): | ||||||
|  |         sys.path[:], sys.meta_path[:] = self.__saved | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Testdir(object): | ||||||
|     """Temporary test directory with tools to test/run pytest itself. |     """Temporary test directory with tools to test/run pytest itself. | ||||||
| 
 | 
 | ||||||
|     This is based on the ``tmpdir`` fixture but provides a number of methods |     This is based on the ``tmpdir`` fixture but provides a number of methods | ||||||
|  | @ -421,9 +450,10 @@ class Testdir: | ||||||
|         name = request.function.__name__ |         name = request.function.__name__ | ||||||
|         self.tmpdir = tmpdir_factory.mktemp(name, numbered=True) |         self.tmpdir = tmpdir_factory.mktemp(name, numbered=True) | ||||||
|         self.plugins = [] |         self.plugins = [] | ||||||
|         self._savesyspath = (list(sys.path), list(sys.meta_path)) |         self._cwd_snapshot = CwdSnapshot() | ||||||
|         self._savemodulekeys = set(sys.modules) |         self._sys_path_snapshot = SysPathsSnapshot() | ||||||
|         self.chdir()  # always chdir |         self._sys_modules_snapshot = self.__take_sys_modules_snapshot() | ||||||
|  |         self.chdir() | ||||||
|         self.request.addfinalizer(self.finalize) |         self.request.addfinalizer(self.finalize) | ||||||
|         method = self.request.config.getoption("--runpytest") |         method = self.request.config.getoption("--runpytest") | ||||||
|         if method == "inprocess": |         if method == "inprocess": | ||||||
|  | @ -442,23 +472,17 @@ class Testdir: | ||||||
|         it can be looked at after the test run has finished. |         it can be looked at after the test run has finished. | ||||||
| 
 | 
 | ||||||
|         """ |         """ | ||||||
|         sys.path[:], sys.meta_path[:] = self._savesyspath |         self._sys_modules_snapshot.restore() | ||||||
|         if hasattr(self, '_olddir'): |         self._sys_path_snapshot.restore() | ||||||
|             self._olddir.chdir() |         self._cwd_snapshot.restore() | ||||||
|         self.delete_loaded_modules() |  | ||||||
| 
 | 
 | ||||||
|     def delete_loaded_modules(self): |     def __take_sys_modules_snapshot(self): | ||||||
|         """Delete modules that have been loaded during a test. |         # some zope modules used by twisted-related tests keep internal state | ||||||
| 
 |         # and can't be deleted; we had some trouble in the past with | ||||||
|         This allows the interpreter to catch module changes in case |         # `zope.interface` for example | ||||||
|         the module is re-imported. |         def preserve_module(name): | ||||||
|         """ |             return name.startswith("zope") | ||||||
|         for name in set(sys.modules).difference(self._savemodulekeys): |         return SysModulesSnapshot(preserve=preserve_module) | ||||||
|             # some zope modules used by twisted-related tests keeps internal |  | ||||||
|             # state and can't be deleted; we had some trouble in the past |  | ||||||
|             # with zope.interface for example |  | ||||||
|             if not name.startswith("zope"): |  | ||||||
|                 del sys.modules[name] |  | ||||||
| 
 | 
 | ||||||
|     def make_hook_recorder(self, pluginmanager): |     def make_hook_recorder(self, pluginmanager): | ||||||
|         """Create a new :py:class:`HookRecorder` for a PluginManager.""" |         """Create a new :py:class:`HookRecorder` for a PluginManager.""" | ||||||
|  | @ -473,9 +497,7 @@ class Testdir: | ||||||
|         This is done automatically upon instantiation. |         This is done automatically upon instantiation. | ||||||
| 
 | 
 | ||||||
|         """ |         """ | ||||||
|         old = self.tmpdir.chdir() |         self.tmpdir.chdir() | ||||||
|         if not hasattr(self, '_olddir'): |  | ||||||
|             self._olddir = old |  | ||||||
| 
 | 
 | ||||||
|     def _makefile(self, ext, args, kwargs, encoding='utf-8'): |     def _makefile(self, ext, args, kwargs, encoding='utf-8'): | ||||||
|         items = list(kwargs.items()) |         items = list(kwargs.items()) | ||||||
|  | @ -690,42 +712,58 @@ class Testdir: | ||||||
|         :return: a :py:class:`HookRecorder` instance |         :return: a :py:class:`HookRecorder` instance | ||||||
| 
 | 
 | ||||||
|         """ |         """ | ||||||
|         # When running py.test inline any plugins active in the main test |         finalizers = [] | ||||||
|         # process are already imported.  So this disables the warning which |         try: | ||||||
|         # will trigger to say they can no longer be rewritten, which is fine as |             # When running py.test inline any plugins active in the main test | ||||||
|         # they have already been rewritten. |             # process are already imported.  So this disables the warning which | ||||||
|         orig_warn = AssertionRewritingHook._warn_already_imported |             # will trigger to say they can no longer be rewritten, which is | ||||||
|  |             # fine as they have already been rewritten. | ||||||
|  |             orig_warn = AssertionRewritingHook._warn_already_imported | ||||||
| 
 | 
 | ||||||
|         def revert(): |             def revert_warn_already_imported(): | ||||||
|             AssertionRewritingHook._warn_already_imported = orig_warn |                 AssertionRewritingHook._warn_already_imported = orig_warn | ||||||
|  |             finalizers.append(revert_warn_already_imported) | ||||||
|  |             AssertionRewritingHook._warn_already_imported = lambda *a: None | ||||||
| 
 | 
 | ||||||
|         self.request.addfinalizer(revert) |             # Any sys.module or sys.path changes done while running py.test | ||||||
|         AssertionRewritingHook._warn_already_imported = lambda *a: None |             # inline should be reverted after the test run completes to avoid | ||||||
|  |             # clashing with later inline tests run within the same pytest test, | ||||||
|  |             # e.g. just because they use matching test module names. | ||||||
|  |             finalizers.append(self.__take_sys_modules_snapshot().restore) | ||||||
|  |             finalizers.append(SysPathsSnapshot().restore) | ||||||
| 
 | 
 | ||||||
|         rec = [] |             # Important note: | ||||||
|  |             # - our tests should not leave any other references/registrations | ||||||
|  |             #   laying around other than possibly loaded test modules | ||||||
|  |             #   referenced from sys.modules, as nothing will clean those up | ||||||
|  |             #   automatically | ||||||
| 
 | 
 | ||||||
|         class Collect: |             rec = [] | ||||||
|             def pytest_configure(x, config): |  | ||||||
|                 rec.append(self.make_hook_recorder(config.pluginmanager)) |  | ||||||
| 
 | 
 | ||||||
|         plugins = kwargs.get("plugins") or [] |             class Collect(object): | ||||||
|         plugins.append(Collect()) |                 def pytest_configure(x, config): | ||||||
|         ret = pytest.main(list(args), plugins=plugins) |                     rec.append(self.make_hook_recorder(config.pluginmanager)) | ||||||
|         self.delete_loaded_modules() |  | ||||||
|         if len(rec) == 1: |  | ||||||
|             reprec = rec.pop() |  | ||||||
|         else: |  | ||||||
|             class reprec: |  | ||||||
|                 pass |  | ||||||
|         reprec.ret = ret |  | ||||||
| 
 | 
 | ||||||
|         # typically we reraise keyboard interrupts from the child run because |             plugins = kwargs.get("plugins") or [] | ||||||
|         # it's our user requesting interruption of the testing |             plugins.append(Collect()) | ||||||
|         if ret == 2 and not kwargs.get("no_reraise_ctrlc"): |             ret = pytest.main(list(args), plugins=plugins) | ||||||
|             calls = reprec.getcalls("pytest_keyboard_interrupt") |             if len(rec) == 1: | ||||||
|             if calls and calls[-1].excinfo.type == KeyboardInterrupt: |                 reprec = rec.pop() | ||||||
|                 raise KeyboardInterrupt() |             else: | ||||||
|         return reprec |                 class reprec(object): | ||||||
|  |                     pass | ||||||
|  |             reprec.ret = ret | ||||||
|  | 
 | ||||||
|  |             # typically we reraise keyboard interrupts from the child run | ||||||
|  |             # because it's our user requesting interruption of the testing | ||||||
|  |             if ret == 2 and not kwargs.get("no_reraise_ctrlc"): | ||||||
|  |                 calls = reprec.getcalls("pytest_keyboard_interrupt") | ||||||
|  |                 if calls and calls[-1].excinfo.type == KeyboardInterrupt: | ||||||
|  |                     raise KeyboardInterrupt() | ||||||
|  |             return reprec | ||||||
|  |         finally: | ||||||
|  |             for finalizer in finalizers: | ||||||
|  |                 finalizer() | ||||||
| 
 | 
 | ||||||
|     def runpytest_inprocess(self, *args, **kwargs): |     def runpytest_inprocess(self, *args, **kwargs): | ||||||
|         """Return result of running pytest in-process, providing a similar |         """Return result of running pytest in-process, providing a similar | ||||||
|  | @ -742,13 +780,13 @@ class Testdir: | ||||||
|                 reprec = self.inline_run(*args, **kwargs) |                 reprec = self.inline_run(*args, **kwargs) | ||||||
|             except SystemExit as e: |             except SystemExit as e: | ||||||
| 
 | 
 | ||||||
|                 class reprec: |                 class reprec(object): | ||||||
|                     ret = e.args[0] |                     ret = e.args[0] | ||||||
| 
 | 
 | ||||||
|             except Exception: |             except Exception: | ||||||
|                 traceback.print_exc() |                 traceback.print_exc() | ||||||
| 
 | 
 | ||||||
|                 class reprec: |                 class reprec(object): | ||||||
|                     ret = 3 |                     ret = 3 | ||||||
|         finally: |         finally: | ||||||
|             out, err = capture.readouterr() |             out, err = capture.readouterr() | ||||||
|  | @ -1029,7 +1067,7 @@ def getdecoded(out): | ||||||
|             py.io.saferepr(out),) |             py.io.saferepr(out),) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class LineComp: | class LineComp(object): | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         self.stringio = py.io.TextIO() |         self.stringio = py.io.TextIO() | ||||||
| 
 | 
 | ||||||
|  | @ -1047,7 +1085,7 @@ class LineComp: | ||||||
|         return LineMatcher(lines1).fnmatch_lines(lines2) |         return LineMatcher(lines1).fnmatch_lines(lines2) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class LineMatcher: | class LineMatcher(object): | ||||||
|     """Flexible matching of text. |     """Flexible matching of text. | ||||||
| 
 | 
 | ||||||
|     This is a convenience class to test large texts like the output of |     This is a convenience class to test large texts like the output of | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ from _pytest.config import hookimpl | ||||||
| import _pytest | import _pytest | ||||||
| import pluggy | import pluggy | ||||||
| from _pytest import fixtures | from _pytest import fixtures | ||||||
| from _pytest import main | from _pytest import nodes | ||||||
| from _pytest import deprecated | from _pytest import deprecated | ||||||
| from _pytest.compat import ( | from _pytest.compat import ( | ||||||
|     isclass, isfunction, is_generator, ascii_escaped, |     isclass, isfunction, is_generator, ascii_escaped, | ||||||
|  | @ -269,7 +269,7 @@ class PyobjMixin(PyobjContext): | ||||||
|         return fspath, lineno, modpath |         return fspath, lineno, modpath | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class PyCollector(PyobjMixin, main.Collector): | class PyCollector(PyobjMixin, nodes.Collector): | ||||||
| 
 | 
 | ||||||
|     def funcnamefilter(self, name): |     def funcnamefilter(self, name): | ||||||
|         return self._matches_prefix_or_glob_option('python_functions', name) |         return self._matches_prefix_or_glob_option('python_functions', name) | ||||||
|  | @ -394,7 +394,7 @@ class PyCollector(PyobjMixin, main.Collector): | ||||||
|                                ) |                                ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Module(main.File, PyCollector): | class Module(nodes.File, PyCollector): | ||||||
|     """ Collector for test classes and functions. """ |     """ Collector for test classes and functions. """ | ||||||
| 
 | 
 | ||||||
|     def _getobj(self): |     def _getobj(self): | ||||||
|  | @ -785,6 +785,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): | ||||||
|         from _pytest.fixtures import scope2index |         from _pytest.fixtures import scope2index | ||||||
|         from _pytest.mark import ParameterSet |         from _pytest.mark import ParameterSet | ||||||
|         from py.io import saferepr |         from py.io import saferepr | ||||||
|  | 
 | ||||||
|         argnames, parameters = ParameterSet._for_parametrize( |         argnames, parameters = ParameterSet._for_parametrize( | ||||||
|             argnames, argvalues, self.function, self.config) |             argnames, argvalues, self.function, self.config) | ||||||
|         del argvalues |         del argvalues | ||||||
|  | @ -940,7 +941,7 @@ def _idval(val, argname, idx, idfn, config=None): | ||||||
|         return ascii_escaped(val.pattern) |         return ascii_escaped(val.pattern) | ||||||
|     elif enum is not None and isinstance(val, enum.Enum): |     elif enum is not None and isinstance(val, enum.Enum): | ||||||
|         return str(val) |         return str(val) | ||||||
|     elif isclass(val) and hasattr(val, '__name__'): |     elif (isclass(val) or isfunction(val)) and hasattr(val, '__name__'): | ||||||
|         return val.__name__ |         return val.__name__ | ||||||
|     return str(argname) + str(idx) |     return str(argname) + str(idx) | ||||||
| 
 | 
 | ||||||
|  | @ -1097,7 +1098,7 @@ def write_docstring(tw, doc): | ||||||
|             tw.write(INDENT + line + "\n") |             tw.write(INDENT + line + "\n") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Function(FunctionMixin, main.Item, fixtures.FuncargnamesCompatAttr): | class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): | ||||||
|     """ a Function Item is responsible for setting up and executing a |     """ a Function Item is responsible for setting up and executing a | ||||||
|     Python test function. |     Python test function. | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|  | @ -60,6 +60,9 @@ def pytest_runtest_protocol(item, nextitem): | ||||||
|         nodeid=item.nodeid, location=item.location, |         nodeid=item.nodeid, location=item.location, | ||||||
|     ) |     ) | ||||||
|     runtestprotocol(item, nextitem=nextitem) |     runtestprotocol(item, nextitem=nextitem) | ||||||
|  |     item.ihook.pytest_runtest_logfinish( | ||||||
|  |         nodeid=item.nodeid, location=item.location, | ||||||
|  |     ) | ||||||
|     return True |     return True | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -175,7 +178,7 @@ def call_runtest_hook(item, when, **kwds): | ||||||
|     return CallInfo(lambda: ihook(item=item, **kwds), when=when) |     return CallInfo(lambda: ihook(item=item, **kwds), when=when) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class CallInfo: | class CallInfo(object): | ||||||
|     """ Result/Exception info a function invocation. """ |     """ Result/Exception info a function invocation. """ | ||||||
|     #: None or ExceptionInfo object. |     #: None or ExceptionInfo object. | ||||||
|     excinfo = None |     excinfo = None | ||||||
|  |  | ||||||
|  | @ -94,7 +94,7 @@ def pytest_report_teststatus(report): | ||||||
|     return report.outcome, letter, report.outcome.upper() |     return report.outcome, letter, report.outcome.upper() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class WarningReport: | class WarningReport(object): | ||||||
|     """ |     """ | ||||||
|     Simple structure to hold warnings information captured by ``pytest_logwarning``. |     Simple structure to hold warnings information captured by ``pytest_logwarning``. | ||||||
|     """ |     """ | ||||||
|  | @ -129,7 +129,7 @@ class WarningReport: | ||||||
|         return None |         return None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TerminalReporter: | class TerminalReporter(object): | ||||||
|     def __init__(self, config, file=None): |     def __init__(self, config, file=None): | ||||||
|         import _pytest.config |         import _pytest.config | ||||||
|         self.config = config |         self.config = config | ||||||
|  | @ -152,8 +152,18 @@ class TerminalReporter: | ||||||
|         self.reportchars = getreportopt(config) |         self.reportchars = getreportopt(config) | ||||||
|         self.hasmarkup = self._tw.hasmarkup |         self.hasmarkup = self._tw.hasmarkup | ||||||
|         self.isatty = file.isatty() |         self.isatty = file.isatty() | ||||||
|         self._progress_items_reported = 0 |         self._progress_nodeids_reported = set() | ||||||
|         self._show_progress_info = self.config.getini('console_output_style') == 'progress' |         self._show_progress_info = self._determine_show_progress_info() | ||||||
|  | 
 | ||||||
|  |     def _determine_show_progress_info(self): | ||||||
|  |         """Return True if we should display progress information based on the current config""" | ||||||
|  |         # do not show progress if we are not capturing output (#3038) | ||||||
|  |         if self.config.getoption('capture') == 'no': | ||||||
|  |             return False | ||||||
|  |         # do not show progress if we are showing fixture setup/teardown | ||||||
|  |         if self.config.getoption('setupshow'): | ||||||
|  |             return False | ||||||
|  |         return self.config.getini('console_output_style') == 'progress' | ||||||
| 
 | 
 | ||||||
|     def hasopt(self, char): |     def hasopt(self, char): | ||||||
|         char = {'xfailed': 'x', 'skipped': 's'}.get(char, char) |         char = {'xfailed': 'x', 'skipped': 's'}.get(char, char) | ||||||
|  | @ -178,7 +188,6 @@ class TerminalReporter: | ||||||
|         if extra: |         if extra: | ||||||
|             self._tw.write(extra, **kwargs) |             self._tw.write(extra, **kwargs) | ||||||
|             self.currentfspath = -2 |             self.currentfspath = -2 | ||||||
|             self._write_progress_information_filling_space() |  | ||||||
| 
 | 
 | ||||||
|     def ensure_newline(self): |     def ensure_newline(self): | ||||||
|         if self.currentfspath: |         if self.currentfspath: | ||||||
|  | @ -268,14 +277,13 @@ class TerminalReporter: | ||||||
|             # probably passed setup/teardown |             # probably passed setup/teardown | ||||||
|             return |             return | ||||||
|         running_xdist = hasattr(rep, 'node') |         running_xdist = hasattr(rep, 'node') | ||||||
|         self._progress_items_reported += 1 |  | ||||||
|         if self.verbosity <= 0: |         if self.verbosity <= 0: | ||||||
|             if not running_xdist and self.showfspath: |             if not running_xdist and self.showfspath: | ||||||
|                 self.write_fspath_result(rep.nodeid, letter) |                 self.write_fspath_result(rep.nodeid, letter) | ||||||
|             else: |             else: | ||||||
|                 self._tw.write(letter) |                 self._tw.write(letter) | ||||||
|             self._write_progress_if_past_edge() |  | ||||||
|         else: |         else: | ||||||
|  |             self._progress_nodeids_reported.add(rep.nodeid) | ||||||
|             if markup is None: |             if markup is None: | ||||||
|                 if rep.passed: |                 if rep.passed: | ||||||
|                     markup = {'green': True} |                     markup = {'green': True} | ||||||
|  | @ -288,6 +296,8 @@ class TerminalReporter: | ||||||
|             line = self._locationline(rep.nodeid, *rep.location) |             line = self._locationline(rep.nodeid, *rep.location) | ||||||
|             if not running_xdist: |             if not running_xdist: | ||||||
|                 self.write_ensure_prefix(line, word, **markup) |                 self.write_ensure_prefix(line, word, **markup) | ||||||
|  |                 if self._show_progress_info: | ||||||
|  |                     self._write_progress_information_filling_space() | ||||||
|             else: |             else: | ||||||
|                 self.ensure_newline() |                 self.ensure_newline() | ||||||
|                 self._tw.write("[%s]" % rep.node.gateway.id) |                 self._tw.write("[%s]" % rep.node.gateway.id) | ||||||
|  | @ -299,31 +309,28 @@ class TerminalReporter: | ||||||
|                 self._tw.write(" " + line) |                 self._tw.write(" " + line) | ||||||
|                 self.currentfspath = -2 |                 self.currentfspath = -2 | ||||||
| 
 | 
 | ||||||
|     def _write_progress_if_past_edge(self): |     def pytest_runtest_logfinish(self, nodeid): | ||||||
|         if not self._show_progress_info: |         if self.verbosity <= 0 and self._show_progress_info: | ||||||
|             return |             self._progress_nodeids_reported.add(nodeid) | ||||||
|         last_item = self._progress_items_reported == self._session.testscollected |             last_item = len(self._progress_nodeids_reported) == self._session.testscollected | ||||||
|         if last_item: |             if last_item: | ||||||
|             self._write_progress_information_filling_space() |                 self._write_progress_information_filling_space() | ||||||
|             return |             else: | ||||||
| 
 |                 past_edge = self._tw.chars_on_current_line + self._PROGRESS_LENGTH + 1 >= self._screen_width | ||||||
|         past_edge = self._tw.chars_on_current_line + self._PROGRESS_LENGTH + 1 >= self._screen_width |                 if past_edge: | ||||||
|         if past_edge: |                     msg = self._get_progress_information_message() | ||||||
|             msg = self._get_progress_information_message() |                     self._tw.write(msg + '\n', cyan=True) | ||||||
|             self._tw.write(msg + '\n', cyan=True) |  | ||||||
| 
 | 
 | ||||||
|     _PROGRESS_LENGTH = len(' [100%]') |     _PROGRESS_LENGTH = len(' [100%]') | ||||||
| 
 | 
 | ||||||
|     def _get_progress_information_message(self): |     def _get_progress_information_message(self): | ||||||
|         collected = self._session.testscollected |         collected = self._session.testscollected | ||||||
|         if collected: |         if collected: | ||||||
|             progress = self._progress_items_reported * 100 // collected |             progress = len(self._progress_nodeids_reported) * 100 // collected | ||||||
|             return ' [{:3d}%]'.format(progress) |             return ' [{:3d}%]'.format(progress) | ||||||
|         return ' [100%]' |         return ' [100%]' | ||||||
| 
 | 
 | ||||||
|     def _write_progress_information_filling_space(self): |     def _write_progress_information_filling_space(self): | ||||||
|         if not self._show_progress_info: |  | ||||||
|             return |  | ||||||
|         msg = self._get_progress_information_message() |         msg = self._get_progress_information_message() | ||||||
|         fill = ' ' * (self._tw.fullwidth - self._tw.chars_on_current_line - len(msg) - 1) |         fill = ' ' * (self._tw.fullwidth - self._tw.chars_on_current_line - len(msg) - 1) | ||||||
|         self.write(fill + msg, cyan=True) |         self.write(fill + msg, cyan=True) | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ import py | ||||||
| from _pytest.monkeypatch import MonkeyPatch | from _pytest.monkeypatch import MonkeyPatch | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TempdirFactory: | class TempdirFactory(object): | ||||||
|     """Factory for temporary directories under the common base temp directory. |     """Factory for temporary directories under the common base temp directory. | ||||||
| 
 | 
 | ||||||
|     The base directory can be configured using the ``--basetemp`` option. |     The base directory can be configured using the ``--basetemp`` option. | ||||||
|  |  | ||||||
|  | @ -1 +0,0 @@ | ||||||
| Fixed hanging pexpect test on MacOS by using flush() instead of wait(). |  | ||||||
|  | @ -1 +0,0 @@ | ||||||
| Document hooks (defined with ``historic=True``) which cannot be used with ``hookwrapper=True``. |  | ||||||
|  | @ -1 +0,0 @@ | ||||||
| Clarify that warning capturing doesn't change the warning filter by default. |  | ||||||
|  | @ -1 +0,0 @@ | ||||||
| Clarify a possible confusion when using pytest_fixture_setup with fixture functions that return None. |  | ||||||
|  | @ -1 +0,0 @@ | ||||||
| Replace py.std with stdlib imports. |  | ||||||
|  | @ -1 +0,0 @@ | ||||||
| Fix skipping plugin reporting hook when test aborted before plugin setup hook. |  | ||||||
|  | @ -1 +0,0 @@ | ||||||
| Fix the wording of a sentence on doctest flags use in pytest. |  | ||||||
|  | @ -1 +0,0 @@ | ||||||
| Prefer ``https://*.readthedocs.io`` over ``http://*.rtfd.org`` for links in the documentation. |  | ||||||
|  | @ -1 +0,0 @@ | ||||||
| Corrected 'you' to 'your' in logging docs. |  | ||||||
|  | @ -1 +0,0 @@ | ||||||
| Improve readability (wording, grammar) of Getting Started guide |  | ||||||
|  | @ -1 +0,0 @@ | ||||||
| Added note that calling pytest.main multiple times from the same process is not recommended because of import caching. |  | ||||||
|  | @ -6,6 +6,7 @@ Release announcements | ||||||
|    :maxdepth: 2 |    :maxdepth: 2 | ||||||
| 
 | 
 | ||||||
|     |     | ||||||
|  |    release-3.4.0 | ||||||
|    release-3.3.2 |    release-3.3.2 | ||||||
|    release-3.3.1 |    release-3.3.1 | ||||||
|    release-3.3.0 |    release-3.3.0 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,52 @@ | ||||||
|  | pytest-3.4.0 | ||||||
|  | ======================================= | ||||||
|  | 
 | ||||||
|  | The pytest team is proud to announce the 3.4.0 release! | ||||||
|  | 
 | ||||||
|  | pytest is a mature Python testing tool with more than a 1600 tests | ||||||
|  | against itself, passing on many different interpreters and platforms. | ||||||
|  | 
 | ||||||
|  | This release contains a number of bugs fixes and improvements, so users are encouraged | ||||||
|  | to take a look at the CHANGELOG: | ||||||
|  | 
 | ||||||
|  |     http://doc.pytest.org/en/latest/changelog.html | ||||||
|  | 
 | ||||||
|  | For complete documentation, please visit: | ||||||
|  | 
 | ||||||
|  |     http://docs.pytest.org | ||||||
|  | 
 | ||||||
|  | As usual, you can upgrade from pypi via: | ||||||
|  | 
 | ||||||
|  |     pip install -U pytest | ||||||
|  | 
 | ||||||
|  | Thanks to all who contributed to this release, among them: | ||||||
|  | 
 | ||||||
|  | * Aaron | ||||||
|  | * Alan Velasco | ||||||
|  | * Anders Hovmöller | ||||||
|  | * Andrew Toolan | ||||||
|  | * Anthony Sottile | ||||||
|  | * Aron Coyle | ||||||
|  | * Brian Maissy | ||||||
|  | * Bruno Oliveira | ||||||
|  | * Cyrus Maden | ||||||
|  | * Florian Bruhin | ||||||
|  | * Henk-Jaap Wagenaar | ||||||
|  | * Ian Lesperance | ||||||
|  | * Jon Dufresne | ||||||
|  | * Jurko Gospodnetić | ||||||
|  | * Kate | ||||||
|  | * Kimberly | ||||||
|  | * Per A. Brodtkorb | ||||||
|  | * Pierre-Alexandre Fonta | ||||||
|  | * Raphael Castaneda | ||||||
|  | * Ronny Pfannschmidt | ||||||
|  | * ST John | ||||||
|  | * Segev Finer | ||||||
|  | * Thomas Hisch | ||||||
|  | * Tzu-ping Chung | ||||||
|  | * feuillemorte | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Happy testing, | ||||||
|  | The Pytest Development Team | ||||||
|  | @ -116,6 +116,10 @@ You can ask for available builtin or project-custom | ||||||
|         Add extra xml properties to the tag for the calling test. |         Add extra xml properties to the tag for the calling test. | ||||||
|         The fixture is callable with ``(name, value)``, with value being automatically |         The fixture is callable with ``(name, value)``, with value being automatically | ||||||
|         xml-encoded. |         xml-encoded. | ||||||
|  |     record_xml_attribute | ||||||
|  |         Add extra xml attributes to the tag for the calling test. | ||||||
|  |         The fixture is callable with ``(name, value)``, with value being automatically | ||||||
|  |         xml-encoded | ||||||
|     caplog |     caplog | ||||||
|         Access and control log capturing. |         Access and control log capturing. | ||||||
|          |          | ||||||
|  |  | ||||||
|  | @ -225,7 +225,7 @@ You can always peek at the content of the cache using the | ||||||
|     =========================== test session starts ============================ |     =========================== test session starts ============================ | ||||||
|     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y |     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y | ||||||
|     rootdir: $REGENDOC_TMPDIR, inifile: |     rootdir: $REGENDOC_TMPDIR, inifile: | ||||||
|     cachedir: $REGENDOC_TMPDIR/.cache |     cachedir: $REGENDOC_TMPDIR/.pytest_cache | ||||||
|     ------------------------------- cache values ------------------------------- |     ------------------------------- cache values ------------------------------- | ||||||
|     cache/lastfailed contains: |     cache/lastfailed contains: | ||||||
|       {'test_caching.py::test_function': True} |       {'test_caching.py::test_function': True} | ||||||
|  |  | ||||||
|  | @ -152,11 +152,25 @@ above will show verbose output because ``-v`` overwrites ``-q``. | ||||||
| Builtin configuration file options | Builtin configuration file options | ||||||
| ---------------------------------------------- | ---------------------------------------------- | ||||||
| 
 | 
 | ||||||
|  | Here is a list of builtin configuration options that may be written in a ``pytest.ini``, ``tox.ini`` or ``setup.cfg`` | ||||||
|  | file, usually located at the root of your repository. All options must be under a ``[pytest]`` section | ||||||
|  | (``[tool:pytest]`` for ``setup.cfg`` files). | ||||||
|  | 
 | ||||||
|  | Configuration file options may be overwritten in the command-line by using ``-o/--override``, which can also be | ||||||
|  | passed multiple times. The expected format is ``name=value``. For example:: | ||||||
|  | 
 | ||||||
|  |    pytest -o console_output_style=classic -o cache_dir=/tmp/mycache | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| .. confval:: minversion | .. confval:: minversion | ||||||
| 
 | 
 | ||||||
|    Specifies a minimal pytest version required for running tests. |    Specifies a minimal pytest version required for running tests. | ||||||
| 
 | 
 | ||||||
|         minversion = 2.1  # will fail if we run with pytest-2.0 |    .. code-block:: ini | ||||||
|  | 
 | ||||||
|  |         # content of pytest.ini | ||||||
|  |         [pytest] | ||||||
|  |         minversion = 3.0  # will fail if we run with pytest-2.8 | ||||||
| 
 | 
 | ||||||
| .. confval:: addopts | .. confval:: addopts | ||||||
| 
 | 
 | ||||||
|  | @ -165,6 +179,7 @@ Builtin configuration file options | ||||||
| 
 | 
 | ||||||
|    .. code-block:: ini |    .. code-block:: ini | ||||||
| 
 | 
 | ||||||
|  |         # content of pytest.ini | ||||||
|         [pytest] |         [pytest] | ||||||
|         addopts = --maxfail=2 -rf  # exit after 2 failures, report fail info |         addopts = --maxfail=2 -rf  # exit after 2 failures, report fail info | ||||||
| 
 | 
 | ||||||
|  | @ -331,3 +346,28 @@ Builtin configuration file options | ||||||
|         # content of pytest.ini |         # content of pytest.ini | ||||||
|         [pytest] |         [pytest] | ||||||
|         console_output_style = classic |         console_output_style = classic | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | .. confval:: empty_parameter_set_mark | ||||||
|  | 
 | ||||||
|  |     .. versionadded:: 3.4 | ||||||
|  | 
 | ||||||
|  |     Allows to pick the action for empty parametersets in parameterization | ||||||
|  | 
 | ||||||
|  |     * ``skip`` skips tests with a empty parameterset (default) | ||||||
|  |     * ``xfail`` marks tests with a empty parameterset as xfail(run=False) | ||||||
|  | 
 | ||||||
|  |     .. code-block:: ini | ||||||
|  | 
 | ||||||
|  |       # content of pytest.ini | ||||||
|  |       [pytest] | ||||||
|  |       empty_parameter_set_mark = xfail | ||||||
|  | 
 | ||||||
|  |     .. note:: | ||||||
|  | 
 | ||||||
|  |       The default value of this option is planned to change to ``xfail`` in future releases | ||||||
|  |       as this is considered less error prone, see `#3155`_ for more details. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | .. _`#3155`: https://github.com/pytest-dev/pytest/issues/3155 | ||||||
|  |  | ||||||
|  | @ -157,6 +157,8 @@ class TestRaises(object): | ||||||
| 
 | 
 | ||||||
| # thanks to Matthew Scott for this test | # thanks to Matthew Scott for this test | ||||||
| def test_dynamic_compile_shows_nicely(): | def test_dynamic_compile_shows_nicely(): | ||||||
|  |     import imp | ||||||
|  |     import sys | ||||||
|     src = 'def foo():\n assert 1 == 0\n' |     src = 'def foo():\n assert 1 == 0\n' | ||||||
|     name = 'abc-123' |     name = 'abc-123' | ||||||
|     module = imp.new_module(name) |     module = imp.new_module(name) | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ You can then restrict a test run to only run tests marked with ``webtest``:: | ||||||
|     $ pytest -v -m webtest |     $ pytest -v -m webtest | ||||||
|     =========================== test session starts ============================ |     =========================== test session starts ============================ | ||||||
|     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 |     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 | ||||||
|     cachedir: .cache |     cachedir: .pytest_cache | ||||||
|     rootdir: $REGENDOC_TMPDIR, inifile: |     rootdir: $REGENDOC_TMPDIR, inifile: | ||||||
|     collecting ... collected 4 items |     collecting ... collected 4 items | ||||||
|      |      | ||||||
|  | @ -46,7 +46,7 @@ Or the inverse, running all tests except the webtest ones:: | ||||||
|     $ pytest -v -m "not webtest" |     $ pytest -v -m "not webtest" | ||||||
|     =========================== test session starts ============================ |     =========================== test session starts ============================ | ||||||
|     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 |     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 | ||||||
|     cachedir: .cache |     cachedir: .pytest_cache | ||||||
|     rootdir: $REGENDOC_TMPDIR, inifile: |     rootdir: $REGENDOC_TMPDIR, inifile: | ||||||
|     collecting ... collected 4 items |     collecting ... collected 4 items | ||||||
|      |      | ||||||
|  | @ -67,7 +67,7 @@ tests based on their module, class, method, or function name:: | ||||||
|     $ pytest -v test_server.py::TestClass::test_method |     $ pytest -v test_server.py::TestClass::test_method | ||||||
|     =========================== test session starts ============================ |     =========================== test session starts ============================ | ||||||
|     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 |     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 | ||||||
|     cachedir: .cache |     cachedir: .pytest_cache | ||||||
|     rootdir: $REGENDOC_TMPDIR, inifile: |     rootdir: $REGENDOC_TMPDIR, inifile: | ||||||
|     collecting ... collected 1 item |     collecting ... collected 1 item | ||||||
|      |      | ||||||
|  | @ -80,7 +80,7 @@ You can also select on the class:: | ||||||
|     $ pytest -v test_server.py::TestClass |     $ pytest -v test_server.py::TestClass | ||||||
|     =========================== test session starts ============================ |     =========================== test session starts ============================ | ||||||
|     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 |     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 | ||||||
|     cachedir: .cache |     cachedir: .pytest_cache | ||||||
|     rootdir: $REGENDOC_TMPDIR, inifile: |     rootdir: $REGENDOC_TMPDIR, inifile: | ||||||
|     collecting ... collected 1 item |     collecting ... collected 1 item | ||||||
|      |      | ||||||
|  | @ -93,7 +93,7 @@ Or select multiple nodes:: | ||||||
|   $ pytest -v test_server.py::TestClass test_server.py::test_send_http |   $ pytest -v test_server.py::TestClass test_server.py::test_send_http | ||||||
|   =========================== test session starts ============================ |   =========================== test session starts ============================ | ||||||
|   platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 |   platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 | ||||||
|   cachedir: .cache |   cachedir: .pytest_cache | ||||||
|   rootdir: $REGENDOC_TMPDIR, inifile: |   rootdir: $REGENDOC_TMPDIR, inifile: | ||||||
|   collecting ... collected 2 items |   collecting ... collected 2 items | ||||||
|    |    | ||||||
|  | @ -131,7 +131,7 @@ select tests based on their names:: | ||||||
|     $ pytest -v -k http  # running with the above defined example module |     $ pytest -v -k http  # running with the above defined example module | ||||||
|     =========================== test session starts ============================ |     =========================== test session starts ============================ | ||||||
|     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 |     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 | ||||||
|     cachedir: .cache |     cachedir: .pytest_cache | ||||||
|     rootdir: $REGENDOC_TMPDIR, inifile: |     rootdir: $REGENDOC_TMPDIR, inifile: | ||||||
|     collecting ... collected 4 items |     collecting ... collected 4 items | ||||||
|      |      | ||||||
|  | @ -145,7 +145,7 @@ And you can also run all tests except the ones that match the keyword:: | ||||||
|     $ pytest -k "not send_http" -v |     $ pytest -k "not send_http" -v | ||||||
|     =========================== test session starts ============================ |     =========================== test session starts ============================ | ||||||
|     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 |     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 | ||||||
|     cachedir: .cache |     cachedir: .pytest_cache | ||||||
|     rootdir: $REGENDOC_TMPDIR, inifile: |     rootdir: $REGENDOC_TMPDIR, inifile: | ||||||
|     collecting ... collected 4 items |     collecting ... collected 4 items | ||||||
|      |      | ||||||
|  | @ -161,7 +161,7 @@ Or to select "http" and "quick" tests:: | ||||||
|     $ pytest -k "http or quick" -v |     $ pytest -k "http or quick" -v | ||||||
|     =========================== test session starts ============================ |     =========================== test session starts ============================ | ||||||
|     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 |     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 | ||||||
|     cachedir: .cache |     cachedir: .pytest_cache | ||||||
|     rootdir: $REGENDOC_TMPDIR, inifile: |     rootdir: $REGENDOC_TMPDIR, inifile: | ||||||
|     collecting ... collected 4 items |     collecting ... collected 4 items | ||||||
|      |      | ||||||
|  | @ -432,7 +432,7 @@ The output is as follows:: | ||||||
| 
 | 
 | ||||||
|     $ pytest -q -s |     $ pytest -q -s | ||||||
|     Marker info name=my_marker args=(<function hello_world at 0xdeadbeef>,) kwars={} |     Marker info name=my_marker args=(<function hello_world at 0xdeadbeef>,) kwars={} | ||||||
|     .                                                                    [100%] |     . | ||||||
|     1 passed in 0.12 seconds |     1 passed in 0.12 seconds | ||||||
| 
 | 
 | ||||||
| We can see that the custom marker has its argument set extended with the function ``hello_world``. This is the key difference between creating a custom marker as a callable, which invokes ``__call__`` behind the scenes, and using ``with_args``. | We can see that the custom marker has its argument set extended with the function ``hello_world``. This is the key difference between creating a custom marker as a callable, which invokes ``__call__`` behind the scenes, and using ``with_args``. | ||||||
|  | @ -477,7 +477,7 @@ Let's run this without capturing output and see what we get:: | ||||||
|     glob args=('function',) kwargs={'x': 3} |     glob args=('function',) kwargs={'x': 3} | ||||||
|     glob args=('class',) kwargs={'x': 2} |     glob args=('class',) kwargs={'x': 2} | ||||||
|     glob args=('module',) kwargs={'x': 1} |     glob args=('module',) kwargs={'x': 1} | ||||||
|     .                                                                    [100%] |     . | ||||||
|     1 passed in 0.12 seconds |     1 passed in 0.12 seconds | ||||||
| 
 | 
 | ||||||
| marking platform specific tests with pytest | marking platform specific tests with pytest | ||||||
|  |  | ||||||
|  | @ -60,7 +60,7 @@ consulted when reporting in ``verbose`` mode:: | ||||||
|     nonpython $ pytest -v |     nonpython $ pytest -v | ||||||
|     =========================== test session starts ============================ |     =========================== test session starts ============================ | ||||||
|     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 |     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 | ||||||
|     cachedir: .cache |     cachedir: .pytest_cache | ||||||
|     rootdir: $REGENDOC_TMPDIR/nonpython, inifile: |     rootdir: $REGENDOC_TMPDIR/nonpython, inifile: | ||||||
|     collecting ... collected 2 items |     collecting ... collected 2 items | ||||||
|      |      | ||||||
|  |  | ||||||
|  | @ -411,6 +411,8 @@ get on the terminal - we are working on that):: | ||||||
|     ____________________ test_dynamic_compile_shows_nicely _____________________ |     ____________________ test_dynamic_compile_shows_nicely _____________________ | ||||||
|      |      | ||||||
|         def test_dynamic_compile_shows_nicely(): |         def test_dynamic_compile_shows_nicely(): | ||||||
|  |             import imp | ||||||
|  |             import sys | ||||||
|             src = 'def foo():\n assert 1 == 0\n' |             src = 'def foo():\n assert 1 == 0\n' | ||||||
|             name = 'abc-123' |             name = 'abc-123' | ||||||
|             module = imp.new_module(name) |             module = imp.new_module(name) | ||||||
|  | @ -419,14 +421,14 @@ get on the terminal - we are working on that):: | ||||||
|             sys.modules[name] = module |             sys.modules[name] = module | ||||||
|     >       module.foo() |     >       module.foo() | ||||||
|      |      | ||||||
|     failure_demo.py:166:  |     failure_demo.py:168:  | ||||||
|     _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _  |     _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _  | ||||||
|      |      | ||||||
|         def foo(): |         def foo(): | ||||||
|     >    assert 1 == 0 |     >    assert 1 == 0 | ||||||
|     E    AssertionError |     E    AssertionError | ||||||
|      |      | ||||||
|     <2-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:163>:2: AssertionError |     <2-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:165>:2: AssertionError | ||||||
|     ____________________ TestMoreErrors.test_complex_error _____________________ |     ____________________ TestMoreErrors.test_complex_error _____________________ | ||||||
|      |      | ||||||
|     self = <failure_demo.TestMoreErrors object at 0xdeadbeef> |     self = <failure_demo.TestMoreErrors object at 0xdeadbeef> | ||||||
|  | @ -438,7 +440,7 @@ get on the terminal - we are working on that):: | ||||||
|                 return 43 |                 return 43 | ||||||
|     >       somefunc(f(), g()) |     >       somefunc(f(), g()) | ||||||
|      |      | ||||||
|     failure_demo.py:176:  |     failure_demo.py:178:  | ||||||
|     _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _  |     _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _  | ||||||
|     failure_demo.py:9: in somefunc |     failure_demo.py:9: in somefunc | ||||||
|         otherfunc(x,y) |         otherfunc(x,y) | ||||||
|  | @ -460,7 +462,7 @@ get on the terminal - we are working on that):: | ||||||
|     >       a,b  = l |     >       a,b  = l | ||||||
|     E       ValueError: not enough values to unpack (expected 2, got 0) |     E       ValueError: not enough values to unpack (expected 2, got 0) | ||||||
|      |      | ||||||
|     failure_demo.py:180: ValueError |     failure_demo.py:182: ValueError | ||||||
|     ____________________ TestMoreErrors.test_z2_type_error _____________________ |     ____________________ TestMoreErrors.test_z2_type_error _____________________ | ||||||
|      |      | ||||||
|     self = <failure_demo.TestMoreErrors object at 0xdeadbeef> |     self = <failure_demo.TestMoreErrors object at 0xdeadbeef> | ||||||
|  | @ -470,7 +472,7 @@ get on the terminal - we are working on that):: | ||||||
|     >       a,b  = l |     >       a,b  = l | ||||||
|     E       TypeError: 'int' object is not iterable |     E       TypeError: 'int' object is not iterable | ||||||
|      |      | ||||||
|     failure_demo.py:184: TypeError |     failure_demo.py:186: TypeError | ||||||
|     ______________________ TestMoreErrors.test_startswith ______________________ |     ______________________ TestMoreErrors.test_startswith ______________________ | ||||||
|      |      | ||||||
|     self = <failure_demo.TestMoreErrors object at 0xdeadbeef> |     self = <failure_demo.TestMoreErrors object at 0xdeadbeef> | ||||||
|  | @ -483,7 +485,7 @@ get on the terminal - we are working on that):: | ||||||
|     E        +  where False = <built-in method startswith of str object at 0xdeadbeef>('456') |     E        +  where False = <built-in method startswith of str object at 0xdeadbeef>('456') | ||||||
|     E        +    where <built-in method startswith of str object at 0xdeadbeef> = '123'.startswith |     E        +    where <built-in method startswith of str object at 0xdeadbeef> = '123'.startswith | ||||||
|      |      | ||||||
|     failure_demo.py:189: AssertionError |     failure_demo.py:191: AssertionError | ||||||
|     __________________ TestMoreErrors.test_startswith_nested ___________________ |     __________________ TestMoreErrors.test_startswith_nested ___________________ | ||||||
|      |      | ||||||
|     self = <failure_demo.TestMoreErrors object at 0xdeadbeef> |     self = <failure_demo.TestMoreErrors object at 0xdeadbeef> | ||||||
|  | @ -500,7 +502,7 @@ get on the terminal - we are working on that):: | ||||||
|     E        +      where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef>() |     E        +      where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef>() | ||||||
|     E        +    and   '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef>() |     E        +    and   '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef>() | ||||||
|      |      | ||||||
|     failure_demo.py:196: AssertionError |     failure_demo.py:198: AssertionError | ||||||
|     _____________________ TestMoreErrors.test_global_func ______________________ |     _____________________ TestMoreErrors.test_global_func ______________________ | ||||||
|      |      | ||||||
|     self = <failure_demo.TestMoreErrors object at 0xdeadbeef> |     self = <failure_demo.TestMoreErrors object at 0xdeadbeef> | ||||||
|  | @ -511,7 +513,7 @@ get on the terminal - we are working on that):: | ||||||
|     E        +  where False = isinstance(43, float) |     E        +  where False = isinstance(43, float) | ||||||
|     E        +    where 43 = globf(42) |     E        +    where 43 = globf(42) | ||||||
|      |      | ||||||
|     failure_demo.py:199: AssertionError |     failure_demo.py:201: AssertionError | ||||||
|     _______________________ TestMoreErrors.test_instance _______________________ |     _______________________ TestMoreErrors.test_instance _______________________ | ||||||
|      |      | ||||||
|     self = <failure_demo.TestMoreErrors object at 0xdeadbeef> |     self = <failure_demo.TestMoreErrors object at 0xdeadbeef> | ||||||
|  | @ -522,7 +524,7 @@ get on the terminal - we are working on that):: | ||||||
|     E       assert 42 != 42 |     E       assert 42 != 42 | ||||||
|     E        +  where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef>.x |     E        +  where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef>.x | ||||||
|      |      | ||||||
|     failure_demo.py:203: AssertionError |     failure_demo.py:205: AssertionError | ||||||
|     _______________________ TestMoreErrors.test_compare ________________________ |     _______________________ TestMoreErrors.test_compare ________________________ | ||||||
|      |      | ||||||
|     self = <failure_demo.TestMoreErrors object at 0xdeadbeef> |     self = <failure_demo.TestMoreErrors object at 0xdeadbeef> | ||||||
|  | @ -532,7 +534,7 @@ get on the terminal - we are working on that):: | ||||||
|     E       assert 11 < 5 |     E       assert 11 < 5 | ||||||
|     E        +  where 11 = globf(10) |     E        +  where 11 = globf(10) | ||||||
|      |      | ||||||
|     failure_demo.py:206: AssertionError |     failure_demo.py:208: AssertionError | ||||||
|     _____________________ TestMoreErrors.test_try_finally ______________________ |     _____________________ TestMoreErrors.test_try_finally ______________________ | ||||||
|      |      | ||||||
|     self = <failure_demo.TestMoreErrors object at 0xdeadbeef> |     self = <failure_demo.TestMoreErrors object at 0xdeadbeef> | ||||||
|  | @ -543,7 +545,7 @@ get on the terminal - we are working on that):: | ||||||
|     >           assert x == 0 |     >           assert x == 0 | ||||||
|     E           assert 1 == 0 |     E           assert 1 == 0 | ||||||
|      |      | ||||||
|     failure_demo.py:211: AssertionError |     failure_demo.py:213: AssertionError | ||||||
|     ___________________ TestCustomAssertMsg.test_single_line ___________________ |     ___________________ TestCustomAssertMsg.test_single_line ___________________ | ||||||
|      |      | ||||||
|     self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef> |     self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef> | ||||||
|  | @ -557,7 +559,7 @@ get on the terminal - we are working on that):: | ||||||
|     E       assert 1 == 2 |     E       assert 1 == 2 | ||||||
|     E        +  where 1 = <class 'failure_demo.TestCustomAssertMsg.test_single_line.<locals>.A'>.a |     E        +  where 1 = <class 'failure_demo.TestCustomAssertMsg.test_single_line.<locals>.A'>.a | ||||||
|      |      | ||||||
|     failure_demo.py:222: AssertionError |     failure_demo.py:224: AssertionError | ||||||
|     ____________________ TestCustomAssertMsg.test_multiline ____________________ |     ____________________ TestCustomAssertMsg.test_multiline ____________________ | ||||||
|      |      | ||||||
|     self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef> |     self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef> | ||||||
|  | @ -574,7 +576,7 @@ get on the terminal - we are working on that):: | ||||||
|     E       assert 1 == 2 |     E       assert 1 == 2 | ||||||
|     E        +  where 1 = <class 'failure_demo.TestCustomAssertMsg.test_multiline.<locals>.A'>.a |     E        +  where 1 = <class 'failure_demo.TestCustomAssertMsg.test_multiline.<locals>.A'>.a | ||||||
|      |      | ||||||
|     failure_demo.py:228: AssertionError |     failure_demo.py:230: AssertionError | ||||||
|     ___________________ TestCustomAssertMsg.test_custom_repr ___________________ |     ___________________ TestCustomAssertMsg.test_custom_repr ___________________ | ||||||
|      |      | ||||||
|     self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef> |     self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef> | ||||||
|  | @ -594,7 +596,7 @@ get on the terminal - we are working on that):: | ||||||
|     E       assert 1 == 2 |     E       assert 1 == 2 | ||||||
|     E        +  where 1 = This is JSON\n{\n  'foo': 'bar'\n}.a |     E        +  where 1 = This is JSON\n{\n  'foo': 'bar'\n}.a | ||||||
|      |      | ||||||
|     failure_demo.py:238: AssertionError |     failure_demo.py:240: AssertionError | ||||||
|     ============================= warnings summary ============================= |     ============================= warnings summary ============================= | ||||||
|     None |     None | ||||||
|       Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0. |       Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0. | ||||||
|  |  | ||||||
|  | @ -332,7 +332,7 @@ which will add info only when run with "--v":: | ||||||
|     $ pytest -v |     $ pytest -v | ||||||
|     =========================== test session starts ============================ |     =========================== test session starts ============================ | ||||||
|     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 |     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 | ||||||
|     cachedir: .cache |     cachedir: .pytest_cache | ||||||
|     info1: did you know that ... |     info1: did you know that ... | ||||||
|     did you? |     did you? | ||||||
|     rootdir: $REGENDOC_TMPDIR, inifile: |     rootdir: $REGENDOC_TMPDIR, inifile: | ||||||
|  | @ -385,9 +385,9 @@ Now we can profile which test functions execute the slowest:: | ||||||
|     test_some_are_slow.py ...                                            [100%] |     test_some_are_slow.py ...                                            [100%] | ||||||
|      |      | ||||||
|     ========================= slowest 3 test durations ========================= |     ========================= slowest 3 test durations ========================= | ||||||
|     0.31s call     test_some_are_slow.py::test_funcslow2 |     0.58s call     test_some_are_slow.py::test_funcslow2 | ||||||
|     0.20s call     test_some_are_slow.py::test_funcslow1 |     0.41s call     test_some_are_slow.py::test_funcslow1 | ||||||
|     0.17s call     test_some_are_slow.py::test_funcfast |     0.10s call     test_some_are_slow.py::test_funcfast | ||||||
|     ========================= 3 passed in 0.12 seconds ========================= |     ========================= 3 passed in 0.12 seconds ========================= | ||||||
| 
 | 
 | ||||||
| incremental testing - test steps | incremental testing - test steps | ||||||
|  | @ -537,7 +537,7 @@ We can run this:: | ||||||
|     file $REGENDOC_TMPDIR/b/test_error.py, line 1 |     file $REGENDOC_TMPDIR/b/test_error.py, line 1 | ||||||
|       def test_root(db):  # no db here, will error out |       def test_root(db):  # no db here, will error out | ||||||
|     E       fixture 'db' not found |     E       fixture 'db' not found | ||||||
|     >       available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_xml_property, recwarn, tmpdir, tmpdir_factory |     >       available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_xml_attribute, record_xml_property, recwarn, tmpdir, tmpdir_factory | ||||||
|     >       use 'pytest --fixtures [testpath]' for help on them. |     >       use 'pytest --fixtures [testpath]' for help on them. | ||||||
|      |      | ||||||
|     $REGENDOC_TMPDIR/b/test_error.py:1 |     $REGENDOC_TMPDIR/b/test_error.py:1 | ||||||
|  | @ -731,7 +731,7 @@ and run it:: | ||||||
|      |      | ||||||
|     test_module.py Esetting up a test failed! test_module.py::test_setup_fails |     test_module.py Esetting up a test failed! test_module.py::test_setup_fails | ||||||
|     Fexecuting test failed test_module.py::test_call_fails |     Fexecuting test failed test_module.py::test_call_fails | ||||||
|     F                                                   [100%] |     F | ||||||
|      |      | ||||||
|     ================================== ERRORS ================================== |     ================================== ERRORS ================================== | ||||||
|     ____________________ ERROR at setup of test_setup_fails ____________________ |     ____________________ ERROR at setup of test_setup_fails ____________________ | ||||||
|  |  | ||||||
|  | @ -68,5 +68,5 @@ If you run this without output capturing:: | ||||||
|     .test_method1 called |     .test_method1 called | ||||||
|     .test other |     .test other | ||||||
|     .test_unit1 method called |     .test_unit1 method called | ||||||
|     .                                                                 [100%] |     . | ||||||
|     4 passed in 0.12 seconds |     4 passed in 0.12 seconds | ||||||
|  |  | ||||||
|  | @ -286,7 +286,7 @@ tests. | ||||||
| Let's execute it:: | Let's execute it:: | ||||||
| 
 | 
 | ||||||
|     $ pytest -s -q --tb=no |     $ pytest -s -q --tb=no | ||||||
|     FF                                                                   [100%]teardown smtp |     FFteardown smtp | ||||||
|      |      | ||||||
|     2 failed in 0.12 seconds |     2 failed in 0.12 seconds | ||||||
| 
 | 
 | ||||||
|  | @ -391,7 +391,7 @@ We use the ``request.module`` attribute to optionally obtain an | ||||||
| again, nothing much has changed:: | again, nothing much has changed:: | ||||||
| 
 | 
 | ||||||
|     $ pytest -s -q --tb=no |     $ pytest -s -q --tb=no | ||||||
|     FF                                                                   [100%]finalizing <smtplib.SMTP object at 0xdeadbeef> (smtp.gmail.com) |     FFfinalizing <smtplib.SMTP object at 0xdeadbeef> (smtp.gmail.com) | ||||||
|      |      | ||||||
|     2 failed in 0.12 seconds |     2 failed in 0.12 seconds | ||||||
| 
 | 
 | ||||||
|  | @ -612,7 +612,7 @@ Here we declare an ``app`` fixture which receives the previously defined | ||||||
|     $ pytest -v test_appsetup.py |     $ pytest -v test_appsetup.py | ||||||
|     =========================== test session starts ============================ |     =========================== test session starts ============================ | ||||||
|     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 |     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 | ||||||
|     cachedir: .cache |     cachedir: .pytest_cache | ||||||
|     rootdir: $REGENDOC_TMPDIR, inifile: |     rootdir: $REGENDOC_TMPDIR, inifile: | ||||||
|     collecting ... collected 2 items |     collecting ... collected 2 items | ||||||
|      |      | ||||||
|  | @ -681,40 +681,40 @@ Let's run the tests in verbose mode and with looking at the print-output:: | ||||||
|     $ pytest -v -s test_module.py |     $ pytest -v -s test_module.py | ||||||
|     =========================== test session starts ============================ |     =========================== test session starts ============================ | ||||||
|     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 |     platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 | ||||||
|     cachedir: .cache |     cachedir: .pytest_cache | ||||||
|     rootdir: $REGENDOC_TMPDIR, inifile: |     rootdir: $REGENDOC_TMPDIR, inifile: | ||||||
|     collecting ... collected 8 items |     collecting ... collected 8 items | ||||||
|      |      | ||||||
|     test_module.py::test_0[1]   SETUP otherarg 1 |     test_module.py::test_0[1]   SETUP otherarg 1 | ||||||
|       RUN test0 with otherarg 1 |       RUN test0 with otherarg 1 | ||||||
|     PASSED                                     [ 12%]  TEARDOWN otherarg 1 |     PASSED  TEARDOWN otherarg 1 | ||||||
|      |      | ||||||
|     test_module.py::test_0[2]   SETUP otherarg 2 |     test_module.py::test_0[2]   SETUP otherarg 2 | ||||||
|       RUN test0 with otherarg 2 |       RUN test0 with otherarg 2 | ||||||
|     PASSED                                     [ 25%]  TEARDOWN otherarg 2 |     PASSED  TEARDOWN otherarg 2 | ||||||
|      |      | ||||||
|     test_module.py::test_1[mod1]   SETUP modarg mod1 |     test_module.py::test_1[mod1]   SETUP modarg mod1 | ||||||
|       RUN test1 with modarg mod1 |       RUN test1 with modarg mod1 | ||||||
|     PASSED                                  [ 37%] |     PASSED | ||||||
|     test_module.py::test_2[1-mod1]   SETUP otherarg 1 |     test_module.py::test_2[1-mod1]   SETUP otherarg 1 | ||||||
|       RUN test2 with otherarg 1 and modarg mod1 |       RUN test2 with otherarg 1 and modarg mod1 | ||||||
|     PASSED                                [ 50%]  TEARDOWN otherarg 1 |     PASSED  TEARDOWN otherarg 1 | ||||||
|      |      | ||||||
|     test_module.py::test_2[2-mod1]   SETUP otherarg 2 |     test_module.py::test_2[2-mod1]   SETUP otherarg 2 | ||||||
|       RUN test2 with otherarg 2 and modarg mod1 |       RUN test2 with otherarg 2 and modarg mod1 | ||||||
|     PASSED                                [ 62%]  TEARDOWN otherarg 2 |     PASSED  TEARDOWN otherarg 2 | ||||||
|      |      | ||||||
|     test_module.py::test_1[mod2]   TEARDOWN modarg mod1 |     test_module.py::test_1[mod2]   TEARDOWN modarg mod1 | ||||||
|       SETUP modarg mod2 |       SETUP modarg mod2 | ||||||
|       RUN test1 with modarg mod2 |       RUN test1 with modarg mod2 | ||||||
|     PASSED                                  [ 75%] |     PASSED | ||||||
|     test_module.py::test_2[1-mod2]   SETUP otherarg 1 |     test_module.py::test_2[1-mod2]   SETUP otherarg 1 | ||||||
|       RUN test2 with otherarg 1 and modarg mod2 |       RUN test2 with otherarg 1 and modarg mod2 | ||||||
|     PASSED                                [ 87%]  TEARDOWN otherarg 1 |     PASSED  TEARDOWN otherarg 1 | ||||||
|      |      | ||||||
|     test_module.py::test_2[2-mod2]   SETUP otherarg 2 |     test_module.py::test_2[2-mod2]   SETUP otherarg 2 | ||||||
|       RUN test2 with otherarg 2 and modarg mod2 |       RUN test2 with otherarg 2 and modarg mod2 | ||||||
|     PASSED                                [100%]  TEARDOWN otherarg 2 |     PASSED  TEARDOWN otherarg 2 | ||||||
|       TEARDOWN modarg mod2 |       TEARDOWN modarg mod2 | ||||||
|      |      | ||||||
|      |      | ||||||
|  |  | ||||||
|  | @ -3,24 +3,11 @@ | ||||||
| Logging | Logging | ||||||
| ------- | ------- | ||||||
| 
 | 
 | ||||||
| .. versionadded 3.3.0 | .. versionadded:: 3.3 | ||||||
|  | .. versionchanged:: 3.4 | ||||||
| 
 | 
 | ||||||
| .. note:: | pytest captures log messages of level ``WARNING`` or above automatically and displays them in their own section | ||||||
| 
 | for each failed test in the same manner as captured stdout and stderr. | ||||||
|    This feature is a drop-in replacement for the `pytest-catchlog |  | ||||||
|    <https://pypi.org/project/pytest-catchlog/>`_ plugin and they will conflict |  | ||||||
|    with each other. The backward compatibility API with ``pytest-capturelog`` |  | ||||||
|    has been dropped when this feature was introduced, so if for that reason you |  | ||||||
|    still need ``pytest-catchlog`` you can disable the internal feature by |  | ||||||
|    adding to your ``pytest.ini``: |  | ||||||
| 
 |  | ||||||
|    .. code-block:: ini |  | ||||||
| 
 |  | ||||||
|        [pytest] |  | ||||||
|            addopts=-p no:logging |  | ||||||
| 
 |  | ||||||
| Log messages are captured by default and for each failed test will be shown in |  | ||||||
| the same manner as captured stdout and stderr. |  | ||||||
| 
 | 
 | ||||||
| Running without options:: | Running without options:: | ||||||
| 
 | 
 | ||||||
|  | @ -29,7 +16,7 @@ Running without options:: | ||||||
| Shows failed tests like so:: | Shows failed tests like so:: | ||||||
| 
 | 
 | ||||||
|     ----------------------- Captured stdlog call ---------------------- |     ----------------------- Captured stdlog call ---------------------- | ||||||
|     test_reporting.py    26 INFO     text going to logger |     test_reporting.py    26 WARNING  text going to logger | ||||||
|     ----------------------- Captured stdout call ---------------------- |     ----------------------- Captured stdout call ---------------------- | ||||||
|     text going to stdout |     text going to stdout | ||||||
|     ----------------------- Captured stderr call ---------------------- |     ----------------------- Captured stderr call ---------------------- | ||||||
|  | @ -37,11 +24,10 @@ Shows failed tests like so:: | ||||||
|     ==================== 2 failed in 0.02 seconds ===================== |     ==================== 2 failed in 0.02 seconds ===================== | ||||||
| 
 | 
 | ||||||
| By default each captured log message shows the module, line number, log level | By default each captured log message shows the module, line number, log level | ||||||
| and message.  Showing the exact module and line number is useful for testing and | and message. | ||||||
| debugging.  If desired the log format and date format can be specified to |  | ||||||
| anything that the logging module supports. |  | ||||||
| 
 | 
 | ||||||
| Running pytest specifying formatting options:: | If desired the log and date format can be specified to | ||||||
|  | anything that the logging module supports by passing specific formatting options:: | ||||||
| 
 | 
 | ||||||
|     pytest --log-format="%(asctime)s %(levelname)s %(message)s" \ |     pytest --log-format="%(asctime)s %(levelname)s %(message)s" \ | ||||||
|             --log-date-format="%Y-%m-%d %H:%M:%S" |             --log-date-format="%Y-%m-%d %H:%M:%S" | ||||||
|  | @ -49,14 +35,14 @@ Running pytest specifying formatting options:: | ||||||
| Shows failed tests like so:: | Shows failed tests like so:: | ||||||
| 
 | 
 | ||||||
|     ----------------------- Captured stdlog call ---------------------- |     ----------------------- Captured stdlog call ---------------------- | ||||||
|     2010-04-10 14:48:44 INFO text going to logger |     2010-04-10 14:48:44 WARNING text going to logger | ||||||
|     ----------------------- Captured stdout call ---------------------- |     ----------------------- Captured stdout call ---------------------- | ||||||
|     text going to stdout |     text going to stdout | ||||||
|     ----------------------- Captured stderr call ---------------------- |     ----------------------- Captured stderr call ---------------------- | ||||||
|     text going to stderr |     text going to stderr | ||||||
|     ==================== 2 failed in 0.02 seconds ===================== |     ==================== 2 failed in 0.02 seconds ===================== | ||||||
| 
 | 
 | ||||||
| These options can also be customized through a configuration file: | These options can also be customized through ``pytest.ini`` file: | ||||||
| 
 | 
 | ||||||
| .. code-block:: ini | .. code-block:: ini | ||||||
| 
 | 
 | ||||||
|  | @ -69,7 +55,7 @@ with:: | ||||||
| 
 | 
 | ||||||
|     pytest --no-print-logs |     pytest --no-print-logs | ||||||
| 
 | 
 | ||||||
| Or in your ``pytest.ini``: | Or in the ``pytest.ini`` file: | ||||||
| 
 | 
 | ||||||
| .. code-block:: ini | .. code-block:: ini | ||||||
| 
 | 
 | ||||||
|  | @ -85,6 +71,10 @@ Shows failed tests in the normal manner as no logs were captured:: | ||||||
|     text going to stderr |     text going to stderr | ||||||
|     ==================== 2 failed in 0.02 seconds ===================== |     ==================== 2 failed in 0.02 seconds ===================== | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | caplog fixture | ||||||
|  | ^^^^^^^^^^^^^^ | ||||||
|  | 
 | ||||||
| Inside tests it is possible to change the log level for the captured log | Inside tests it is possible to change the log level for the captured log | ||||||
| messages.  This is supported by the ``caplog`` fixture:: | messages.  This is supported by the ``caplog`` fixture:: | ||||||
| 
 | 
 | ||||||
|  | @ -92,7 +82,7 @@ messages.  This is supported by the ``caplog`` fixture:: | ||||||
|         caplog.set_level(logging.INFO) |         caplog.set_level(logging.INFO) | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
| By default the level is set on the handler used to catch the log messages, | By default the level is set on the root logger, | ||||||
| however as a convenience it is also possible to set the log level of any | however as a convenience it is also possible to set the log level of any | ||||||
| logger:: | logger:: | ||||||
| 
 | 
 | ||||||
|  | @ -100,14 +90,16 @@ logger:: | ||||||
|         caplog.set_level(logging.CRITICAL, logger='root.baz') |         caplog.set_level(logging.CRITICAL, logger='root.baz') | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
|  | The log levels set are restored automatically at the end of the test. | ||||||
|  | 
 | ||||||
| It is also possible to use a context manager to temporarily change the log | It is also possible to use a context manager to temporarily change the log | ||||||
| level:: | level inside a ``with`` block:: | ||||||
| 
 | 
 | ||||||
|     def test_bar(caplog): |     def test_bar(caplog): | ||||||
|         with caplog.at_level(logging.INFO): |         with caplog.at_level(logging.INFO): | ||||||
|             pass |             pass | ||||||
| 
 | 
 | ||||||
| Again, by default the level of the handler is affected but the level of any | Again, by default the level of the root logger is affected but the level of any | ||||||
| logger can be changed instead with:: | logger can be changed instead with:: | ||||||
| 
 | 
 | ||||||
|     def test_bar(caplog): |     def test_bar(caplog): | ||||||
|  | @ -115,7 +107,7 @@ logger can be changed instead with:: | ||||||
|             pass |             pass | ||||||
| 
 | 
 | ||||||
| Lastly all the logs sent to the logger during the test run are made available on | Lastly all the logs sent to the logger during the test run are made available on | ||||||
| the fixture in the form of both the LogRecord instances and the final log text. | the fixture in the form of both the ``logging.LogRecord`` instances and the final log text. | ||||||
| This is useful for when you want to assert on the contents of a message:: | This is useful for when you want to assert on the contents of a message:: | ||||||
| 
 | 
 | ||||||
|     def test_baz(caplog): |     def test_baz(caplog): | ||||||
|  | @ -146,12 +138,41 @@ You can call ``caplog.clear()`` to reset the captured log records in a test:: | ||||||
|         your_test_method() |         your_test_method() | ||||||
|         assert ['Foo'] == [rec.message for rec in caplog.records] |         assert ['Foo'] == [rec.message for rec in caplog.records] | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | The ``caplop.records`` attribute contains records from the current stage only, so | ||||||
|  | inside the ``setup`` phase it contains only setup logs, same with the ``call`` and | ||||||
|  | ``teardown`` phases. | ||||||
|  | 
 | ||||||
|  | To access logs from other stages, use the ``caplog.get_records(when)`` method. As an example, | ||||||
|  | if you want to make sure that tests which use a certain fixture never log any warnings, you can inspect | ||||||
|  | the records for the ``setup`` and ``call`` stages during teardown like so: | ||||||
|  | 
 | ||||||
|  | .. code-block:: python | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     @pytest.fixture | ||||||
|  |     def window(caplog): | ||||||
|  |         window = create_window() | ||||||
|  |         yield window | ||||||
|  |         for when in ('setup', 'call'): | ||||||
|  |             messages = [x.message for x in caplog.get_records(when) if x.level == logging.WARNING] | ||||||
|  |             if messages: | ||||||
|  |                 pytest.fail('warning messages encountered during testing: {}'.format(messages)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | caplog fixture API | ||||||
|  | ~~~~~~~~~~~~~~~~~~ | ||||||
|  | 
 | ||||||
|  | .. autoclass:: _pytest.logging.LogCaptureFixture | ||||||
|  |     :members: | ||||||
|  | 
 | ||||||
|  | .. _live_logs: | ||||||
|  | 
 | ||||||
| Live Logs | Live Logs | ||||||
| ^^^^^^^^^ | ^^^^^^^^^ | ||||||
| 
 | 
 | ||||||
| By default, pytest will output any logging records with a level higher or | By setting the :confval:`log_cli` configuration option to ``true``, pytest will output | ||||||
| equal to WARNING. In order to actually see these logs in the console you have to | logging records as they are emitted directly into the console. | ||||||
| disable pytest output capture by passing ``-s``. |  | ||||||
| 
 | 
 | ||||||
| You can specify the logging level for which log records with equal or higher | You can specify the logging level for which log records with equal or higher | ||||||
| level are printed to the console by passing ``--log-cli-level``. This setting | level are printed to the console by passing ``--log-cli-level``. This setting | ||||||
|  | @ -190,3 +211,49 @@ option names are: | ||||||
| * ``log_file_level`` | * ``log_file_level`` | ||||||
| * ``log_file_format`` | * ``log_file_format`` | ||||||
| * ``log_file_date_format`` | * ``log_file_date_format`` | ||||||
|  | 
 | ||||||
|  | .. _log_release_notes: | ||||||
|  | 
 | ||||||
|  | Release notes | ||||||
|  | ^^^^^^^^^^^^^ | ||||||
|  | 
 | ||||||
|  | This feature was introduced as a drop-in replacement for the `pytest-catchlog | ||||||
|  | <https://pypi.org/project/pytest-catchlog/>`_ plugin and they conflict | ||||||
|  | with each other. The backward compatibility API with ``pytest-capturelog`` | ||||||
|  | has been dropped when this feature was introduced, so if for that reason you | ||||||
|  | still need ``pytest-catchlog`` you can disable the internal feature by | ||||||
|  | adding to your ``pytest.ini``: | ||||||
|  | 
 | ||||||
|  | .. code-block:: ini | ||||||
|  | 
 | ||||||
|  |    [pytest] | ||||||
|  |        addopts=-p no:logging | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | .. _log_changes_3_4: | ||||||
|  | 
 | ||||||
|  | Incompatible changes in pytest 3.4 | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  | 
 | ||||||
|  | This feature was introduced in ``3.3`` and some **incompatible changes** have been | ||||||
|  | made in ``3.4`` after community feedback: | ||||||
|  | 
 | ||||||
|  | * Log levels are no longer changed unless explicitly requested by the :confval:`log_level` configuration | ||||||
|  |   or ``--log-level`` command-line options. This allows users to configure logger objects themselves. | ||||||
|  | * :ref:`Live Logs <live_logs>` is now disabled by default and can be enabled setting the | ||||||
|  |   :confval:`log_cli` configuration option to ``true``. When enabled, the verbosity is increased so logging for each | ||||||
|  |   test is visible. | ||||||
|  | * :ref:`Live Logs <live_logs>` are now sent to ``sys.stdout`` and no longer require the ``-s`` command-line option | ||||||
|  |   to work. | ||||||
|  | 
 | ||||||
|  | If you want to partially restore the logging behavior of version ``3.3``, you can add this options to your ``ini`` | ||||||
|  | file: | ||||||
|  | 
 | ||||||
|  | .. code-block:: ini | ||||||
|  | 
 | ||||||
|  |     [pytest] | ||||||
|  |     log_cli=true | ||||||
|  |     log_level=NOTSET | ||||||
|  | 
 | ||||||
|  | More details about the discussion that lead to this changes can be read in | ||||||
|  | issue `#3013 <https://github.com/pytest-dev/pytest/issues/3013>`_. | ||||||
|  |  | ||||||
|  | @ -256,6 +256,66 @@ This will add an extra property ``example_key="1"`` to the generated | ||||||
|     Also please note that using this feature will break any schema verification. |     Also please note that using this feature will break any schema verification. | ||||||
|     This might be a problem when used with some CI servers. |     This might be a problem when used with some CI servers. | ||||||
| 
 | 
 | ||||||
|  | record_xml_attribute | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  | 
 | ||||||
|  | .. versionadded:: 3.4 | ||||||
|  | 
 | ||||||
|  | To add an additional xml attribute to a testcase element, you can use | ||||||
|  | ``record_xml_attribute`` fixture. This can also be used to override existing values: | ||||||
|  | 
 | ||||||
|  | .. code-block:: python | ||||||
|  | 
 | ||||||
|  |     def test_function(record_xml_attribute): | ||||||
|  |         record_xml_attribute("assertions", "REQ-1234") | ||||||
|  |         record_xml_attribute("classname", "custom_classname") | ||||||
|  |         print('hello world') | ||||||
|  |         assert True | ||||||
|  | 
 | ||||||
|  | Unlike ``record_xml_property``, this will not add a new child element. | ||||||
|  | Instead, this will add an attribute ``assertions="REQ-1234"`` inside the generated | ||||||
|  | ``testcase`` tag and override the default ``classname`` with ``"classname=custom_classname"``: | ||||||
|  | 
 | ||||||
|  | .. code-block:: xml | ||||||
|  | 
 | ||||||
|  |     <testcase classname="custom_classname" file="test_function.py" line="0" name="test_function" time="0.003" assertions="REQ-1234"> | ||||||
|  |         <system-out> | ||||||
|  |             hello world | ||||||
|  |         </system-out> | ||||||
|  |     </testcase> | ||||||
|  | 
 | ||||||
|  | .. warning:: | ||||||
|  | 
 | ||||||
|  |     ``record_xml_attribute`` is an experimental feature, and its interface might be replaced | ||||||
|  |     by something more powerful and general in future versions. The | ||||||
|  |     functionality per-se will be kept, however. | ||||||
|  | 
 | ||||||
|  |     Using this over ``record_xml_property`` can help when using ci tools to parse the xml report. | ||||||
|  |     However, some parsers are quite strict about the elements and attributes that are allowed. | ||||||
|  |     Many tools use an xsd schema (like the example below) to validate incoming xml. | ||||||
|  |     Make sure you are using attribute names that are allowed by your parser. | ||||||
|  | 
 | ||||||
|  |     Below is the Scheme used by Jenkins to validate the XML report: | ||||||
|  | 
 | ||||||
|  |     .. code-block:: xml | ||||||
|  | 
 | ||||||
|  |         <xs:element name="testcase"> | ||||||
|  |             <xs:complexType> | ||||||
|  |                 <xs:sequence> | ||||||
|  |                     <xs:element ref="skipped" minOccurs="0" maxOccurs="1"/> | ||||||
|  |                     <xs:element ref="error" minOccurs="0" maxOccurs="unbounded"/> | ||||||
|  |                     <xs:element ref="failure" minOccurs="0" maxOccurs="unbounded"/> | ||||||
|  |                     <xs:element ref="system-out" minOccurs="0" maxOccurs="unbounded"/> | ||||||
|  |                     <xs:element ref="system-err" minOccurs="0" maxOccurs="unbounded"/> | ||||||
|  |                 </xs:sequence> | ||||||
|  |                 <xs:attribute name="name" type="xs:string" use="required"/> | ||||||
|  |                 <xs:attribute name="assertions" type="xs:string" use="optional"/> | ||||||
|  |                 <xs:attribute name="time" type="xs:string" use="optional"/> | ||||||
|  |                 <xs:attribute name="classname" type="xs:string" use="optional"/> | ||||||
|  |                 <xs:attribute name="status" type="xs:string" use="optional"/> | ||||||
|  |             </xs:complexType> | ||||||
|  |         </xs:element> | ||||||
|  | 
 | ||||||
| LogXML: add_global_property | LogXML: add_global_property | ||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
| 
 | 
 | ||||||
|  | @ -388,6 +448,7 @@ hook was invoked:: | ||||||
|     $ python myinvoke.py |     $ python myinvoke.py | ||||||
|     *** test run reporting finishing |     *** test run reporting finishing | ||||||
|      |      | ||||||
|  | 
 | ||||||
| .. note:: | .. note:: | ||||||
| 
 | 
 | ||||||
|     Calling ``pytest.main()`` will result in importing your tests and any modules |     Calling ``pytest.main()`` will result in importing your tests and any modules | ||||||
|  |  | ||||||
|  | @ -609,6 +609,8 @@ All runtest related hooks receive a :py:class:`pytest.Item <_pytest.main.Item>` | ||||||
| 
 | 
 | ||||||
| .. autofunction:: pytest_runtestloop | .. autofunction:: pytest_runtestloop | ||||||
| .. autofunction:: pytest_runtest_protocol | .. autofunction:: pytest_runtest_protocol | ||||||
|  | .. autofunction:: pytest_runtest_logstart | ||||||
|  | .. autofunction:: pytest_runtest_logfinish | ||||||
| .. autofunction:: pytest_runtest_setup | .. autofunction:: pytest_runtest_setup | ||||||
| .. autofunction:: pytest_runtest_call | .. autofunction:: pytest_runtest_call | ||||||
| .. autofunction:: pytest_runtest_teardown | .. autofunction:: pytest_runtest_teardown | ||||||
|  | @ -693,14 +695,14 @@ Reference of objects involved in hooks | ||||||
| .. autoclass:: _pytest.config.Parser() | .. autoclass:: _pytest.config.Parser() | ||||||
|     :members: |     :members: | ||||||
| 
 | 
 | ||||||
| .. autoclass:: _pytest.main.Node() | .. autoclass:: _pytest.nodes.Node() | ||||||
|     :members: |     :members: | ||||||
| 
 | 
 | ||||||
| .. autoclass:: _pytest.main.Collector() | .. autoclass:: _pytest.nodes.Collector() | ||||||
|     :members: |     :members: | ||||||
|     :show-inheritance: |     :show-inheritance: | ||||||
| 
 | 
 | ||||||
| .. autoclass:: _pytest.main.FSCollector() | .. autoclass:: _pytest.nodes.FSCollector() | ||||||
|     :members: |     :members: | ||||||
|     :show-inheritance: |     :show-inheritance: | ||||||
| 
 | 
 | ||||||
|  | @ -708,7 +710,7 @@ Reference of objects involved in hooks | ||||||
|     :members: |     :members: | ||||||
|     :show-inheritance: |     :show-inheritance: | ||||||
| 
 | 
 | ||||||
| .. autoclass:: _pytest.main.Item() | .. autoclass:: _pytest.nodes.Item() | ||||||
|     :members: |     :members: | ||||||
|     :show-inheritance: |     :show-inheritance: | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -18,7 +18,8 @@ from _pytest.debugging import pytestPDB as __pytestPDB | ||||||
| from _pytest.recwarn import warns, deprecated_call | from _pytest.recwarn import warns, deprecated_call | ||||||
| from _pytest.outcomes import fail, skip, importorskip, exit, xfail | from _pytest.outcomes import fail, skip, importorskip, exit, xfail | ||||||
| from _pytest.mark import MARK_GEN as mark, param | from _pytest.mark import MARK_GEN as mark, param | ||||||
| from _pytest.main import Item, Collector, File, Session | from _pytest.main import Session | ||||||
|  | from _pytest.nodes import Item, Collector, File | ||||||
| from _pytest.fixtures import fillfixtures as _fillfuncargs | from _pytest.fixtures import fillfixtures as _fillfuncargs | ||||||
| from _pytest.python import ( | from _pytest.python import ( | ||||||
|     Module, Class, Instance, Function, Generator, |     Module, Class, Instance, Function, Generator, | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| invoke | devpi-client | ||||||
| tox |  | ||||||
| gitpython | gitpython | ||||||
|  | invoke | ||||||
| towncrier | towncrier | ||||||
|  | tox | ||||||
| wheel | wheel | ||||||
|  |  | ||||||
|  | @ -536,7 +536,7 @@ class TestInvocationVariants(object): | ||||||
|         path = testdir.mkpydir("tpkg") |         path = testdir.mkpydir("tpkg") | ||||||
|         path.join("test_hello.py").write('raise ImportError') |         path.join("test_hello.py").write('raise ImportError') | ||||||
| 
 | 
 | ||||||
|         result = testdir.runpytest_subprocess("--pyargs", "tpkg.test_hello") |         result = testdir.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True) | ||||||
|         assert result.ret != 0 |         assert result.ret != 0 | ||||||
| 
 | 
 | ||||||
|         result.stdout.fnmatch_lines([ |         result.stdout.fnmatch_lines([ | ||||||
|  | @ -554,7 +554,7 @@ class TestInvocationVariants(object): | ||||||
|         result.stdout.fnmatch_lines([ |         result.stdout.fnmatch_lines([ | ||||||
|             "*2 passed*" |             "*2 passed*" | ||||||
|         ]) |         ]) | ||||||
|         result = testdir.runpytest("--pyargs", "tpkg.test_hello") |         result = testdir.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True) | ||||||
|         assert result.ret == 0 |         assert result.ret == 0 | ||||||
|         result.stdout.fnmatch_lines([ |         result.stdout.fnmatch_lines([ | ||||||
|             "*1 passed*" |             "*1 passed*" | ||||||
|  | @ -578,7 +578,7 @@ class TestInvocationVariants(object): | ||||||
|         ]) |         ]) | ||||||
| 
 | 
 | ||||||
|         monkeypatch.setenv('PYTHONPATH', join_pythonpath(testdir)) |         monkeypatch.setenv('PYTHONPATH', join_pythonpath(testdir)) | ||||||
|         result = testdir.runpytest("--pyargs", "tpkg.test_missing") |         result = testdir.runpytest("--pyargs", "tpkg.test_missing", syspathinsert=True) | ||||||
|         assert result.ret != 0 |         assert result.ret != 0 | ||||||
|         result.stderr.fnmatch_lines([ |         result.stderr.fnmatch_lines([ | ||||||
|             "*not*found*test_missing*", |             "*not*found*test_missing*", | ||||||
|  | @ -902,7 +902,7 @@ def test_deferred_hook_checking(testdir): | ||||||
|     testdir.syspathinsert() |     testdir.syspathinsert() | ||||||
|     testdir.makepyfile(**{ |     testdir.makepyfile(**{ | ||||||
|         'plugin.py': """ |         'plugin.py': """ | ||||||
|         class Hooks: |         class Hooks(object): | ||||||
|             def pytest_my_hook(self, config): |             def pytest_my_hook(self, config): | ||||||
|                 pass |                 pass | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -58,7 +58,6 @@ def test_str_args_deprecated(tmpdir, testdir): | ||||||
|             warnings.append(message) |             warnings.append(message) | ||||||
| 
 | 
 | ||||||
|     ret = pytest.main("%s -x" % tmpdir, plugins=[Collect()]) |     ret = pytest.main("%s -x" % tmpdir, plugins=[Collect()]) | ||||||
|     testdir.delete_loaded_modules() |  | ||||||
|     msg = ('passing a string to pytest.main() is deprecated, ' |     msg = ('passing a string to pytest.main() is deprecated, ' | ||||||
|            'pass a list of arguments instead.') |            'pass a list of arguments instead.') | ||||||
|     assert msg in warnings |     assert msg in warnings | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| import logging | import logging | ||||||
| 
 | 
 | ||||||
|  | import pytest | ||||||
| 
 | 
 | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
| sublogger = logging.getLogger(__name__ + '.baz') | sublogger = logging.getLogger(__name__ + '.baz') | ||||||
|  | @ -26,6 +27,30 @@ def test_change_level(caplog): | ||||||
|     assert 'CRITICAL' in caplog.text |     assert 'CRITICAL' in caplog.text | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def test_change_level_undo(testdir): | ||||||
|  |     """Ensure that 'set_level' is undone after the end of the test""" | ||||||
|  |     testdir.makepyfile(''' | ||||||
|  |         import logging | ||||||
|  | 
 | ||||||
|  |         def test1(caplog): | ||||||
|  |             caplog.set_level(logging.INFO) | ||||||
|  |             # using + operator here so fnmatch_lines doesn't match the code in the traceback | ||||||
|  |             logging.info('log from ' + 'test1') | ||||||
|  |             assert 0 | ||||||
|  | 
 | ||||||
|  |         def test2(caplog): | ||||||
|  |             # using + operator here so fnmatch_lines doesn't match the code in the traceback | ||||||
|  |             logging.info('log from ' + 'test2') | ||||||
|  |             assert 0 | ||||||
|  |     ''') | ||||||
|  |     result = testdir.runpytest_subprocess() | ||||||
|  |     result.stdout.fnmatch_lines([ | ||||||
|  |         '*log from test1*', | ||||||
|  |         '*2 failed in *', | ||||||
|  |     ]) | ||||||
|  |     assert 'log from test2' not in result.stdout.str() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def test_with_statement(caplog): | def test_with_statement(caplog): | ||||||
|     with caplog.at_level(logging.INFO): |     with caplog.at_level(logging.INFO): | ||||||
|         logger.debug('handler DEBUG level') |         logger.debug('handler DEBUG level') | ||||||
|  | @ -42,6 +67,7 @@ def test_with_statement(caplog): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_log_access(caplog): | def test_log_access(caplog): | ||||||
|  |     caplog.set_level(logging.INFO) | ||||||
|     logger.info('boo %s', 'arg') |     logger.info('boo %s', 'arg') | ||||||
|     assert caplog.records[0].levelname == 'INFO' |     assert caplog.records[0].levelname == 'INFO' | ||||||
|     assert caplog.records[0].msg == 'boo %s' |     assert caplog.records[0].msg == 'boo %s' | ||||||
|  | @ -49,6 +75,7 @@ def test_log_access(caplog): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_record_tuples(caplog): | def test_record_tuples(caplog): | ||||||
|  |     caplog.set_level(logging.INFO) | ||||||
|     logger.info('boo %s', 'arg') |     logger.info('boo %s', 'arg') | ||||||
| 
 | 
 | ||||||
|     assert caplog.record_tuples == [ |     assert caplog.record_tuples == [ | ||||||
|  | @ -57,6 +84,7 @@ def test_record_tuples(caplog): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_unicode(caplog): | def test_unicode(caplog): | ||||||
|  |     caplog.set_level(logging.INFO) | ||||||
|     logger.info(u'bū') |     logger.info(u'bū') | ||||||
|     assert caplog.records[0].levelname == 'INFO' |     assert caplog.records[0].levelname == 'INFO' | ||||||
|     assert caplog.records[0].msg == u'bū' |     assert caplog.records[0].msg == u'bū' | ||||||
|  | @ -64,7 +92,29 @@ def test_unicode(caplog): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_clear(caplog): | def test_clear(caplog): | ||||||
|  |     caplog.set_level(logging.INFO) | ||||||
|     logger.info(u'bū') |     logger.info(u'bū') | ||||||
|     assert len(caplog.records) |     assert len(caplog.records) | ||||||
|     caplog.clear() |     caplog.clear() | ||||||
|     assert not len(caplog.records) |     assert not len(caplog.records) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @pytest.fixture | ||||||
|  | def logging_during_setup_and_teardown(caplog): | ||||||
|  |     caplog.set_level('INFO') | ||||||
|  |     logger.info('a_setup_log') | ||||||
|  |     yield | ||||||
|  |     logger.info('a_teardown_log') | ||||||
|  |     assert [x.message for x in caplog.get_records('teardown')] == ['a_teardown_log'] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardown): | ||||||
|  |     assert not caplog.records | ||||||
|  |     assert not caplog.get_records('call') | ||||||
|  |     logger.info('a_call_log') | ||||||
|  |     assert [x.message for x in caplog.get_records('call')] == ['a_call_log'] | ||||||
|  | 
 | ||||||
|  |     assert [x.message for x in caplog.get_records('setup')] == ['a_setup_log'] | ||||||
|  | 
 | ||||||
|  |     # This reachers into private API, don't use this type of thing in real tests! | ||||||
|  |     assert set(caplog._item.catch_log_handlers.keys()) == {'setup', 'call'} | ||||||
|  |  | ||||||
|  | @ -0,0 +1,29 @@ | ||||||
|  | import logging | ||||||
|  | 
 | ||||||
|  | import py.io | ||||||
|  | from _pytest.logging import ColoredLevelFormatter | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_coloredlogformatter(): | ||||||
|  |     logfmt = '%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s' | ||||||
|  | 
 | ||||||
|  |     record = logging.LogRecord( | ||||||
|  |         name='dummy', level=logging.INFO, pathname='dummypath', lineno=10, | ||||||
|  |         msg='Test Message', args=(), exc_info=False) | ||||||
|  | 
 | ||||||
|  |     class ColorConfig(object): | ||||||
|  |         class option(object): | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  |     tw = py.io.TerminalWriter() | ||||||
|  |     tw.hasmarkup = True | ||||||
|  |     formatter = ColoredLevelFormatter(tw, logfmt) | ||||||
|  |     output = formatter.format(record) | ||||||
|  |     assert output == ('dummypath                   10 ' | ||||||
|  |                       '\x1b[32mINFO    \x1b[0m Test Message') | ||||||
|  | 
 | ||||||
|  |     tw.hasmarkup = False | ||||||
|  |     formatter = ColoredLevelFormatter(tw, logfmt) | ||||||
|  |     output = formatter.format(record) | ||||||
|  |     assert output == ('dummypath                   10 ' | ||||||
|  |                       'INFO     Test Message') | ||||||
|  | @ -1,5 +1,8 @@ | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| import os | import os | ||||||
|  | 
 | ||||||
|  | import six | ||||||
|  | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -35,7 +38,7 @@ def test_messages_logged(testdir): | ||||||
|             logger.info('text going to logger') |             logger.info('text going to logger') | ||||||
|             assert False |             assert False | ||||||
|         ''') |         ''') | ||||||
|     result = testdir.runpytest() |     result = testdir.runpytest('--log-level=INFO') | ||||||
|     assert result.ret == 1 |     assert result.ret == 1 | ||||||
|     result.stdout.fnmatch_lines(['*- Captured *log call -*', |     result.stdout.fnmatch_lines(['*- Captured *log call -*', | ||||||
|                                  '*text going to logger*']) |                                  '*text going to logger*']) | ||||||
|  | @ -58,7 +61,7 @@ def test_setup_logging(testdir): | ||||||
|             logger.info('text going to logger from call') |             logger.info('text going to logger from call') | ||||||
|             assert False |             assert False | ||||||
|         ''') |         ''') | ||||||
|     result = testdir.runpytest() |     result = testdir.runpytest('--log-level=INFO') | ||||||
|     assert result.ret == 1 |     assert result.ret == 1 | ||||||
|     result.stdout.fnmatch_lines(['*- Captured *log setup -*', |     result.stdout.fnmatch_lines(['*- Captured *log setup -*', | ||||||
|                                  '*text going to logger from setup*', |                                  '*text going to logger from setup*', | ||||||
|  | @ -79,7 +82,7 @@ def test_teardown_logging(testdir): | ||||||
|             logger.info('text going to logger from teardown') |             logger.info('text going to logger from teardown') | ||||||
|             assert False |             assert False | ||||||
|         ''') |         ''') | ||||||
|     result = testdir.runpytest() |     result = testdir.runpytest('--log-level=INFO') | ||||||
|     assert result.ret == 1 |     assert result.ret == 1 | ||||||
|     result.stdout.fnmatch_lines(['*- Captured *log call -*', |     result.stdout.fnmatch_lines(['*- Captured *log call -*', | ||||||
|                                  '*text going to logger from call*', |                                  '*text going to logger from call*', | ||||||
|  | @ -141,6 +144,30 @@ def test_disable_log_capturing_ini(testdir): | ||||||
|         result.stdout.fnmatch_lines(['*- Captured *log call -*']) |         result.stdout.fnmatch_lines(['*- Captured *log call -*']) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @pytest.mark.parametrize('enabled', [True, False]) | ||||||
|  | def test_log_cli_enabled_disabled(testdir, enabled): | ||||||
|  |     msg = 'critical message logged by test' | ||||||
|  |     testdir.makepyfile(''' | ||||||
|  |         import logging | ||||||
|  |         def test_log_cli(): | ||||||
|  |             logging.critical("{}") | ||||||
|  |     '''.format(msg)) | ||||||
|  |     if enabled: | ||||||
|  |         testdir.makeini(''' | ||||||
|  |             [pytest] | ||||||
|  |             log_cli=true | ||||||
|  |         ''') | ||||||
|  |     result = testdir.runpytest() | ||||||
|  |     if enabled: | ||||||
|  |         result.stdout.fnmatch_lines([ | ||||||
|  |             'test_log_cli_enabled_disabled.py::test_log_cli ', | ||||||
|  |             'test_log_cli_enabled_disabled.py* CRITICAL critical message logged by test', | ||||||
|  |             'PASSED*', | ||||||
|  |         ]) | ||||||
|  |     else: | ||||||
|  |         assert msg not in result.stdout.str() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def test_log_cli_default_level(testdir): | def test_log_cli_default_level(testdir): | ||||||
|     # Default log file level |     # Default log file level | ||||||
|     testdir.makepyfile(''' |     testdir.makepyfile(''' | ||||||
|  | @ -148,32 +175,103 @@ def test_log_cli_default_level(testdir): | ||||||
|         import logging |         import logging | ||||||
|         def test_log_cli(request): |         def test_log_cli(request): | ||||||
|             plugin = request.config.pluginmanager.getplugin('logging-plugin') |             plugin = request.config.pluginmanager.getplugin('logging-plugin') | ||||||
|             assert plugin.log_cli_handler.level == logging.WARNING |             assert plugin.log_cli_handler.level == logging.NOTSET | ||||||
|             logging.getLogger('catchlog').info("This log message won't be shown") |             logging.getLogger('catchlog').info("INFO message won't be shown") | ||||||
|             logging.getLogger('catchlog').warning("This log message will be shown") |             logging.getLogger('catchlog').warning("WARNING message will be shown") | ||||||
|             print('PASSED') |     ''') | ||||||
|  |     testdir.makeini(''' | ||||||
|  |         [pytest] | ||||||
|  |         log_cli=true | ||||||
|     ''') |     ''') | ||||||
| 
 | 
 | ||||||
|     result = testdir.runpytest('-s') |     result = testdir.runpytest() | ||||||
| 
 | 
 | ||||||
|     # fnmatch_lines does an assertion internally |     # fnmatch_lines does an assertion internally | ||||||
|     result.stdout.fnmatch_lines([ |     result.stdout.fnmatch_lines([ | ||||||
|         'test_log_cli_default_level.py PASSED', |         'test_log_cli_default_level.py::test_log_cli ', | ||||||
|  |         'test_log_cli_default_level.py*WARNING message will be shown*', | ||||||
|     ]) |     ]) | ||||||
|     result.stderr.fnmatch_lines([ |     assert "INFO message won't be shown" not in result.stdout.str() | ||||||
|         "* This log message will be shown" |  | ||||||
|     ]) |  | ||||||
|     for line in result.errlines: |  | ||||||
|         try: |  | ||||||
|             assert "This log message won't be shown" in line |  | ||||||
|             pytest.fail("A log message was shown and it shouldn't have been") |  | ||||||
|         except AssertionError: |  | ||||||
|             continue |  | ||||||
| 
 |  | ||||||
|     # make sure that that we get a '0' exit code for the testsuite |     # make sure that that we get a '0' exit code for the testsuite | ||||||
|     assert result.ret == 0 |     assert result.ret == 0 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def test_log_cli_default_level_multiple_tests(testdir, request): | ||||||
|  |     """Ensure we reset the first newline added by the live logger between tests""" | ||||||
|  |     filename = request.node.name + '.py' | ||||||
|  |     testdir.makepyfile(''' | ||||||
|  |         import logging | ||||||
|  | 
 | ||||||
|  |         def test_log_1(): | ||||||
|  |             logging.warning("log message from test_log_1") | ||||||
|  | 
 | ||||||
|  |         def test_log_2(): | ||||||
|  |             logging.warning("log message from test_log_2") | ||||||
|  |     ''') | ||||||
|  |     testdir.makeini(''' | ||||||
|  |         [pytest] | ||||||
|  |         log_cli=true | ||||||
|  |     ''') | ||||||
|  | 
 | ||||||
|  |     result = testdir.runpytest() | ||||||
|  |     result.stdout.fnmatch_lines([ | ||||||
|  |         '{}::test_log_1 '.format(filename), | ||||||
|  |         '*WARNING*log message from test_log_1*', | ||||||
|  |         'PASSED *50%*', | ||||||
|  |         '{}::test_log_2 '.format(filename), | ||||||
|  |         '*WARNING*log message from test_log_2*', | ||||||
|  |         'PASSED *100%*', | ||||||
|  |         '=* 2 passed in *=', | ||||||
|  |     ]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_log_cli_default_level_sections(testdir, request): | ||||||
|  |     """Check that with live logging enable we are printing the correct headers during setup/call/teardown.""" | ||||||
|  |     filename = request.node.name + '.py' | ||||||
|  |     testdir.makepyfile(''' | ||||||
|  |         import pytest | ||||||
|  |         import logging | ||||||
|  | 
 | ||||||
|  |         @pytest.fixture | ||||||
|  |         def fix(request): | ||||||
|  |             logging.warning("log message from setup of {}".format(request.node.name)) | ||||||
|  |             yield | ||||||
|  |             logging.warning("log message from teardown of {}".format(request.node.name)) | ||||||
|  | 
 | ||||||
|  |         def test_log_1(fix): | ||||||
|  |             logging.warning("log message from test_log_1") | ||||||
|  | 
 | ||||||
|  |         def test_log_2(fix): | ||||||
|  |             logging.warning("log message from test_log_2") | ||||||
|  |     ''') | ||||||
|  |     testdir.makeini(''' | ||||||
|  |         [pytest] | ||||||
|  |         log_cli=true | ||||||
|  |     ''') | ||||||
|  | 
 | ||||||
|  |     result = testdir.runpytest() | ||||||
|  |     result.stdout.fnmatch_lines([ | ||||||
|  |         '{}::test_log_1 '.format(filename), | ||||||
|  |         '*-- live log setup --*', | ||||||
|  |         '*WARNING*log message from setup of test_log_1*', | ||||||
|  |         '*-- live log call --*', | ||||||
|  |         '*WARNING*log message from test_log_1*', | ||||||
|  |         'PASSED *50%*', | ||||||
|  |         '*-- live log teardown --*', | ||||||
|  |         '*WARNING*log message from teardown of test_log_1*', | ||||||
|  | 
 | ||||||
|  |         '{}::test_log_2 '.format(filename), | ||||||
|  |         '*-- live log setup --*', | ||||||
|  |         '*WARNING*log message from setup of test_log_2*', | ||||||
|  |         '*-- live log call --*', | ||||||
|  |         '*WARNING*log message from test_log_2*', | ||||||
|  |         'PASSED *100%*', | ||||||
|  |         '*-- live log teardown --*', | ||||||
|  |         '*WARNING*log message from teardown of test_log_2*', | ||||||
|  |         '=* 2 passed in *=', | ||||||
|  |     ]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def test_log_cli_level(testdir): | def test_log_cli_level(testdir): | ||||||
|     # Default log file level |     # Default log file level | ||||||
|     testdir.makepyfile(''' |     testdir.makepyfile(''' | ||||||
|  | @ -186,22 +284,19 @@ def test_log_cli_level(testdir): | ||||||
|             logging.getLogger('catchlog').info("This log message will be shown") |             logging.getLogger('catchlog').info("This log message will be shown") | ||||||
|             print('PASSED') |             print('PASSED') | ||||||
|     ''') |     ''') | ||||||
|  |     testdir.makeini(''' | ||||||
|  |         [pytest] | ||||||
|  |         log_cli=true | ||||||
|  |     ''') | ||||||
| 
 | 
 | ||||||
|     result = testdir.runpytest('-s', '--log-cli-level=INFO') |     result = testdir.runpytest('-s', '--log-cli-level=INFO') | ||||||
| 
 | 
 | ||||||
|     # fnmatch_lines does an assertion internally |     # fnmatch_lines does an assertion internally | ||||||
|     result.stdout.fnmatch_lines([ |     result.stdout.fnmatch_lines([ | ||||||
|         'test_log_cli_level.py PASSED', |         'test_log_cli_level.py*This log message will be shown', | ||||||
|  |         'PASSED',  # 'PASSED' on its own line because the log message prints a new line | ||||||
|     ]) |     ]) | ||||||
|     result.stderr.fnmatch_lines([ |     assert "This log message won't be shown" not in result.stdout.str() | ||||||
|         "* This log message will be shown" |  | ||||||
|     ]) |  | ||||||
|     for line in result.errlines: |  | ||||||
|         try: |  | ||||||
|             assert "This log message won't be shown" in line |  | ||||||
|             pytest.fail("A log message was shown and it shouldn't have been") |  | ||||||
|         except AssertionError: |  | ||||||
|             continue |  | ||||||
| 
 | 
 | ||||||
|     # make sure that that we get a '0' exit code for the testsuite |     # make sure that that we get a '0' exit code for the testsuite | ||||||
|     assert result.ret == 0 |     assert result.ret == 0 | ||||||
|  | @ -210,17 +305,10 @@ def test_log_cli_level(testdir): | ||||||
| 
 | 
 | ||||||
|     # fnmatch_lines does an assertion internally |     # fnmatch_lines does an assertion internally | ||||||
|     result.stdout.fnmatch_lines([ |     result.stdout.fnmatch_lines([ | ||||||
|         'test_log_cli_level.py PASSED', |         'test_log_cli_level.py* This log message will be shown', | ||||||
|  |         'PASSED',  # 'PASSED' on its own line because the log message prints a new line | ||||||
|     ]) |     ]) | ||||||
|     result.stderr.fnmatch_lines([ |     assert "This log message won't be shown" not in result.stdout.str() | ||||||
|         "* This log message will be shown" |  | ||||||
|     ]) |  | ||||||
|     for line in result.errlines: |  | ||||||
|         try: |  | ||||||
|             assert "This log message won't be shown" in line |  | ||||||
|             pytest.fail("A log message was shown and it shouldn't have been") |  | ||||||
|         except AssertionError: |  | ||||||
|             continue |  | ||||||
| 
 | 
 | ||||||
|     # make sure that that we get a '0' exit code for the testsuite |     # make sure that that we get a '0' exit code for the testsuite | ||||||
|     assert result.ret == 0 |     assert result.ret == 0 | ||||||
|  | @ -230,6 +318,7 @@ def test_log_cli_ini_level(testdir): | ||||||
|     testdir.makeini( |     testdir.makeini( | ||||||
|         """ |         """ | ||||||
|         [pytest] |         [pytest] | ||||||
|  |         log_cli=true | ||||||
|         log_cli_level = INFO |         log_cli_level = INFO | ||||||
|         """) |         """) | ||||||
|     testdir.makepyfile(''' |     testdir.makepyfile(''' | ||||||
|  | @ -247,17 +336,10 @@ def test_log_cli_ini_level(testdir): | ||||||
| 
 | 
 | ||||||
|     # fnmatch_lines does an assertion internally |     # fnmatch_lines does an assertion internally | ||||||
|     result.stdout.fnmatch_lines([ |     result.stdout.fnmatch_lines([ | ||||||
|         'test_log_cli_ini_level.py PASSED', |         'test_log_cli_ini_level.py* This log message will be shown', | ||||||
|  |         'PASSED',  # 'PASSED' on its own line because the log message prints a new line | ||||||
|     ]) |     ]) | ||||||
|     result.stderr.fnmatch_lines([ |     assert "This log message won't be shown" not in result.stdout.str() | ||||||
|         "* This log message will be shown" |  | ||||||
|     ]) |  | ||||||
|     for line in result.errlines: |  | ||||||
|         try: |  | ||||||
|             assert "This log message won't be shown" in line |  | ||||||
|             pytest.fail("A log message was shown and it shouldn't have been") |  | ||||||
|         except AssertionError: |  | ||||||
|             continue |  | ||||||
| 
 | 
 | ||||||
|     # make sure that that we get a '0' exit code for the testsuite |     # make sure that that we get a '0' exit code for the testsuite | ||||||
|     assert result.ret == 0 |     assert result.ret == 0 | ||||||
|  | @ -278,7 +360,7 @@ def test_log_file_cli(testdir): | ||||||
| 
 | 
 | ||||||
|     log_file = testdir.tmpdir.join('pytest.log').strpath |     log_file = testdir.tmpdir.join('pytest.log').strpath | ||||||
| 
 | 
 | ||||||
|     result = testdir.runpytest('-s', '--log-file={0}'.format(log_file)) |     result = testdir.runpytest('-s', '--log-file={0}'.format(log_file), '--log-file-level=WARNING') | ||||||
| 
 | 
 | ||||||
|     # fnmatch_lines does an assertion internally |     # fnmatch_lines does an assertion internally | ||||||
|     result.stdout.fnmatch_lines([ |     result.stdout.fnmatch_lines([ | ||||||
|  | @ -327,6 +409,16 @@ def test_log_file_cli_level(testdir): | ||||||
|         assert "This log message won't be shown" not in contents |         assert "This log message won't be shown" not in contents | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def test_log_level_not_changed_by_default(testdir): | ||||||
|  |     testdir.makepyfile(''' | ||||||
|  |         import logging | ||||||
|  |         def test_log_file(): | ||||||
|  |             assert logging.getLogger().level == logging.WARNING | ||||||
|  |     ''') | ||||||
|  |     result = testdir.runpytest('-s') | ||||||
|  |     result.stdout.fnmatch_lines('* 1 passed in *') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def test_log_file_ini(testdir): | def test_log_file_ini(testdir): | ||||||
|     log_file = testdir.tmpdir.join('pytest.log').strpath |     log_file = testdir.tmpdir.join('pytest.log').strpath | ||||||
| 
 | 
 | ||||||
|  | @ -334,6 +426,7 @@ def test_log_file_ini(testdir): | ||||||
|         """ |         """ | ||||||
|         [pytest] |         [pytest] | ||||||
|         log_file={0} |         log_file={0} | ||||||
|  |         log_file_level=WARNING | ||||||
|         """.format(log_file)) |         """.format(log_file)) | ||||||
|     testdir.makepyfile(''' |     testdir.makepyfile(''' | ||||||
|         import pytest |         import pytest | ||||||
|  | @ -396,3 +489,53 @@ def test_log_file_ini_level(testdir): | ||||||
|         contents = rfh.read() |         contents = rfh.read() | ||||||
|         assert "This log message will be shown" in contents |         assert "This log message will be shown" in contents | ||||||
|         assert "This log message won't be shown" not in contents |         assert "This log message won't be shown" not in contents | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @pytest.mark.parametrize('has_capture_manager', [True, False]) | ||||||
|  | def test_live_logging_suspends_capture(has_capture_manager, request): | ||||||
|  |     """Test that capture manager is suspended when we emitting messages for live logging. | ||||||
|  | 
 | ||||||
|  |     This tests the implementation calls instead of behavior because it is difficult/impossible to do it using | ||||||
|  |     ``testdir`` facilities because they do their own capturing. | ||||||
|  | 
 | ||||||
|  |     We parametrize the test to also make sure _LiveLoggingStreamHandler works correctly if no capture manager plugin | ||||||
|  |     is installed. | ||||||
|  |     """ | ||||||
|  |     import logging | ||||||
|  |     from functools import partial | ||||||
|  |     from _pytest.capture import CaptureManager | ||||||
|  |     from _pytest.logging import _LiveLoggingStreamHandler | ||||||
|  | 
 | ||||||
|  |     class MockCaptureManager: | ||||||
|  |         calls = [] | ||||||
|  | 
 | ||||||
|  |         def suspend_global_capture(self): | ||||||
|  |             self.calls.append('suspend_global_capture') | ||||||
|  | 
 | ||||||
|  |         def resume_global_capture(self): | ||||||
|  |             self.calls.append('resume_global_capture') | ||||||
|  | 
 | ||||||
|  |     # sanity check | ||||||
|  |     assert CaptureManager.suspend_capture_item | ||||||
|  |     assert CaptureManager.resume_global_capture | ||||||
|  | 
 | ||||||
|  |     class DummyTerminal(six.StringIO): | ||||||
|  | 
 | ||||||
|  |         def section(self, *args, **kwargs): | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  |     out_file = DummyTerminal() | ||||||
|  |     capture_manager = MockCaptureManager() if has_capture_manager else None | ||||||
|  |     handler = _LiveLoggingStreamHandler(out_file, capture_manager) | ||||||
|  |     handler.set_when('call') | ||||||
|  | 
 | ||||||
|  |     logger = logging.getLogger(__name__ + '.test_live_logging_suspends_capture') | ||||||
|  |     logger.addHandler(handler) | ||||||
|  |     request.addfinalizer(partial(logger.removeHandler, handler)) | ||||||
|  | 
 | ||||||
|  |     logger.critical('some message') | ||||||
|  |     if has_capture_manager: | ||||||
|  |         assert MockCaptureManager.calls == ['suspend_global_capture', 'resume_global_capture'] | ||||||
|  |     else: | ||||||
|  |         assert MockCaptureManager.calls == [] | ||||||
|  |     assert out_file.getvalue() == '\nsome message\n' | ||||||
|  |  | ||||||
|  | @ -5,11 +5,8 @@ from textwrap import dedent | ||||||
| 
 | 
 | ||||||
| import _pytest._code | import _pytest._code | ||||||
| import pytest | import pytest | ||||||
| from _pytest.main import ( | from _pytest.main import EXIT_NOTESTSCOLLECTED | ||||||
|     Collector, | from _pytest.nodes import Collector | ||||||
|     EXIT_NOTESTSCOLLECTED |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| ignore_parametrized_marks = pytest.mark.filterwarnings('ignore:Applying marks directly to parameters') | ignore_parametrized_marks = pytest.mark.filterwarnings('ignore:Applying marks directly to parameters') | ||||||
| 
 | 
 | ||||||
|  | @ -882,10 +879,10 @@ class TestConftestCustomization(object): | ||||||
|             import sys, os, imp |             import sys, os, imp | ||||||
|             from _pytest.python import Module |             from _pytest.python import Module | ||||||
| 
 | 
 | ||||||
|             class Loader: |             class Loader(object): | ||||||
|                 def load_module(self, name): |                 def load_module(self, name): | ||||||
|                     return imp.load_source(name, name + ".narf") |                     return imp.load_source(name, name + ".narf") | ||||||
|             class Finder: |             class Finder(object): | ||||||
|                 def find_module(self, name, path=None): |                 def find_module(self, name, path=None): | ||||||
|                     if os.path.exists(name + ".narf"): |                     if os.path.exists(name + ".narf"): | ||||||
|                         return Loader() |                         return Loader() | ||||||
|  |  | ||||||
|  | @ -2828,7 +2828,7 @@ class TestShowFixtures(object): | ||||||
|     def test_show_fixtures_indented_in_class(self, testdir): |     def test_show_fixtures_indented_in_class(self, testdir): | ||||||
|         p = testdir.makepyfile(dedent(''' |         p = testdir.makepyfile(dedent(''' | ||||||
|             import pytest |             import pytest | ||||||
|             class TestClass: |             class TestClass(object): | ||||||
|                 @pytest.fixture |                 @pytest.fixture | ||||||
|                 def fixture1(self): |                 def fixture1(self): | ||||||
|                     """line1 |                     """line1 | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ PY3 = sys.version_info >= (3, 0) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestMetafunc(object): | class TestMetafunc(object): | ||||||
|     def Metafunc(self, func): |     def Metafunc(self, func, config=None): | ||||||
|         # the unit tests of this class check if things work correctly |         # the unit tests of this class check if things work correctly | ||||||
|         # on the funcarg level, so we don't need a full blown |         # on the funcarg level, so we don't need a full blown | ||||||
|         # initiliazation |         # initiliazation | ||||||
|  | @ -26,7 +26,7 @@ class TestMetafunc(object): | ||||||
| 
 | 
 | ||||||
|         names = fixtures.getfuncargnames(func) |         names = fixtures.getfuncargnames(func) | ||||||
|         fixtureinfo = FixtureInfo(names) |         fixtureinfo = FixtureInfo(names) | ||||||
|         return python.Metafunc(func, fixtureinfo, None) |         return python.Metafunc(func, fixtureinfo, config) | ||||||
| 
 | 
 | ||||||
|     def test_no_funcargs(self, testdir): |     def test_no_funcargs(self, testdir): | ||||||
|         def function(): |         def function(): | ||||||
|  | @ -156,7 +156,19 @@ class TestMetafunc(object): | ||||||
|     def test_parametrize_empty_list(self): |     def test_parametrize_empty_list(self): | ||||||
|         def func(y): |         def func(y): | ||||||
|             pass |             pass | ||||||
|         metafunc = self.Metafunc(func) | 
 | ||||||
|  |         class MockConfig(object): | ||||||
|  |             def getini(self, name): | ||||||
|  |                 return '' | ||||||
|  | 
 | ||||||
|  |             @property | ||||||
|  |             def hook(self): | ||||||
|  |                 return self | ||||||
|  | 
 | ||||||
|  |             def pytest_make_parametrize_id(self, **kw): | ||||||
|  |                 pass | ||||||
|  | 
 | ||||||
|  |         metafunc = self.Metafunc(func, MockConfig()) | ||||||
|         metafunc.parametrize("y", []) |         metafunc.parametrize("y", []) | ||||||
|         assert 'skip' == metafunc._calls[0].marks[0].name |         assert 'skip' == metafunc._calls[0].marks[0].name | ||||||
| 
 | 
 | ||||||
|  | @ -235,6 +247,25 @@ class TestMetafunc(object): | ||||||
|         for val, expected in values: |         for val, expected in values: | ||||||
|             assert _idval(val, 'a', 6, None) == expected |             assert _idval(val, 'a', 6, None) == expected | ||||||
| 
 | 
 | ||||||
|  |     def test_class_or_function_idval(self): | ||||||
|  |         """unittest for the expected behavior to obtain ids for parametrized | ||||||
|  |         values that are classes or functions: their __name__. | ||||||
|  |         """ | ||||||
|  |         from _pytest.python import _idval | ||||||
|  | 
 | ||||||
|  |         class TestClass(object): | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  |         def test_function(): | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  |         values = [ | ||||||
|  |             (TestClass, "TestClass"), | ||||||
|  |             (test_function, "test_function"), | ||||||
|  |         ] | ||||||
|  |         for val, expected in values: | ||||||
|  |             assert _idval(val, 'a', 6, None) == expected | ||||||
|  | 
 | ||||||
|     @pytest.mark.issue250 |     @pytest.mark.issue250 | ||||||
|     def test_idmaker_autoname(self): |     def test_idmaker_autoname(self): | ||||||
|         from _pytest.python import idmaker |         from _pytest.python import idmaker | ||||||
|  |  | ||||||
|  | @ -31,7 +31,7 @@ class TestNewAPI(object): | ||||||
| 
 | 
 | ||||||
|     def test_cache_writefail_cachfile_silent(self, testdir): |     def test_cache_writefail_cachfile_silent(self, testdir): | ||||||
|         testdir.makeini("[pytest]") |         testdir.makeini("[pytest]") | ||||||
|         testdir.tmpdir.join('.cache').write('gone wrong') |         testdir.tmpdir.join('.pytest_cache').write('gone wrong') | ||||||
|         config = testdir.parseconfigure() |         config = testdir.parseconfigure() | ||||||
|         cache = config.cache |         cache = config.cache | ||||||
|         cache.set('test/broken', []) |         cache.set('test/broken', []) | ||||||
|  | @ -39,14 +39,14 @@ class TestNewAPI(object): | ||||||
|     @pytest.mark.skipif(sys.platform.startswith('win'), reason='no chmod on windows') |     @pytest.mark.skipif(sys.platform.startswith('win'), reason='no chmod on windows') | ||||||
|     def test_cache_writefail_permissions(self, testdir): |     def test_cache_writefail_permissions(self, testdir): | ||||||
|         testdir.makeini("[pytest]") |         testdir.makeini("[pytest]") | ||||||
|         testdir.tmpdir.ensure_dir('.cache').chmod(0) |         testdir.tmpdir.ensure_dir('.pytest_cache').chmod(0) | ||||||
|         config = testdir.parseconfigure() |         config = testdir.parseconfigure() | ||||||
|         cache = config.cache |         cache = config.cache | ||||||
|         cache.set('test/broken', []) |         cache.set('test/broken', []) | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.skipif(sys.platform.startswith('win'), reason='no chmod on windows') |     @pytest.mark.skipif(sys.platform.startswith('win'), reason='no chmod on windows') | ||||||
|     def test_cache_failure_warns(self, testdir): |     def test_cache_failure_warns(self, testdir): | ||||||
|         testdir.tmpdir.ensure_dir('.cache').chmod(0) |         testdir.tmpdir.ensure_dir('.pytest_cache').chmod(0) | ||||||
|         testdir.makepyfile(""" |         testdir.makepyfile(""" | ||||||
|             def test_error(): |             def test_error(): | ||||||
|                 raise Exception |                 raise Exception | ||||||
|  | @ -127,7 +127,7 @@ def test_cache_reportheader(testdir): | ||||||
|     """) |     """) | ||||||
|     result = testdir.runpytest("-v") |     result = testdir.runpytest("-v") | ||||||
|     result.stdout.fnmatch_lines([ |     result.stdout.fnmatch_lines([ | ||||||
|         "cachedir: .cache" |         "cachedir: .pytest_cache" | ||||||
|     ]) |     ]) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -201,8 +201,8 @@ class TestLastFailed(object): | ||||||
|         ]) |         ]) | ||||||
| 
 | 
 | ||||||
|         # Run this again to make sure clear-cache is robust |         # Run this again to make sure clear-cache is robust | ||||||
|         if os.path.isdir('.cache'): |         if os.path.isdir('.pytest_cache'): | ||||||
|             shutil.rmtree('.cache') |             shutil.rmtree('.pytest_cache') | ||||||
|         result = testdir.runpytest("--lf", "--cache-clear") |         result = testdir.runpytest("--lf", "--cache-clear") | ||||||
|         result.stdout.fnmatch_lines([ |         result.stdout.fnmatch_lines([ | ||||||
|             "*1 failed*2 passed*", |             "*1 failed*2 passed*", | ||||||
|  | @ -495,15 +495,15 @@ class TestLastFailed(object): | ||||||
|         # Issue #1342 |         # Issue #1342 | ||||||
|         testdir.makepyfile(test_empty='') |         testdir.makepyfile(test_empty='') | ||||||
|         testdir.runpytest('-q', '--lf') |         testdir.runpytest('-q', '--lf') | ||||||
|         assert not os.path.exists('.cache') |         assert not os.path.exists('.pytest_cache') | ||||||
| 
 | 
 | ||||||
|         testdir.makepyfile(test_successful='def test_success():\n    assert True') |         testdir.makepyfile(test_successful='def test_success():\n    assert True') | ||||||
|         testdir.runpytest('-q', '--lf') |         testdir.runpytest('-q', '--lf') | ||||||
|         assert not os.path.exists('.cache') |         assert not os.path.exists('.pytest_cache') | ||||||
| 
 | 
 | ||||||
|         testdir.makepyfile(test_errored='def test_error():\n    assert False') |         testdir.makepyfile(test_errored='def test_error():\n    assert False') | ||||||
|         testdir.runpytest('-q', '--lf') |         testdir.runpytest('-q', '--lf') | ||||||
|         assert os.path.exists('.cache') |         assert os.path.exists('.pytest_cache') | ||||||
| 
 | 
 | ||||||
|     def test_xfail_not_considered_failure(self, testdir): |     def test_xfail_not_considered_failure(self, testdir): | ||||||
|         testdir.makepyfile(''' |         testdir.makepyfile(''' | ||||||
|  | @ -1245,7 +1245,7 @@ def test_py36_windowsconsoleio_workaround_non_standard_streams(): | ||||||
|     """ |     """ | ||||||
|     from _pytest.capture import _py36_windowsconsoleio_workaround |     from _pytest.capture import _py36_windowsconsoleio_workaround | ||||||
| 
 | 
 | ||||||
|     class DummyStream: |     class DummyStream(object): | ||||||
|         def write(self, s): |         def write(self, s): | ||||||
|             pass |             pass | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -781,16 +781,18 @@ class TestOverrideIniArgs(object): | ||||||
|         testdir.makeini(""" |         testdir.makeini(""" | ||||||
|             [pytest] |             [pytest] | ||||||
|             custom_option_1=custom_option_1 |             custom_option_1=custom_option_1 | ||||||
|             custom_option_2=custom_option_2""") |             custom_option_2=custom_option_2 | ||||||
|  |         """) | ||||||
|         testdir.makepyfile(""" |         testdir.makepyfile(""" | ||||||
|             def test_multiple_options(pytestconfig): |             def test_multiple_options(pytestconfig): | ||||||
|                 prefix = "custom_option" |                 prefix = "custom_option" | ||||||
|                 for x in range(1, 5): |                 for x in range(1, 5): | ||||||
|                     ini_value=pytestconfig.getini("%s_%d" % (prefix, x)) |                     ini_value=pytestconfig.getini("%s_%d" % (prefix, x)) | ||||||
|                     print('\\nini%d:%s' % (x, ini_value))""") |                     print('\\nini%d:%s' % (x, ini_value)) | ||||||
|  |         """) | ||||||
|         result = testdir.runpytest( |         result = testdir.runpytest( | ||||||
|             "--override-ini", 'custom_option_1=fulldir=/tmp/user1', |             "--override-ini", 'custom_option_1=fulldir=/tmp/user1', | ||||||
|             'custom_option_2=url=/tmp/user2?a=b&d=e', |             '-o', 'custom_option_2=url=/tmp/user2?a=b&d=e', | ||||||
|             "-o", 'custom_option_3=True', |             "-o", 'custom_option_3=True', | ||||||
|             "-o", 'custom_option_4=no', "-s") |             "-o", 'custom_option_4=no', "-s") | ||||||
|         result.stdout.fnmatch_lines(["ini1:fulldir=/tmp/user1", |         result.stdout.fnmatch_lines(["ini1:fulldir=/tmp/user1", | ||||||
|  | @ -853,10 +855,42 @@ class TestOverrideIniArgs(object): | ||||||
|             assert rootdir == tmpdir |             assert rootdir == tmpdir | ||||||
|             assert inifile is None |             assert inifile is None | ||||||
| 
 | 
 | ||||||
|     def test_addopts_before_initini(self, testdir, tmpdir, monkeypatch): |     def test_addopts_before_initini(self, monkeypatch): | ||||||
|         cache_dir = '.custom_cache' |         cache_dir = '.custom_cache' | ||||||
|         monkeypatch.setenv('PYTEST_ADDOPTS', '-o cache_dir=%s' % cache_dir) |         monkeypatch.setenv('PYTEST_ADDOPTS', '-o cache_dir=%s' % cache_dir) | ||||||
|         from _pytest.config import get_config |         from _pytest.config import get_config | ||||||
|         config = get_config() |         config = get_config() | ||||||
|         config._preparse([], addopts=True) |         config._preparse([], addopts=True) | ||||||
|         assert config._override_ini == [['cache_dir=%s' % cache_dir]] |         assert config._override_ini == ['cache_dir=%s' % cache_dir] | ||||||
|  | 
 | ||||||
|  |     def test_override_ini_does_not_contain_paths(self): | ||||||
|  |         """Check that -o no longer swallows all options after it (#3103)""" | ||||||
|  |         from _pytest.config import get_config | ||||||
|  |         config = get_config() | ||||||
|  |         config._preparse(['-o', 'cache_dir=/cache', '/some/test/path']) | ||||||
|  |         assert config._override_ini == ['cache_dir=/cache'] | ||||||
|  | 
 | ||||||
|  |     def test_multiple_override_ini_options(self, testdir, request): | ||||||
|  |         """Ensure a file path following a '-o' option does not generate an error (#3103)""" | ||||||
|  |         testdir.makepyfile(**{ | ||||||
|  |             "conftest.py": """ | ||||||
|  |                 def pytest_addoption(parser): | ||||||
|  |                     parser.addini('foo', default=None, help='some option') | ||||||
|  |                     parser.addini('bar', default=None, help='some option') | ||||||
|  |             """, | ||||||
|  |             "test_foo.py": """ | ||||||
|  |                 def test(pytestconfig): | ||||||
|  |                     assert pytestconfig.getini('foo') == '1' | ||||||
|  |                     assert pytestconfig.getini('bar') == '0' | ||||||
|  |             """, | ||||||
|  |             "test_bar.py": """ | ||||||
|  |                 def test(): | ||||||
|  |                     assert False | ||||||
|  |             """, | ||||||
|  |         }) | ||||||
|  |         result = testdir.runpytest('-o', 'foo=1', '-o', 'bar=0', 'test_foo.py') | ||||||
|  |         assert 'ERROR:' not in result.stderr.str() | ||||||
|  |         result.stdout.fnmatch_lines([ | ||||||
|  |             'collected 1 item', | ||||||
|  |             '*= 1 passed in *=', | ||||||
|  |         ]) | ||||||
|  |  | ||||||
|  | @ -879,6 +879,27 @@ def test_record_property_same_name(testdir): | ||||||
|     pnodes[1].assert_attr(name="foo", value="baz") |     pnodes[1].assert_attr(name="foo", value="baz") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def test_record_attribute(testdir): | ||||||
|  |     testdir.makepyfile(""" | ||||||
|  |         import pytest | ||||||
|  | 
 | ||||||
|  |         @pytest.fixture | ||||||
|  |         def other(record_xml_attribute): | ||||||
|  |             record_xml_attribute("bar", 1) | ||||||
|  |         def test_record(record_xml_attribute, other): | ||||||
|  |             record_xml_attribute("foo", "<1"); | ||||||
|  |     """) | ||||||
|  |     result, dom = runandparse(testdir, '-rw') | ||||||
|  |     node = dom.find_first_by_tag("testsuite") | ||||||
|  |     tnode = node.find_first_by_tag("testcase") | ||||||
|  |     tnode.assert_attr(bar="1") | ||||||
|  |     tnode.assert_attr(foo="<1") | ||||||
|  |     result.stdout.fnmatch_lines([ | ||||||
|  |         'test_record_attribute.py::test_record', | ||||||
|  |         '*record_xml_attribute*experimental*', | ||||||
|  |     ]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def test_random_report_log_xdist(testdir): | def test_random_report_log_xdist(testdir): | ||||||
|     """xdist calls pytest_runtest_logreport as they are executed by the slaves, |     """xdist calls pytest_runtest_logreport as they are executed by the slaves, | ||||||
|     with nodes from several nodes overlapping, so junitxml must cope with that |     with nodes from several nodes overlapping, so junitxml must cope with that | ||||||
|  |  | ||||||
|  | @ -3,7 +3,10 @@ import os | ||||||
| import sys | import sys | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| from _pytest.mark import MarkGenerator as Mark, ParameterSet, transfer_markers | from _pytest.mark import ( | ||||||
|  |     MarkGenerator as Mark, ParameterSet, transfer_markers, | ||||||
|  |     EMPTY_PARAMETERSET_OPTION, | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestMark(object): | class TestMark(object): | ||||||
|  | @ -344,6 +347,21 @@ def test_keyword_option_parametrize(spec, testdir): | ||||||
|     assert list(passed) == list(passed_result) |     assert list(passed) == list(passed_result) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @pytest.mark.parametrize("spec", [ | ||||||
|  |     ("foo or import", "ERROR: Python keyword 'import' not accepted in expressions passed to '-k'"), | ||||||
|  |     ("foo or", "ERROR: Wrong expression passed to '-k': foo or") | ||||||
|  | ]) | ||||||
|  | def test_keyword_option_wrong_arguments(spec, testdir, capsys): | ||||||
|  |     testdir.makepyfile(""" | ||||||
|  |             def test_func(arg): | ||||||
|  |                 pass | ||||||
|  |         """) | ||||||
|  |     opt, expected_result = spec | ||||||
|  |     testdir.inline_run("-k", opt) | ||||||
|  |     out = capsys.readouterr().err | ||||||
|  |     assert expected_result in out | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def test_parametrized_collected_from_command_line(testdir): | def test_parametrized_collected_from_command_line(testdir): | ||||||
|     """Parametrized test not collected if test named specified |     """Parametrized test not collected if test named specified | ||||||
|        in command line issue#649. |        in command line issue#649. | ||||||
|  | @ -876,3 +894,27 @@ class TestMarkDecorator(object): | ||||||
|     ]) |     ]) | ||||||
|     def test__eq__(self, lhs, rhs, expected): |     def test__eq__(self, lhs, rhs, expected): | ||||||
|         assert (lhs == rhs) == expected |         assert (lhs == rhs) == expected | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @pytest.mark.parametrize('mark', [None, '', 'skip', 'xfail']) | ||||||
|  | def test_parameterset_for_parametrize_marks(testdir, mark): | ||||||
|  |     if mark is not None: | ||||||
|  |         testdir.makeini( | ||||||
|  |             "[pytest]\n{}={}".format(EMPTY_PARAMETERSET_OPTION, mark)) | ||||||
|  | 
 | ||||||
|  |     config = testdir.parseconfig() | ||||||
|  |     from _pytest.mark import pytest_configure, get_empty_parameterset_mark | ||||||
|  |     pytest_configure(config) | ||||||
|  |     result_mark = get_empty_parameterset_mark(config, ['a'], all) | ||||||
|  |     if mark in (None, ''): | ||||||
|  |         # normalize to the requested name | ||||||
|  |         mark = 'skip' | ||||||
|  |     assert result_mark.name == mark | ||||||
|  |     assert result_mark.kwargs['reason'].startswith("got empty parameter set ") | ||||||
|  |     if mark == 'xfail': | ||||||
|  |         assert result_mark.kwargs.get('run') is False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_parameterset_for_parametrize_bad_markname(testdir): | ||||||
|  |     with pytest.raises(pytest.UsageError): | ||||||
|  |         test_parameterset_for_parametrize_marks(testdir, 'bad') | ||||||
|  |  | ||||||
|  | @ -1,8 +1,12 @@ | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| from __future__ import absolute_import, division, print_function | from __future__ import absolute_import, division, print_function | ||||||
| import pytest |  | ||||||
| import os | import os | ||||||
|  | import py.path | ||||||
|  | import pytest | ||||||
|  | import sys | ||||||
|  | import _pytest.pytester as pytester | ||||||
| from _pytest.pytester import HookRecorder | from _pytest.pytester import HookRecorder | ||||||
|  | from _pytest.pytester import CwdSnapshot, SysModulesSnapshot, SysPathsSnapshot | ||||||
| from _pytest.config import PytestPluginManager | from _pytest.config import PytestPluginManager | ||||||
| from _pytest.main import EXIT_OK, EXIT_TESTSFAILED | from _pytest.main import EXIT_OK, EXIT_TESTSFAILED | ||||||
| 
 | 
 | ||||||
|  | @ -131,14 +135,116 @@ def test_makepyfile_utf8(testdir): | ||||||
|     assert u"mixed_encoding = u'São Paulo'".encode('utf-8') in p.read('rb') |     assert u"mixed_encoding = u'São Paulo'".encode('utf-8') in p.read('rb') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_inline_run_clean_modules(testdir): | class TestInlineRunModulesCleanup(object): | ||||||
|     test_mod = testdir.makepyfile("def test_foo(): assert True") |     def test_inline_run_test_module_not_cleaned_up(self, testdir): | ||||||
|     result = testdir.inline_run(str(test_mod)) |         test_mod = testdir.makepyfile("def test_foo(): assert True") | ||||||
|     assert result.ret == EXIT_OK |         result = testdir.inline_run(str(test_mod)) | ||||||
|     # rewrite module, now test should fail if module was re-imported |         assert result.ret == EXIT_OK | ||||||
|     test_mod.write("def test_foo(): assert False") |         # rewrite module, now test should fail if module was re-imported | ||||||
|     result2 = testdir.inline_run(str(test_mod)) |         test_mod.write("def test_foo(): assert False") | ||||||
|     assert result2.ret == EXIT_TESTSFAILED |         result2 = testdir.inline_run(str(test_mod)) | ||||||
|  |         assert result2.ret == EXIT_TESTSFAILED | ||||||
|  | 
 | ||||||
|  |     def spy_factory(self): | ||||||
|  |         class SysModulesSnapshotSpy(object): | ||||||
|  |             instances = [] | ||||||
|  | 
 | ||||||
|  |             def __init__(self, preserve=None): | ||||||
|  |                 SysModulesSnapshotSpy.instances.append(self) | ||||||
|  |                 self._spy_restore_count = 0 | ||||||
|  |                 self._spy_preserve = preserve | ||||||
|  |                 self.__snapshot = SysModulesSnapshot(preserve=preserve) | ||||||
|  | 
 | ||||||
|  |             def restore(self): | ||||||
|  |                 self._spy_restore_count += 1 | ||||||
|  |                 return self.__snapshot.restore() | ||||||
|  |         return SysModulesSnapshotSpy | ||||||
|  | 
 | ||||||
|  |     def test_inline_run_taking_and_restoring_a_sys_modules_snapshot( | ||||||
|  |             self, testdir, monkeypatch): | ||||||
|  |         spy_factory = self.spy_factory() | ||||||
|  |         monkeypatch.setattr(pytester, "SysModulesSnapshot", spy_factory) | ||||||
|  |         original = dict(sys.modules) | ||||||
|  |         testdir.syspathinsert() | ||||||
|  |         testdir.makepyfile(import1="# you son of a silly person") | ||||||
|  |         testdir.makepyfile(import2="# my hovercraft is full of eels") | ||||||
|  |         test_mod = testdir.makepyfile(""" | ||||||
|  |             import import1 | ||||||
|  |             def test_foo(): import import2""") | ||||||
|  |         testdir.inline_run(str(test_mod)) | ||||||
|  |         assert len(spy_factory.instances) == 1 | ||||||
|  |         spy = spy_factory.instances[0] | ||||||
|  |         assert spy._spy_restore_count == 1 | ||||||
|  |         assert sys.modules == original | ||||||
|  |         assert all(sys.modules[x] is original[x] for x in sys.modules) | ||||||
|  | 
 | ||||||
|  |     def test_inline_run_sys_modules_snapshot_restore_preserving_modules( | ||||||
|  |             self, testdir, monkeypatch): | ||||||
|  |         spy_factory = self.spy_factory() | ||||||
|  |         monkeypatch.setattr(pytester, "SysModulesSnapshot", spy_factory) | ||||||
|  |         test_mod = testdir.makepyfile("def test_foo(): pass") | ||||||
|  |         testdir.inline_run(str(test_mod)) | ||||||
|  |         spy = spy_factory.instances[0] | ||||||
|  |         assert not spy._spy_preserve("black_knight") | ||||||
|  |         assert spy._spy_preserve("zope") | ||||||
|  |         assert spy._spy_preserve("zope.interface") | ||||||
|  |         assert spy._spy_preserve("zopelicious") | ||||||
|  | 
 | ||||||
|  |     def test_external_test_module_imports_not_cleaned_up(self, testdir): | ||||||
|  |         testdir.syspathinsert() | ||||||
|  |         testdir.makepyfile(imported="data = 'you son of a silly person'") | ||||||
|  |         import imported | ||||||
|  |         test_mod = testdir.makepyfile(""" | ||||||
|  |             def test_foo(): | ||||||
|  |                 import imported | ||||||
|  |                 imported.data = 42""") | ||||||
|  |         testdir.inline_run(str(test_mod)) | ||||||
|  |         assert imported.data == 42 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_inline_run_clean_sys_paths(testdir): | ||||||
|  |     def test_sys_path_change_cleanup(self, testdir): | ||||||
|  |         test_path1 = testdir.tmpdir.join("boink1").strpath | ||||||
|  |         test_path2 = testdir.tmpdir.join("boink2").strpath | ||||||
|  |         test_path3 = testdir.tmpdir.join("boink3").strpath | ||||||
|  |         sys.path.append(test_path1) | ||||||
|  |         sys.meta_path.append(test_path1) | ||||||
|  |         original_path = list(sys.path) | ||||||
|  |         original_meta_path = list(sys.meta_path) | ||||||
|  |         test_mod = testdir.makepyfile(""" | ||||||
|  |             import sys | ||||||
|  |             sys.path.append({:test_path2}) | ||||||
|  |             sys.meta_path.append({:test_path2}) | ||||||
|  |             def test_foo(): | ||||||
|  |                 sys.path.append({:test_path3}) | ||||||
|  |                 sys.meta_path.append({:test_path3})""".format(locals())) | ||||||
|  |         testdir.inline_run(str(test_mod)) | ||||||
|  |         assert sys.path == original_path | ||||||
|  |         assert sys.meta_path == original_meta_path | ||||||
|  | 
 | ||||||
|  |     def spy_factory(self): | ||||||
|  |         class SysPathsSnapshotSpy(object): | ||||||
|  |             instances = [] | ||||||
|  | 
 | ||||||
|  |             def __init__(self): | ||||||
|  |                 SysPathsSnapshotSpy.instances.append(self) | ||||||
|  |                 self._spy_restore_count = 0 | ||||||
|  |                 self.__snapshot = SysPathsSnapshot() | ||||||
|  | 
 | ||||||
|  |             def restore(self): | ||||||
|  |                 self._spy_restore_count += 1 | ||||||
|  |                 return self.__snapshot.restore() | ||||||
|  |         return SysPathsSnapshotSpy | ||||||
|  | 
 | ||||||
|  |     def test_inline_run_taking_and_restoring_a_sys_paths_snapshot( | ||||||
|  |             self, testdir, monkeypatch): | ||||||
|  |         spy_factory = self.spy_factory() | ||||||
|  |         monkeypatch.setattr(pytester, "SysPathsSnapshot", spy_factory) | ||||||
|  |         test_mod = testdir.makepyfile("def test_foo(): pass") | ||||||
|  |         testdir.inline_run(str(test_mod)) | ||||||
|  |         assert len(spy_factory.instances) == 1 | ||||||
|  |         spy = spy_factory.instances[0] | ||||||
|  |         assert spy._spy_restore_count == 1 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_assert_outcomes_after_pytest_error(testdir): | def test_assert_outcomes_after_pytest_error(testdir): | ||||||
|  | @ -147,3 +253,126 @@ def test_assert_outcomes_after_pytest_error(testdir): | ||||||
|     result = testdir.runpytest('--unexpected-argument') |     result = testdir.runpytest('--unexpected-argument') | ||||||
|     with pytest.raises(ValueError, message="Pytest terminal report not found"): |     with pytest.raises(ValueError, message="Pytest terminal report not found"): | ||||||
|         result.assert_outcomes(passed=0) |         result.assert_outcomes(passed=0) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_cwd_snapshot(tmpdir): | ||||||
|  |     foo = tmpdir.ensure('foo', dir=1) | ||||||
|  |     bar = tmpdir.ensure('bar', dir=1) | ||||||
|  |     foo.chdir() | ||||||
|  |     snapshot = CwdSnapshot() | ||||||
|  |     bar.chdir() | ||||||
|  |     assert py.path.local() == bar | ||||||
|  |     snapshot.restore() | ||||||
|  |     assert py.path.local() == foo | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TestSysModulesSnapshot(object): | ||||||
|  |     key = 'my-test-module' | ||||||
|  | 
 | ||||||
|  |     def test_remove_added(self): | ||||||
|  |         original = dict(sys.modules) | ||||||
|  |         assert self.key not in sys.modules | ||||||
|  |         snapshot = SysModulesSnapshot() | ||||||
|  |         sys.modules[self.key] = 'something' | ||||||
|  |         assert self.key in sys.modules | ||||||
|  |         snapshot.restore() | ||||||
|  |         assert sys.modules == original | ||||||
|  | 
 | ||||||
|  |     def test_add_removed(self, monkeypatch): | ||||||
|  |         assert self.key not in sys.modules | ||||||
|  |         monkeypatch.setitem(sys.modules, self.key, 'something') | ||||||
|  |         assert self.key in sys.modules | ||||||
|  |         original = dict(sys.modules) | ||||||
|  |         snapshot = SysModulesSnapshot() | ||||||
|  |         del sys.modules[self.key] | ||||||
|  |         assert self.key not in sys.modules | ||||||
|  |         snapshot.restore() | ||||||
|  |         assert sys.modules == original | ||||||
|  | 
 | ||||||
|  |     def test_restore_reloaded(self, monkeypatch): | ||||||
|  |         assert self.key not in sys.modules | ||||||
|  |         monkeypatch.setitem(sys.modules, self.key, 'something') | ||||||
|  |         assert self.key in sys.modules | ||||||
|  |         original = dict(sys.modules) | ||||||
|  |         snapshot = SysModulesSnapshot() | ||||||
|  |         sys.modules[self.key] = 'something else' | ||||||
|  |         snapshot.restore() | ||||||
|  |         assert sys.modules == original | ||||||
|  | 
 | ||||||
|  |     def test_preserve_modules(self, monkeypatch): | ||||||
|  |         key = [self.key + str(i) for i in range(3)] | ||||||
|  |         assert not any(k in sys.modules for k in key) | ||||||
|  |         for i, k in enumerate(key): | ||||||
|  |             monkeypatch.setitem(sys.modules, k, 'something' + str(i)) | ||||||
|  |         original = dict(sys.modules) | ||||||
|  | 
 | ||||||
|  |         def preserve(name): | ||||||
|  |             return name in (key[0], key[1], 'some-other-key') | ||||||
|  | 
 | ||||||
|  |         snapshot = SysModulesSnapshot(preserve=preserve) | ||||||
|  |         sys.modules[key[0]] = original[key[0]] = 'something else0' | ||||||
|  |         sys.modules[key[1]] = original[key[1]] = 'something else1' | ||||||
|  |         sys.modules[key[2]] = 'something else2' | ||||||
|  |         snapshot.restore() | ||||||
|  |         assert sys.modules == original | ||||||
|  | 
 | ||||||
|  |     def test_preserve_container(self, monkeypatch): | ||||||
|  |         original = dict(sys.modules) | ||||||
|  |         assert self.key not in original | ||||||
|  |         replacement = dict(sys.modules) | ||||||
|  |         replacement[self.key] = 'life of brian' | ||||||
|  |         snapshot = SysModulesSnapshot() | ||||||
|  |         monkeypatch.setattr(sys, 'modules', replacement) | ||||||
|  |         snapshot.restore() | ||||||
|  |         assert sys.modules is replacement | ||||||
|  |         assert sys.modules == original | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @pytest.mark.parametrize('path_type', ('path', 'meta_path')) | ||||||
|  | class TestSysPathsSnapshot(object): | ||||||
|  |     other_path = { | ||||||
|  |         'path': 'meta_path', | ||||||
|  |         'meta_path': 'path'} | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def path(n): | ||||||
|  |         return 'my-dirty-little-secret-' + str(n) | ||||||
|  | 
 | ||||||
|  |     def test_restore(self, monkeypatch, path_type): | ||||||
|  |         other_path_type = self.other_path[path_type] | ||||||
|  |         for i in range(10): | ||||||
|  |             assert self.path(i) not in getattr(sys, path_type) | ||||||
|  |         sys_path = [self.path(i) for i in range(6)] | ||||||
|  |         monkeypatch.setattr(sys, path_type, sys_path) | ||||||
|  |         original = list(sys_path) | ||||||
|  |         original_other = list(getattr(sys, other_path_type)) | ||||||
|  |         snapshot = SysPathsSnapshot() | ||||||
|  |         transformation = { | ||||||
|  |             'source': (0, 1, 2,    3, 4, 5), | ||||||
|  |             'target': (   6, 2, 9, 7,    5, 8)}  # noqa: E201 | ||||||
|  |         assert sys_path == [self.path(x) for x in transformation['source']] | ||||||
|  |         sys_path[1] = self.path(6) | ||||||
|  |         sys_path[3] = self.path(7) | ||||||
|  |         sys_path.append(self.path(8)) | ||||||
|  |         del sys_path[4] | ||||||
|  |         sys_path[3:3] = [self.path(9)] | ||||||
|  |         del sys_path[0] | ||||||
|  |         assert sys_path == [self.path(x) for x in transformation['target']] | ||||||
|  |         snapshot.restore() | ||||||
|  |         assert getattr(sys, path_type) is sys_path | ||||||
|  |         assert getattr(sys, path_type) == original | ||||||
|  |         assert getattr(sys, other_path_type) == original_other | ||||||
|  | 
 | ||||||
|  |     def test_preserve_container(self, monkeypatch, path_type): | ||||||
|  |         other_path_type = self.other_path[path_type] | ||||||
|  |         original_data = list(getattr(sys, path_type)) | ||||||
|  |         original_other = getattr(sys, other_path_type) | ||||||
|  |         original_other_data = list(original_other) | ||||||
|  |         new = [] | ||||||
|  |         snapshot = SysPathsSnapshot() | ||||||
|  |         monkeypatch.setattr(sys, path_type, new) | ||||||
|  |         snapshot.restore() | ||||||
|  |         assert getattr(sys, path_type) is new | ||||||
|  |         assert getattr(sys, path_type) == original_data | ||||||
|  |         assert getattr(sys, other_path_type) is original_other | ||||||
|  |         assert getattr(sys, other_path_type) == original_other_data | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ import os | ||||||
| import _pytest._code | import _pytest._code | ||||||
| import py | import py | ||||||
| import pytest | import pytest | ||||||
| from _pytest.main import Node, Item, FSCollector | from _pytest.nodes import Node, Item, FSCollector | ||||||
| from _pytest.resultlog import generic_path, ResultLog, \ | from _pytest.resultlog import generic_path, ResultLog, \ | ||||||
|     pytest_configure, pytest_unconfigure |     pytest_configure, pytest_unconfigure | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -204,6 +204,18 @@ class BaseFunctionalTests(object): | ||||||
|         """) |         """) | ||||||
|         assert rec.ret == 1 |         assert rec.ret == 1 | ||||||
| 
 | 
 | ||||||
|  |     def test_logstart_logfinish_hooks(self, testdir): | ||||||
|  |         rec = testdir.inline_runsource(""" | ||||||
|  |             import pytest | ||||||
|  |             def test_func(): | ||||||
|  |                 pass | ||||||
|  |         """) | ||||||
|  |         reps = rec.getcalls("pytest_runtest_logstart pytest_runtest_logfinish") | ||||||
|  |         assert [x._name for x in reps] == ['pytest_runtest_logstart', 'pytest_runtest_logfinish'] | ||||||
|  |         for rep in reps: | ||||||
|  |             assert rep.nodeid == 'test_logstart_logfinish_hooks.py::test_func' | ||||||
|  |             assert rep.location == ('test_logstart_logfinish_hooks.py', 1, 'test_func') | ||||||
|  | 
 | ||||||
|     def test_exact_teardown_issue90(self, testdir): |     def test_exact_teardown_issue90(self, testdir): | ||||||
|         rec = testdir.inline_runsource(""" |         rec = testdir.inline_runsource(""" | ||||||
|             import pytest |             import pytest | ||||||
|  |  | ||||||
|  | @ -966,10 +966,10 @@ def test_no_trailing_whitespace_after_inifile_word(testdir): | ||||||
|     assert 'inifile: tox.ini\n' in result.stdout.str() |     assert 'inifile: tox.ini\n' in result.stdout.str() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestProgress: | class TestProgress(object): | ||||||
| 
 | 
 | ||||||
|     @pytest.fixture |     @pytest.fixture | ||||||
|     def many_tests_file(self, testdir): |     def many_tests_files(self, testdir): | ||||||
|         testdir.makepyfile( |         testdir.makepyfile( | ||||||
|             test_bar=""" |             test_bar=""" | ||||||
|                 import pytest |                 import pytest | ||||||
|  | @ -1006,7 +1006,7 @@ class TestProgress: | ||||||
|             '=* 2 passed in *=', |             '=* 2 passed in *=', | ||||||
|         ]) |         ]) | ||||||
| 
 | 
 | ||||||
|     def test_normal(self, many_tests_file, testdir): |     def test_normal(self, many_tests_files, testdir): | ||||||
|         output = testdir.runpytest() |         output = testdir.runpytest() | ||||||
|         output.stdout.re_match_lines([ |         output.stdout.re_match_lines([ | ||||||
|             r'test_bar.py \.{10} \s+ \[ 50%\]', |             r'test_bar.py \.{10} \s+ \[ 50%\]', | ||||||
|  | @ -1014,7 +1014,7 @@ class TestProgress: | ||||||
|             r'test_foobar.py \.{5} \s+ \[100%\]', |             r'test_foobar.py \.{5} \s+ \[100%\]', | ||||||
|         ]) |         ]) | ||||||
| 
 | 
 | ||||||
|     def test_verbose(self, many_tests_file, testdir): |     def test_verbose(self, many_tests_files, testdir): | ||||||
|         output = testdir.runpytest('-v') |         output = testdir.runpytest('-v') | ||||||
|         output.stdout.re_match_lines([ |         output.stdout.re_match_lines([ | ||||||
|             r'test_bar.py::test_bar\[0\] PASSED \s+ \[  5%\]', |             r'test_bar.py::test_bar\[0\] PASSED \s+ \[  5%\]', | ||||||
|  | @ -1022,14 +1022,14 @@ class TestProgress: | ||||||
|             r'test_foobar.py::test_foobar\[4\] PASSED \s+ \[100%\]', |             r'test_foobar.py::test_foobar\[4\] PASSED \s+ \[100%\]', | ||||||
|         ]) |         ]) | ||||||
| 
 | 
 | ||||||
|     def test_xdist_normal(self, many_tests_file, testdir): |     def test_xdist_normal(self, many_tests_files, testdir): | ||||||
|         pytest.importorskip('xdist') |         pytest.importorskip('xdist') | ||||||
|         output = testdir.runpytest('-n2') |         output = testdir.runpytest('-n2') | ||||||
|         output.stdout.re_match_lines([ |         output.stdout.re_match_lines([ | ||||||
|             r'\.{20} \s+ \[100%\]', |             r'\.{20} \s+ \[100%\]', | ||||||
|         ]) |         ]) | ||||||
| 
 | 
 | ||||||
|     def test_xdist_verbose(self, many_tests_file, testdir): |     def test_xdist_verbose(self, many_tests_files, testdir): | ||||||
|         pytest.importorskip('xdist') |         pytest.importorskip('xdist') | ||||||
|         output = testdir.runpytest('-n2', '-v') |         output = testdir.runpytest('-n2', '-v') | ||||||
|         output.stdout.re_match_lines_random([ |         output.stdout.re_match_lines_random([ | ||||||
|  | @ -1037,3 +1037,86 @@ class TestProgress: | ||||||
|             r'\[gw\d\] \[\s*\d+%\] PASSED test_foo.py::test_foo\[1\]', |             r'\[gw\d\] \[\s*\d+%\] PASSED test_foo.py::test_foo\[1\]', | ||||||
|             r'\[gw\d\] \[\s*\d+%\] PASSED test_foobar.py::test_foobar\[1\]', |             r'\[gw\d\] \[\s*\d+%\] PASSED test_foobar.py::test_foobar\[1\]', | ||||||
|         ]) |         ]) | ||||||
|  | 
 | ||||||
|  |     def test_capture_no(self, many_tests_files, testdir): | ||||||
|  |         output = testdir.runpytest('-s') | ||||||
|  |         output.stdout.re_match_lines([ | ||||||
|  |             r'test_bar.py \.{10}', | ||||||
|  |             r'test_foo.py \.{5}', | ||||||
|  |             r'test_foobar.py \.{5}', | ||||||
|  |         ]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TestProgressWithTeardown(object): | ||||||
|  |     """Ensure we show the correct percentages for tests that fail during teardown (#3088)""" | ||||||
|  | 
 | ||||||
|  |     @pytest.fixture | ||||||
|  |     def contest_with_teardown_fixture(self, testdir): | ||||||
|  |         testdir.makeconftest(''' | ||||||
|  |             import pytest | ||||||
|  | 
 | ||||||
|  |             @pytest.fixture | ||||||
|  |             def fail_teardown(): | ||||||
|  |                 yield | ||||||
|  |                 assert False | ||||||
|  |         ''') | ||||||
|  | 
 | ||||||
|  |     @pytest.fixture | ||||||
|  |     def many_files(self, testdir, contest_with_teardown_fixture): | ||||||
|  |         testdir.makepyfile( | ||||||
|  |             test_bar=''' | ||||||
|  |                 import pytest | ||||||
|  |                 @pytest.mark.parametrize('i', range(5)) | ||||||
|  |                 def test_bar(fail_teardown, i): | ||||||
|  |                     pass | ||||||
|  |             ''', | ||||||
|  |             test_foo=''' | ||||||
|  |                 import pytest | ||||||
|  |                 @pytest.mark.parametrize('i', range(15)) | ||||||
|  |                 def test_foo(fail_teardown, i): | ||||||
|  |                     pass | ||||||
|  |             ''', | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def test_teardown_simple(self, testdir, contest_with_teardown_fixture): | ||||||
|  |         testdir.makepyfile(''' | ||||||
|  |             def test_foo(fail_teardown): | ||||||
|  |                 pass | ||||||
|  |         ''') | ||||||
|  |         output = testdir.runpytest() | ||||||
|  |         output.stdout.re_match_lines([ | ||||||
|  |             r'test_teardown_simple.py \.E\s+\[100%\]', | ||||||
|  |         ]) | ||||||
|  | 
 | ||||||
|  |     def test_teardown_with_test_also_failing(self, testdir, contest_with_teardown_fixture): | ||||||
|  |         testdir.makepyfile(''' | ||||||
|  |             def test_foo(fail_teardown): | ||||||
|  |                 assert False | ||||||
|  |         ''') | ||||||
|  |         output = testdir.runpytest() | ||||||
|  |         output.stdout.re_match_lines([ | ||||||
|  |             r'test_teardown_with_test_also_failing.py FE\s+\[100%\]', | ||||||
|  |         ]) | ||||||
|  | 
 | ||||||
|  |     def test_teardown_many(self, testdir, many_files): | ||||||
|  |         output = testdir.runpytest() | ||||||
|  |         output.stdout.re_match_lines([ | ||||||
|  |             r'test_bar.py (\.E){5}\s+\[ 25%\]', | ||||||
|  |             r'test_foo.py (\.E){15}\s+\[100%\]', | ||||||
|  |         ]) | ||||||
|  | 
 | ||||||
|  |     def test_teardown_many_verbose(self, testdir, many_files): | ||||||
|  |         output = testdir.runpytest('-v') | ||||||
|  |         output.stdout.re_match_lines([ | ||||||
|  |             r'test_bar.py::test_bar\[0\] PASSED\s+\[  5%\]', | ||||||
|  |             r'test_bar.py::test_bar\[0\] ERROR\s+\[  5%\]', | ||||||
|  |             r'test_bar.py::test_bar\[4\] PASSED\s+\[ 25%\]', | ||||||
|  |             r'test_bar.py::test_bar\[4\] ERROR\s+\[ 25%\]', | ||||||
|  |         ]) | ||||||
|  | 
 | ||||||
|  |     def test_xdist_normal(self, many_files, testdir): | ||||||
|  |         pytest.importorskip('xdist') | ||||||
|  |         output = testdir.runpytest('-n2') | ||||||
|  |         output.stdout.re_match_lines([ | ||||||
|  |             r'[\.E]{40} \s+ \[100%\]', | ||||||
|  |         ]) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue