From 2d4f1f022eb83d3029a35c3ef854534263af6c16 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 18 Jul 2017 17:18:34 -0300 Subject: [PATCH] Introduce PYTEST_CURRENT_TEST environment variable Fix #2583 --- _pytest/runner.py | 19 ++++++++++++++++++ changelog/2583.feature | 2 ++ doc/en/example/simple.rst | 41 +++++++++++++++++++++++++++++++++++++++ testing/test_runner.py | 27 ++++++++++++++++++++++++++ 4 files changed, 89 insertions(+) create mode 100644 changelog/2583.feature diff --git a/_pytest/runner.py b/_pytest/runner.py index fd0b549a9..27be8f4d1 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, division, print_function import bdb +import os import sys from time import time @@ -91,9 +92,11 @@ def show_test_item(item): tw.write(' (fixtures used: {0})'.format(', '.join(used_fixtures))) def pytest_runtest_setup(item): + _update_current_test_var(item, 'setup') item.session._setupstate.prepare(item) def pytest_runtest_call(item): + _update_current_test_var(item, 'call') try: item.runtest() except Exception: @@ -107,7 +110,23 @@ def pytest_runtest_call(item): raise def pytest_runtest_teardown(item, nextitem): + _update_current_test_var(item, 'teardown') item.session._setupstate.teardown_exact(item, nextitem) + _update_current_test_var(item, None) + + +def _update_current_test_var(item, when): + """ + Update PYTEST_CURRENT_TEST to reflect the current item and stage. + + If ``when`` is None, delete PYTEST_CURRENT_TEST from the environment. + """ + var_name = 'PYTEST_CURRENT_TEST' + if when: + os.environ[var_name] = '{0} ({1})'.format(item.nodeid, when) + else: + os.environ.pop(var_name) + def pytest_report_teststatus(report): if report.when in ("setup", "teardown"): diff --git a/changelog/2583.feature b/changelog/2583.feature new file mode 100644 index 000000000..315f2378e --- /dev/null +++ b/changelog/2583.feature @@ -0,0 +1,2 @@ +Introduce the ``PYTEST_CURRENT_TEST`` environment variable that is set with the ``nodeid`` and stage (``setup``, ``call`` and +``teardown``) of the test being currently executed. See the `documentation `_ for more info. diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index da831244b..6b5d5a868 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -761,6 +761,47 @@ and run it:: You'll see that the fixture finalizers could use the precise reporting information. +``PYTEST_CURRENT_TEST`` environment variable +-------------------------------------------- + +.. versionadded:: 3.2 + +Sometimes a test session might get stuck and there might be no easy way to figure out +which test got stuck, for example if pytest was run in quiet mode (``-q``) or you don't have access to the console +output. This is particularly a problem if the problem helps only sporadically, the famous "flaky" kind of tests. + +``pytest`` sets a ``PYTEST_CURRENT_TEST`` environment variable when running tests, which can be inspected +by process monitoring utilities or libraries like `psutil `_ to discover which +test got stuck if necessary: + +.. code-block:: python + + import psutil + + for pid in psutil.pids(): + environ = psutil.Process(pid).environ() + if 'PYTEST_CURRENT_TEST' in environ: + print(f'pytest process {pid} running: {environ["PYTEST_CURRENT_TEST"]}') + +During the test session pytest will set ``PYTEST_CURRENT_TEST`` to the current test +:ref:`nodeid ` and the current stage, which can be ``setup``, ``call`` +and ``teardown``. + +For example, when running a single test function named ``test_foo`` from ``foo_module.py``, +``PYTEST_CURRENT_TEST`` will be set to: + +#. ``foo_module.py::test_foo (setup)`` +#. ``foo_module.py::test_foo (call)`` +#. ``foo_module.py::test_foo (teardown)`` + +In that order. + +.. note:: + + The contents of ``PYTEST_CURRENT_TEST`` is meant to be human readable and the actual format + can be changed between releases (even bug fixes) so it shouldn't be relied on for scripting + or automation. + Freezing pytest --------------- diff --git a/testing/test_runner.py b/testing/test_runner.py index def80ea5f..e70d955ac 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -681,6 +681,8 @@ def test_store_except_info_on_eror(): """ # Simulate item that raises a specific exception class ItemThatRaises(object): + nodeid = 'item_that_raises' + def runtest(self): raise IndexError('TEST') try: @@ -693,6 +695,31 @@ def test_store_except_info_on_eror(): assert sys.last_traceback +def test_current_test_env_var(testdir, monkeypatch): + pytest_current_test_vars = [] + monkeypatch.setattr(sys, 'pytest_current_test_vars', pytest_current_test_vars, raising=False) + testdir.makepyfile(''' + import pytest + import sys + import os + + @pytest.fixture + def fix(): + sys.pytest_current_test_vars.append(('setup', os.environ['PYTEST_CURRENT_TEST'])) + yield + sys.pytest_current_test_vars.append(('teardown', os.environ['PYTEST_CURRENT_TEST'])) + + def test(fix): + sys.pytest_current_test_vars.append(('call', os.environ['PYTEST_CURRENT_TEST'])) + ''') + result = testdir.runpytest_inprocess() + assert result.ret == 0 + test_id = 'test_current_test_env_var.py::test' + assert pytest_current_test_vars == [ + ('setup', test_id + ' (setup)'), ('call', test_id + ' (call)'), ('teardown', test_id + ' (teardown)')] + assert 'PYTEST_CURRENT_TEST' not in os.environ + + class TestReportContents(object): """ Test user-level API of ``TestReport`` objects.