From 2dd6fb6fc978a090427e568ac687895c58bc933f Mon Sep 17 00:00:00 2001 From: Adrian Gielniewski Date: Tue, 17 Oct 2023 22:31:21 +0200 Subject: [PATCH 01/24] Fix is_debugging for pydevd with cython (#146) When `trace_func` comes from cython module, `inspect.getmodule` returns `None`. Fallback to getting module from `__class__` of `trace_func`. --- pytest_timeout.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pytest_timeout.py b/pytest_timeout.py index 674aab6..e52cf8f 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -236,8 +236,11 @@ def is_debugging(trace_func=None): return True if trace_func is None: trace_func = sys.gettrace() - if trace_func and inspect.getmodule(trace_func): - parts = inspect.getmodule(trace_func).__name__.split(".") + trace_module = None + if trace_func: + trace_module = inspect.getmodule(trace_func) or inspect.getmodule(trace_func.__class__) + if trace_module: + parts = trace_module.__name__.split(".") for name in KNOWN_DEBUGGING_MODULES: if any(part.startswith(name) for part in parts): return True From d91e6d8d69ad706e38a2c9de461a72c4d19777ff Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 19 Oct 2023 21:07:32 +0200 Subject: [PATCH 02/24] Update changelog --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index ddf025c..5563936 100644 --- a/README.rst +++ b/README.rst @@ -332,6 +332,12 @@ function: Changelog ========= +Unreleased +---------- + +- Fix debugger detection for recent VSCode, this compiles pydevd using + cython which is now correctly detected. Thanks Adrian Gielniewski. + 2.2.0 ----- From 4f07f1545603419ffe5696805f94f35073285482 Mon Sep 17 00:00:00 2001 From: Alessio <148966056+olk-m@users.noreply.github.com> Date: Wed, 27 Dec 2023 20:33:05 +0000 Subject: [PATCH 03/24] docs: add a warning about using `timeout_func_only = true` in INI alongside with `pytest.mark` on a test (#160) --- README.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.rst b/README.rst index 5563936..3b16306 100644 --- a/README.rst +++ b/README.rst @@ -227,6 +227,17 @@ this really isn't an option a ``timeout_func_only`` boolean setting exists which can be set in the pytest ini configuration file, as documented in ``pytest --help``. +For the decorated function, a decorator will override +``timeout_func_only = true`` in the pytest ini file to the default +value. If you need to keep this option for a decorated test, you +must specify the option explicitly again: + +.. code:: python + + @pytest.mark.timeout(60, func_only=True) + def test_foo(): + pass + Debugger Detection ================== From 50bb71d14e51dd349ae8f2b6bfee7af6cfdcf28a Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Mon, 22 Jan 2024 11:34:07 +0000 Subject: [PATCH 04/24] Update pre-commit hooks Signed-off-by: Pedro Algarvio --- .pre-commit-config.yaml | 30 +++++++++++++----------------- pytest_timeout.py | 4 +++- tox.ini | 4 +--- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 49f5d0c..d1420d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,17 @@ --- repos: - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 23.12.1 hooks: - id: black - args: [--safe, --quiet, --target-version, py36] + args: [--safe, --quiet, --target-version, py37] - repo: https://github.com/asottile/blacken-docs - rev: v1.12.1 + rev: 1.16.0 hooks: - id: blacken-docs - additional_dependencies: [black==20.8b1] + additional_dependencies: [black==23.12.1] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -21,34 +21,30 @@ repos: - id: debug-statements language_version: python3 - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 7.0.0 hooks: - id: flake8 language_version: python3 - additional_dependencies: [flake8-typing-imports==1.3.0] - - repo: https://github.com/FalconSocial/pre-commit-mirrors-pep257 - rev: v0.3.3 - hooks: - - id: pep257 + additional_dependencies: [flake8-typing-imports==1.15.0] - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 7.0.0 hooks: - id: flake8 language_version: python3 - repo: https://github.com/asottile/reorder_python_imports - rev: v3.1.0 + rev: v3.12.0 hooks: - id: reorder-python-imports - repo: https://github.com/asottile/pyupgrade - rev: v2.32.1 + rev: v3.15.0 hooks: - id: pyupgrade - args: [--keep-percent-format, --py36-plus] + args: [--keep-percent-format, --py37-plus] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.9.0 + rev: v1.10.0 hooks: - id: rst-backticks - repo: https://github.com/adrienverge/yamllint.git - rev: v1.26.3 + rev: v1.33.0 hooks: - id: yamllint diff --git a/pytest_timeout.py b/pytest_timeout.py index e52cf8f..0c56a5a 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -238,7 +238,9 @@ def is_debugging(trace_func=None): trace_func = sys.gettrace() trace_module = None if trace_func: - trace_module = inspect.getmodule(trace_func) or inspect.getmodule(trace_func.__class__) + trace_module = inspect.getmodule(trace_func) or inspect.getmodule( + trace_func.__class__ + ) if trace_module: parts = trace_module.__name__.split(".") for name in KNOWN_DEBUGGING_MODULES: diff --git a/tox.ini b/tox.ini index 966ea49..aa195b6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [pytest] -minversion = 2.8 +minversion = 7.0 addopts = -ra [tox] @@ -23,5 +23,3 @@ commands = pre-commit run --all-files --show-diff-on-failure [flake8] disable-noqa = True max-line-length = 88 -extend-ignore = - E203 # whitespace before : is not PEP8 compliant (& conflicts with black) From 7cb29b9e2927b316f6665946a83d4c2fe50e2e68 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Mon, 22 Jan 2024 11:34:33 +0000 Subject: [PATCH 05/24] Switch to using Pytest's terminal reporter Signed-off-by: Pedro Algarvio --- README.rst | 4 ++++ pytest_timeout.py | 60 ++++++++++++++--------------------------------- 2 files changed, 22 insertions(+), 42 deletions(-) diff --git a/README.rst b/README.rst index 3b16306..8dc7ea4 100644 --- a/README.rst +++ b/README.rst @@ -348,6 +348,10 @@ Unreleased - Fix debugger detection for recent VSCode, this compiles pydevd using cython which is now correctly detected. Thanks Adrian Gielniewski. +- Switched to using Pytest's ``TerminalReporter`` instead of writing + directly to ``sys.{stdout,stderr}``. + This change also switches all output from ``sys.stderr`` to ``sys.stdout``. + Thanks Pedro Algarvio. 2.2.0 ----- diff --git a/pytest_timeout.py b/pytest_timeout.py index 0c56a5a..34b1c2a 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -8,7 +8,6 @@ """ import inspect import os -import shutil import signal import sys import threading @@ -445,11 +444,12 @@ def timeout_sigalrm(item, settings): return __tracebackhide__ = True nthreads = len(threading.enumerate()) + terminal = item.config.get_terminal_writer() if nthreads > 1: - write_title("Timeout", sep="+") - dump_stacks() + terminal.sep("+", title="Timeout") + dump_stacks(terminal) if nthreads > 1: - write_title("Timeout", sep="+") + terminal.sep("+", title="Timeout") pytest.fail("Timeout >%ss" % settings.timeout) @@ -461,6 +461,7 @@ def timeout_timer(item, settings): """ if not settings.disable_debugger_detection and is_debugging(): return + terminal = item.config.get_terminal_writer() try: capman = item.config.pluginmanager.getplugin("capturemanager") if capman: @@ -468,30 +469,31 @@ def timeout_timer(item, settings): stdout, stderr = capman.read_global_capture() else: stdout, stderr = None, None - write_title("Timeout", sep="+") + terminal.sep("+", title="Timeout") caplog = item.config.pluginmanager.getplugin("_capturelog") if caplog and hasattr(item, "capturelog_handler"): log = item.capturelog_handler.stream.getvalue() if log: - write_title("Captured log") - write(log) + terminal.sep("~", title="Captured log") + terminal.write(log) if stdout: - write_title("Captured stdout") - write(stdout) + terminal.sep("~", title="Captured stdout") + terminal.write(stdout) if stderr: - write_title("Captured stderr") - write(stderr) - dump_stacks() - write_title("Timeout", sep="+") + terminal.sep("~", title="Captured stderr") + terminal.write(stderr) + dump_stacks(terminal) + terminal.sep("+", title="Timeout") except Exception: traceback.print_exc() finally: + terminal.flush() sys.stdout.flush() sys.stderr.flush() os._exit(1) -def dump_stacks(): +def dump_stacks(terminal): """Dump the stacks of all threads except the current thread.""" current_ident = threading.current_thread().ident for thread_ident, frame in sys._current_frames().items(): @@ -503,31 +505,5 @@ def dump_stacks(): break else: thread_name = "" - write_title("Stack of %s (%s)" % (thread_name, thread_ident)) - write("".join(traceback.format_stack(frame))) - - -def write_title(title, stream=None, sep="~"): - """Write a section title. - - If *stream* is None sys.stderr will be used, *sep* is used to - draw the line. - """ - if stream is None: - stream = sys.stderr - width, height = shutil.get_terminal_size() - fill = int((width - len(title) - 2) / 2) - line = " ".join([sep * fill, title, sep * fill]) - if len(line) < width: - line += sep * (width - len(line)) - stream.write("\n" + line + "\n") - - -def write(text, stream=None): - """Write text to stream. - - Pretty stupid really, only here for symmetry with .write_title(). - """ - if stream is None: - stream = sys.stderr - stream.write(text) + terminal.sep("~", title="Stack of %s (%s)" % (thread_name, thread_ident)) + terminal.write("".join(traceback.format_stack(frame))) From 35c8b767dc3df0db4db79f69303dde51b5dc2cce Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Mon, 22 Jan 2024 12:14:51 +0000 Subject: [PATCH 06/24] Update tests Signed-off-by: Pedro Algarvio --- test_pytest_timeout.py | 196 ++++++++++++++++++++--------------------- 1 file changed, 95 insertions(+), 101 deletions(-) diff --git a/test_pytest_timeout.py b/test_pytest_timeout.py index da735eb..b34ecff 100644 --- a/test_pytest_timeout.py +++ b/test_pytest_timeout.py @@ -16,30 +16,21 @@ ) -@pytest.fixture -def testdir(testdir): - if hasattr(testdir, "runpytest_subprocess"): - # on pytest-2.8 runpytest runs inline by default - # patch the testdir instance to use the subprocess method - testdir.runpytest = testdir.runpytest_subprocess - return testdir - - -def test_header(testdir): - testdir.makepyfile( +def test_header(pytester): + pytester.makepyfile( """ def test_x(): pass """ ) - result = testdir.runpytest("--timeout=1") + result = pytester.runpytest_subprocess("--timeout=1") result.stdout.fnmatch_lines( ["timeout: 1.0s", "timeout method:*", "timeout func_only:*"] ) @have_sigalrm -def test_sigalrm(testdir): - testdir.makepyfile( +def test_sigalrm(pytester): + pytester.makepyfile( """ import time @@ -47,12 +38,12 @@ def test_foo(): time.sleep(2) """ ) - result = testdir.runpytest("--timeout=1") + result = pytester.runpytest_subprocess("--timeout=1") result.stdout.fnmatch_lines(["*Failed: Timeout >1.0s*"]) -def test_thread(testdir): - testdir.makepyfile( +def test_thread(pytester): + pytester.makepyfile( """ import time @@ -60,8 +51,8 @@ def test_foo(): time.sleep(2) """ ) - result = testdir.runpytest("--timeout=1", "--timeout-method=thread") - result.stderr.fnmatch_lines( + result = pytester.runpytest_subprocess("--timeout=1", "--timeout-method=thread") + result.stdout.fnmatch_lines( [ "*++ Timeout ++*", "*~~ Stack of MainThread* ~~*", @@ -69,16 +60,16 @@ def test_foo(): "*++ Timeout ++*", ] ) - assert "++ Timeout ++" in result.stderr.lines[-1] + assert "++ Timeout ++" in result.stdout.lines[-1] @pytest.mark.skipif( hasattr(sys, "pypy_version_info"), reason="pypy coverage seems broken currently" ) -def test_cov(testdir): +def test_cov(pytester): # This test requires pytest-cov pytest.importorskip("pytest_cov") - testdir.makepyfile( + pytester.makepyfile( """ import time @@ -86,10 +77,10 @@ def test_foo(): time.sleep(2) """ ) - result = testdir.runpytest( + result = pytester.runpytest_subprocess( "--timeout=1", "--cov=test_cov", "--timeout-method=thread" ) - result.stderr.fnmatch_lines( + result.stdout.fnmatch_lines( [ "*++ Timeout ++*", "*~~ Stack of MainThread* ~~*", @@ -97,11 +88,11 @@ def test_foo(): "*++ Timeout ++*", ] ) - assert "++ Timeout ++" in result.stderr.lines[-1] + assert "++ Timeout ++" in result.stdout.lines[-1] -def test_timeout_env(testdir, monkeypatch): - testdir.makepyfile( +def test_timeout_env(pytester, monkeypatch): + pytester.makepyfile( """ import time @@ -110,13 +101,13 @@ def test_foo(): """ ) monkeypatch.setitem(os.environ, "PYTEST_TIMEOUT", "1") - result = testdir.runpytest() + result = pytester.runpytest_subprocess() assert result.ret > 0 # @pytest.mark.parametrize('meth', [have_sigalrm('signal'), 'thread']) -# def test_func_fix(meth, testdir): -# testdir.makepyfile(""" +# def test_func_fix(meth, pytester): +# pytester.makepyfile(""" # import time, pytest # @pytest.fixture(scope='function') @@ -126,7 +117,7 @@ def test_foo(): # def test_foo(fix): # pass # """) -# result = testdir.runpytest('--timeout=1', +# result = pytester.runpytest_subprocess('--timeout=1', # '--timeout-method={0}'.format(meth)) # assert result.ret > 0 # assert 'Timeout' in result.stdout.str() + result.stderr.str() @@ -134,8 +125,8 @@ def test_foo(): @pytest.mark.parametrize("meth", [pytest.param("signal", marks=have_sigalrm), "thread"]) @pytest.mark.parametrize("scope", ["function", "class", "module", "session"]) -def test_fix_setup(meth, scope, testdir): - testdir.makepyfile( +def test_fix_setup(meth, scope, pytester): + pytester.makepyfile( """ import time, pytest @@ -151,13 +142,13 @@ def test_foo(self, fix): scope=scope ) ) - result = testdir.runpytest("--timeout=1", f"--timeout-method={meth}") + result = pytester.runpytest_subprocess("--timeout=1", f"--timeout-method={meth}") assert result.ret > 0 assert "Timeout" in result.stdout.str() + result.stderr.str() -def test_fix_setup_func_only(testdir): - testdir.makepyfile( +def test_fix_setup_func_only(pytester): + pytester.makepyfile( """ import time, pytest @@ -172,15 +163,15 @@ def test_foo(self, fix): pass """ ) - result = testdir.runpytest("--timeout=1") + result = pytester.runpytest_subprocess("--timeout=1") assert result.ret == 0 assert "Timeout" not in result.stdout.str() + result.stderr.str() @pytest.mark.parametrize("meth", [pytest.param("signal", marks=have_sigalrm), "thread"]) @pytest.mark.parametrize("scope", ["function", "class", "module", "session"]) -def test_fix_finalizer(meth, scope, testdir): - testdir.makepyfile( +def test_fix_finalizer(meth, scope, pytester): + pytester.makepyfile( """ import time, pytest @@ -198,13 +189,15 @@ def test_foo(self, fix): pass """ ) - result = testdir.runpytest("--timeout=1", "-s", f"--timeout-method={meth}") + result = pytester.runpytest_subprocess( + "--timeout=1", "-s", f"--timeout-method={meth}" + ) assert result.ret > 0 assert "Timeout" in result.stdout.str() + result.stderr.str() -def test_fix_finalizer_func_only(testdir): - testdir.makepyfile( +def test_fix_finalizer_func_only(pytester): + pytester.makepyfile( """ import time, pytest @@ -223,14 +216,14 @@ def test_foo(self, fix): pass """ ) - result = testdir.runpytest("--timeout=1", "-s") + result = pytester.runpytest_subprocess("--timeout=1", "-s") assert result.ret == 0 assert "Timeout" not in result.stdout.str() + result.stderr.str() @have_sigalrm -def test_timeout_mark_sigalrm(testdir): - testdir.makepyfile( +def test_timeout_mark_sigalrm(pytester): + pytester.makepyfile( """ import time, pytest @@ -240,12 +233,12 @@ def test_foo(): assert False """ ) - result = testdir.runpytest() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*Failed: Timeout >1.0s*"]) -def test_timeout_mark_timer(testdir): - testdir.makepyfile( +def test_timeout_mark_timer(pytester): + pytester.makepyfile( """ import time, pytest @@ -254,12 +247,12 @@ def test_foo(): time.sleep(2) """ ) - result = testdir.runpytest("--timeout-method=thread") - result.stderr.fnmatch_lines(["*++ Timeout ++*"]) + result = pytester.runpytest_subprocess("--timeout-method=thread") + result.stdout.fnmatch_lines(["*++ Timeout ++*"]) -def test_timeout_mark_non_int(testdir): - testdir.makepyfile( +def test_timeout_mark_non_int(pytester): + pytester.makepyfile( """ import time, pytest @@ -268,12 +261,12 @@ def test_foo(): time.sleep(1) """ ) - result = testdir.runpytest("--timeout-method=thread") - result.stderr.fnmatch_lines(["*++ Timeout ++*"]) + result = pytester.runpytest_subprocess("--timeout-method=thread") + result.stdout.fnmatch_lines(["*++ Timeout ++*"]) -def test_timeout_mark_non_number(testdir): - testdir.makepyfile( +def test_timeout_mark_non_number(pytester): + pytester.makepyfile( """ import pytest @@ -282,12 +275,12 @@ def test_foo(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*ValueError*"]) -def test_timeout_mark_args(testdir): - testdir.makepyfile( +def test_timeout_mark_args(pytester): + pytester.makepyfile( """ import pytest @@ -296,12 +289,12 @@ def test_foo(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*ValueError*"]) -def test_timeout_mark_method_nokw(testdir): - testdir.makepyfile( +def test_timeout_mark_method_nokw(pytester): + pytester.makepyfile( """ import time, pytest @@ -310,12 +303,12 @@ def test_foo(): time.sleep(2) """ ) - result = testdir.runpytest() - result.stderr.fnmatch_lines(["*+ Timeout +*"]) + result = pytester.runpytest_subprocess() + result.stdout.fnmatch_lines(["*+ Timeout +*"]) -def test_timeout_mark_noargs(testdir): - testdir.makepyfile( +def test_timeout_mark_noargs(pytester): + pytester.makepyfile( """ import pytest @@ -324,12 +317,12 @@ def test_foo(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*TypeError*"]) -def test_ini_timeout(testdir): - testdir.makepyfile( +def test_ini_timeout(pytester): + pytester.makepyfile( """ import time @@ -337,18 +330,18 @@ def test_foo(): time.sleep(2) """ ) - testdir.makeini( + pytester.makeini( """ [pytest] timeout = 1 """ ) - result = testdir.runpytest() + result = pytester.runpytest_subprocess() assert result.ret -def test_ini_timeout_func_only(testdir): - testdir.makepyfile( +def test_ini_timeout_func_only(pytester): + pytester.makepyfile( """ import time, pytest @@ -359,19 +352,19 @@ def test_foo(slow): pass """ ) - testdir.makeini( + pytester.makeini( """ [pytest] timeout = 1 timeout_func_only = true """ ) - result = testdir.runpytest() + result = pytester.runpytest_subprocess() assert result.ret == 0 -def test_ini_timeout_func_only_marker_override(testdir): - testdir.makepyfile( +def test_ini_timeout_func_only_marker_override(pytester): + pytester.makepyfile( """ import time, pytest @@ -383,19 +376,19 @@ def test_foo(slow): pass """ ) - testdir.makeini( + pytester.makeini( """ [pytest] timeout = 1 timeout_func_only = true """ ) - result = testdir.runpytest() + result = pytester.runpytest_subprocess() assert result.ret == 0 -def test_ini_method(testdir): - testdir.makepyfile( +def test_ini_method(pytester): + pytester.makepyfile( """ import time @@ -403,19 +396,19 @@ def test_foo(): time.sleep(2) """ ) - testdir.makeini( + pytester.makeini( """ [pytest] timeout = 1 timeout_method = thread """ ) - result = testdir.runpytest() + result = pytester.runpytest_subprocess() assert "=== 1 failed in " not in result.outlines[-1] -def test_timeout_marker_inheritance(testdir): - testdir.makepyfile( +def test_timeout_marker_inheritance(pytester): + pytester.makepyfile( """ import time, pytest @@ -430,13 +423,13 @@ def test_foo_1(self): time.sleep(1) """ ) - result = testdir.runpytest("--timeout=1", "-s") + result = pytester.runpytest_subprocess("--timeout=1", "-s") assert result.ret == 0 assert "Timeout" not in result.stdout.str() + result.stderr.str() -def test_marker_help(testdir): - result = testdir.runpytest("--markers") +def test_marker_help(pytester): + result = pytester.runpytest_subprocess("--markers") result.stdout.fnmatch_lines(["@pytest.mark.timeout(*"]) @@ -461,9 +454,9 @@ def test_marker_help(testdir): ) @have_spawn def test_suppresses_timeout_when_debugger_is_entered( - testdir, debugging_module, debugging_set_trace + pytester, debugging_module, debugging_set_trace ): - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ import pytest, {debugging_module} @@ -474,7 +467,7 @@ def test_foo(): debugging_module=debugging_module, debugging_set_trace=debugging_set_trace ) ) - child = testdir.spawn_pytest(str(p1)) + child = pytester.spawn_pytest(str(p1)) child.expect("test_foo") time.sleep(0.2) child.send("c\n") @@ -507,9 +500,9 @@ def test_foo(): ) @have_spawn def test_disable_debugger_detection_flag( - testdir, debugging_module, debugging_set_trace + pytester, debugging_module, debugging_set_trace ): - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ import pytest, {debugging_module} @@ -520,7 +513,7 @@ def test_foo(): debugging_module=debugging_module, debugging_set_trace=debugging_set_trace ) ) - child = testdir.spawn_pytest(f"{p1} --timeout-disable-debugger-detection") + child = pytester.spawn_pytest(f"{p1} --timeout-disable-debugger-detection") child.expect("test_foo") time.sleep(1.2) result = child.read().decode().lower() @@ -551,8 +544,9 @@ def custom_trace(*args): assert pytest_timeout.is_debugging(custom_trace) -def test_not_main_thread(testdir): - testdir.makepyfile( +def test_not_main_thread(pytester): + pytest.skip("The 'pytest_timeout.timeout_setup' function no longer exists") + pytester.makepyfile( """ import threading import pytest_timeout @@ -569,14 +563,14 @@ def new_timeout_setup(item): def test_x(): pass """ ) - result = testdir.runpytest("--timeout=1") + result = pytester.runpytest_subprocess("--timeout=1") result.stdout.fnmatch_lines( ["timeout: 1.0s", "timeout method:*", "timeout func_only:*"] ) -def test_plugin_interface(testdir): - testdir.makeconftest( +def test_plugin_interface(pytester): + pytester.makeconftest( """ import pytest @@ -593,7 +587,7 @@ def pytest_timeout_cancel_timer(item): return True """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -602,7 +596,7 @@ def test_foo(): pass """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest_subprocess("-s") result.stdout.fnmatch_lines( [ "pytest_timeout_set_timer", From 9a024f5200f55c30ae74cf779a5102b3f140cf98 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Mon, 22 Jan 2024 21:39:39 +0000 Subject: [PATCH 07/24] Pytest 7.0 is now the minimum version required Signed-off-by: Pedro Algarvio --- README.rst | 1 + setup.cfg | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 8dc7ea4..ebbaf1a 100644 --- a/README.rst +++ b/README.rst @@ -352,6 +352,7 @@ Unreleased directly to ``sys.{stdout,stderr}``. This change also switches all output from ``sys.stderr`` to ``sys.stdout``. Thanks Pedro Algarvio. +- Pytest 7.0.0 is now the minimum supported version. Thanks Pedro Algarvio. 2.2.0 ----- diff --git a/setup.cfg b/setup.cfg index be6b63f..e49f697 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,7 @@ classifiers = [options] py_modules = pytest_timeout install_requires = - pytest>=5.0.0 + pytest>=7.0.0 python_requires = >=3.7 [options.entry_points] From 97196bfc2053d4e09a885711303f1b27b08db363 Mon Sep 17 00:00:00 2001 From: Brian Okken <1568356+okken@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:58:09 -0800 Subject: [PATCH 08/24] test Python 3.12 --- .github/workflows/main.yml | 2 ++ tox.ini | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c4c27ba..2adbe4d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,6 +21,8 @@ jobs: tox_env: "py310" - v: "3.11" tox_env: "py311" + - v: "3.12" + tox_env: "py312" os: [ubuntu-latest, windows-latest] steps: - name: Set Git to use LF diff --git a/tox.ini b/tox.ini index aa195b6..8dbfa6a 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ minversion = 7.0 addopts = -ra [tox] -envlist = py37,py38,py39,py310,py311,pypy3 +envlist = py37,py38,py39,py310,py311,py312,pypy3 [testenv] deps = pytest From 94f948a5a027bb8de3b2adb7cc353fefb54fc38a Mon Sep 17 00:00:00 2001 From: Brian Okken <1568356+okken@users.noreply.github.com> Date: Tue, 23 Jan 2024 15:28:34 -0800 Subject: [PATCH 09/24] add --suite-timeout --- pytest_timeout.py | 30 ++++++++++++++++++++++++++++++ test_pytest_timeout.py | 19 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/pytest_timeout.py b/pytest_timeout.py index 34b1c2a..b601ba1 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -10,6 +10,7 @@ import os import signal import sys +import time import threading import traceback from collections import namedtuple @@ -43,6 +44,11 @@ When specified, disables debugger detection. breakpoint(), pdb.set_trace(), etc. will be interrupted by the timeout. """.strip() +SUITE_TIMEOUT_DESC = """ +Timeout in minutes for entire suite. Default is None which +means no timeout. Timeout is checked between tests, and will not interrupt a test +in progress. Can be specified as a float for partial minutes. +""".strip() # bdb covers pdb, ipdb, and possibly others # pydevd covers PyCharm, VSCode, and possibly others @@ -79,6 +85,15 @@ def pytest_addoption(parser): action="store_true", help=DISABLE_DEBUGGER_DETECTION_DESC, ) + group.addoption( + "--suite-timeout", + action="store", + dest="suite_timeout", + default=None, + type=float, + metavar="minutes", + help=SUITE_TIMEOUT_DESC, + ) parser.addini("timeout", TIMEOUT_DESC) parser.addini("timeout_method", METHOD_DESC) parser.addini("timeout_func_only", FUNC_ONLY_DESC, type="bool", default=False) @@ -119,9 +134,13 @@ def pytest_addhooks(pluginmanager): pluginmanager.add_hookspecs(TimeoutHooks) +_suite_expire_time = 0 + + @pytest.hookimpl def pytest_configure(config): """Register the marker so it shows up in --markers output.""" + global _suite_expire_time, _suite_timeout_minutes config.addinivalue_line( "markers", "timeout(timeout, method=None, func_only=False, " @@ -143,6 +162,11 @@ def pytest_configure(config): config._env_timeout_func_only = settings.func_only config._env_timeout_disable_debugger_detection = settings.disable_debugger_detection + _suite_timeout_minutes = config.getoption("--suite-timeout") + if _suite_timeout_minutes: + _suite_expire_time = time.time() + (_suite_timeout_minutes * 60) + + @pytest.hookimpl(hookwrapper=True) def pytest_runtest_protocol(item): @@ -507,3 +531,9 @@ def dump_stacks(terminal): thread_name = "" terminal.sep("~", title="Stack of %s (%s)" % (thread_name, thread_ident)) terminal.write("".join(traceback.format_stack(frame))) + + +def pytest_runtest_logfinish(nodeid, location): + if _suite_expire_time and _suite_expire_time < time.time(): + pytest.exit(f"suite-timeout: {_suite_timeout_minutes} minutes exceeded", + returncode=0) \ No newline at end of file diff --git a/test_pytest_timeout.py b/test_pytest_timeout.py index b34ecff..2ba276b 100644 --- a/test_pytest_timeout.py +++ b/test_pytest_timeout.py @@ -603,3 +603,22 @@ def test_foo(): "pytest_timeout_cancel_timer", ] ) + +def test_suite_timeout(pytester): + pytester.makepyfile( + """ + import time, pytest + + @pytest.mark.parametrize('i', range(10)) + def test_foo(i): + time.sleep(0.1) + """ + ) + # each parametrization runs for 0.1 sec + # or about 0.00166 seconds each + # so 0.005 min should be about 3 iterations + result = pytester.runpytest_subprocess("--suite-timeout", "0.005") + result.stdout.fnmatch_lines([ + "*= 3 passed * =*", + "*!! * suite-timeout: 0.005 minutes exceeded !!!*" + ]) \ No newline at end of file From c1c669135cdf507ac2e9f56be37196869515317a Mon Sep 17 00:00:00 2001 From: Brian Okken <1568356+okken@users.noreply.github.com> Date: Fri, 26 Jan 2024 13:43:25 -0800 Subject: [PATCH 10/24] switch to seconds --- pytest_timeout.py | 28 +++++++++++++++++----------- test_pytest_timeout.py | 13 ++++--------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/pytest_timeout.py b/pytest_timeout.py index b601ba1..39f112e 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -19,6 +19,7 @@ __all__ = ("is_debugging", "Settings") +suite_timeout_key = pytest.StashKey[float]() HAVE_SIGALRM = hasattr(signal, "SIGALRM") @@ -45,9 +46,9 @@ will be interrupted by the timeout. """.strip() SUITE_TIMEOUT_DESC = """ -Timeout in minutes for entire suite. Default is None which +Timeout in seconds for entire suite. Default is None which means no timeout. Timeout is checked between tests, and will not interrupt a test -in progress. Can be specified as a float for partial minutes. +in progress. """.strip() # bdb covers pdb, ipdb, and possibly others @@ -91,7 +92,7 @@ def pytest_addoption(parser): dest="suite_timeout", default=None, type=float, - metavar="minutes", + metavar="SECONDS", help=SUITE_TIMEOUT_DESC, ) parser.addini("timeout", TIMEOUT_DESC) @@ -162,10 +163,12 @@ def pytest_configure(config): config._env_timeout_func_only = settings.func_only config._env_timeout_disable_debugger_detection = settings.disable_debugger_detection - _suite_timeout_minutes = config.getoption("--suite-timeout") - if _suite_timeout_minutes: - _suite_expire_time = time.time() + (_suite_timeout_minutes * 60) - + timeout = config.getoption("--suite-timeout") + if timeout is not None: + expire_time = time.time() + timeout + else: + expire_time = 0 + config.stash[suite_timeout_key] = expire_time @pytest.hookimpl(hookwrapper=True) @@ -533,7 +536,10 @@ def dump_stacks(terminal): terminal.write("".join(traceback.format_stack(frame))) -def pytest_runtest_logfinish(nodeid, location): - if _suite_expire_time and _suite_expire_time < time.time(): - pytest.exit(f"suite-timeout: {_suite_timeout_minutes} minutes exceeded", - returncode=0) \ No newline at end of file +def pytest_runtest_makereport(item, call): + session = item.session + config = session.config + expire_time = config.stash[suite_timeout_key] + if expire_time and (expire_time < time.time()): + timeout = config.getoption("--suite-timeout") + session.shouldfail = f"suite-timeout: {timeout} sec exceeded" diff --git a/test_pytest_timeout.py b/test_pytest_timeout.py index 2ba276b..5a621fe 100644 --- a/test_pytest_timeout.py +++ b/test_pytest_timeout.py @@ -611,14 +611,9 @@ def test_suite_timeout(pytester): @pytest.mark.parametrize('i', range(10)) def test_foo(i): - time.sleep(0.1) + time.sleep(1) """ ) - # each parametrization runs for 0.1 sec - # or about 0.00166 seconds each - # so 0.005 min should be about 3 iterations - result = pytester.runpytest_subprocess("--suite-timeout", "0.005") - result.stdout.fnmatch_lines([ - "*= 3 passed * =*", - "*!! * suite-timeout: 0.005 minutes exceeded !!!*" - ]) \ No newline at end of file + result = pytester.runpytest_subprocess("--suite-timeout", "0.5") + result.stdout.fnmatch_lines(["*!! suite-timeout: 0.5 sec exceeded !!!*"]) + result.assert_outcomes(passed=1) From 34bb2d871142fc86456b060a84c28de712980b50 Mon Sep 17 00:00:00 2001 From: Brian Okken <1568356+okken@users.noreply.github.com> Date: Wed, 31 Jan 2024 10:36:44 -0800 Subject: [PATCH 11/24] recommended flub changes --- pytest_timeout.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pytest_timeout.py b/pytest_timeout.py index 39f112e..7f1b175 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -19,7 +19,7 @@ __all__ = ("is_debugging", "Settings") -suite_timeout_key = pytest.StashKey[float]() +SUITE_TIMEOUT_KEY = pytest.StashKey[float]() HAVE_SIGALRM = hasattr(signal, "SIGALRM") @@ -135,13 +135,9 @@ def pytest_addhooks(pluginmanager): pluginmanager.add_hookspecs(TimeoutHooks) -_suite_expire_time = 0 - - @pytest.hookimpl def pytest_configure(config): """Register the marker so it shows up in --markers output.""" - global _suite_expire_time, _suite_timeout_minutes config.addinivalue_line( "markers", "timeout(timeout, method=None, func_only=False, " @@ -168,7 +164,7 @@ def pytest_configure(config): expire_time = time.time() + timeout else: expire_time = 0 - config.stash[suite_timeout_key] = expire_time + config.stash[SUITE_TIMEOUT_KEY] = expire_time @pytest.hookimpl(hookwrapper=True) @@ -539,7 +535,7 @@ def dump_stacks(terminal): def pytest_runtest_makereport(item, call): session = item.session config = session.config - expire_time = config.stash[suite_timeout_key] + expire_time = config.stash[SUITE_TIMEOUT_KEY] if expire_time and (expire_time < time.time()): timeout = config.getoption("--suite-timeout") session.shouldfail = f"suite-timeout: {timeout} sec exceeded" From 6f2a32fcfd533119d76b3598b8f46796b3d468d6 Mon Sep 17 00:00:00 2001 From: Brian Okken <1568356+okken@users.noreply.github.com> Date: Wed, 31 Jan 2024 10:45:54 -0800 Subject: [PATCH 12/24] gate logic to once per test --- pytest_timeout.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pytest_timeout.py b/pytest_timeout.py index 7f1b175..5829ed3 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -533,9 +533,11 @@ def dump_stacks(terminal): def pytest_runtest_makereport(item, call): - session = item.session - config = session.config - expire_time = config.stash[SUITE_TIMEOUT_KEY] - if expire_time and (expire_time < time.time()): - timeout = config.getoption("--suite-timeout") - session.shouldfail = f"suite-timeout: {timeout} sec exceeded" + # only need to check timeout once, at the end, after teardown + if call.when == "teardown": + session = item.session + config = session.config + expire_time = config.stash[SUITE_TIMEOUT_KEY] + if expire_time and (expire_time < time.time()): + timeout = config.getoption("--suite-timeout") + session.shouldfail = f"suite-timeout: {timeout} sec exceeded" From 97a9ef518861385d777eb363f223eb15d6a7ecd7 Mon Sep 17 00:00:00 2001 From: Brian Okken <1568356+okken@users.noreply.github.com> Date: Wed, 31 Jan 2024 10:49:14 -0800 Subject: [PATCH 13/24] try to run hook first --- pytest_timeout.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest_timeout.py b/pytest_timeout.py index 5829ed3..220c727 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -532,6 +532,7 @@ def dump_stacks(terminal): terminal.write("".join(traceback.format_stack(frame))) +@pytest.hookimpl(tryfirst=True) def pytest_runtest_makereport(item, call): # only need to check timeout once, at the end, after teardown if call.when == "teardown": From cc3f27aaad35eab603c634948c251d97c613e492 Mon Sep 17 00:00:00 2001 From: Brian Okken <1568356+okken@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:06:42 -0800 Subject: [PATCH 14/24] add suite timeout to header, linting fix. --- pytest_timeout.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pytest_timeout.py b/pytest_timeout.py index 220c727..7d406f1 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -10,8 +10,8 @@ import os import signal import sys -import time import threading +import time import traceback from collections import namedtuple @@ -47,8 +47,8 @@ """.strip() SUITE_TIMEOUT_DESC = """ Timeout in seconds for entire suite. Default is None which -means no timeout. Timeout is checked between tests, and will not interrupt a test -in progress. +means no timeout. Timeout is checked between tests, and will not interrupt a test +in progress. """.strip() # bdb covers pdb, ipdb, and possibly others @@ -205,15 +205,23 @@ def pytest_runtest_call(item): @pytest.hookimpl(tryfirst=True) def pytest_report_header(config): """Add timeout config to pytest header.""" + timeout_header = [] + if config._env_timeout: - return [ + timeout_header.append( "timeout: %ss\ntimeout method: %s\ntimeout func_only: %s" % ( config._env_timeout, config._env_timeout_method, config._env_timeout_func_only, ) - ] + ) + + suite_timeout = config.getoption("--suite-timeout") + if suite_timeout: + timeout_header.append("suite timeout: %ss" % suite_timeout) + if timeout_header: + return timeout_header @pytest.hookimpl(tryfirst=True) @@ -534,7 +542,7 @@ def dump_stacks(terminal): @pytest.hookimpl(tryfirst=True) def pytest_runtest_makereport(item, call): - # only need to check timeout once, at the end, after teardown + # only need to check timeout once, at the end, after teardown if call.when == "teardown": session = item.session config = session.config From 79dea97deb62852726cc4e46f709ec45731b35ae Mon Sep 17 00:00:00 2001 From: Brian Okken Date: Tue, 6 Feb 2024 14:35:49 -0800 Subject: [PATCH 15/24] move suite timeout to pytest_runtest_protocol --- pytest_timeout.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/pytest_timeout.py b/pytest_timeout.py index 7d406f1..b75b887 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -184,6 +184,12 @@ def pytest_runtest_protocol(item): if is_timeout and settings.func_only is False: hooks.pytest_timeout_cancel_timer(item=item) + # check suite timeout + expire_time = item.session.config.stash[SUITE_TIMEOUT_KEY] + if expire_time and (expire_time < time.time()): + timeout = item.session.config.getoption("--suite-timeout") + item.session.shouldfail = f"suite-timeout: {timeout} sec exceeded" + @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(item): @@ -538,15 +544,3 @@ def dump_stacks(terminal): thread_name = "" terminal.sep("~", title="Stack of %s (%s)" % (thread_name, thread_ident)) terminal.write("".join(traceback.format_stack(frame))) - - -@pytest.hookimpl(tryfirst=True) -def pytest_runtest_makereport(item, call): - # only need to check timeout once, at the end, after teardown - if call.when == "teardown": - session = item.session - config = session.config - expire_time = config.stash[SUITE_TIMEOUT_KEY] - if expire_time and (expire_time < time.time()): - timeout = config.getoption("--suite-timeout") - session.shouldfail = f"suite-timeout: {timeout} sec exceeded" From 333f3be20e7b328b677152030e898b14623f4f7d Mon Sep 17 00:00:00 2001 From: Brian Okken <1568356+okken@users.noreply.github.com> Date: Tue, 6 Feb 2024 14:55:12 -0800 Subject: [PATCH 16/24] suite-timeout -> session-timeout --- .gitignore | 1 + pytest_timeout.py | 30 +++++++++++++++--------------- test_pytest_timeout.py | 7 ++++--- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 88cb850..56db748 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ docs/_build/ # Virtual Envs .env* +venv # IDE .idea diff --git a/pytest_timeout.py b/pytest_timeout.py index b75b887..800d0cd 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -19,7 +19,7 @@ __all__ = ("is_debugging", "Settings") -SUITE_TIMEOUT_KEY = pytest.StashKey[float]() +SESSION_TIMEOUT_KEY = pytest.StashKey[float]() HAVE_SIGALRM = hasattr(signal, "SIGALRM") @@ -45,8 +45,8 @@ When specified, disables debugger detection. breakpoint(), pdb.set_trace(), etc. will be interrupted by the timeout. """.strip() -SUITE_TIMEOUT_DESC = """ -Timeout in seconds for entire suite. Default is None which +SESSION_TIMEOUT_DESC = """ +Timeout in seconds for entire session. Default is None which means no timeout. Timeout is checked between tests, and will not interrupt a test in progress. """.strip() @@ -87,13 +87,13 @@ def pytest_addoption(parser): help=DISABLE_DEBUGGER_DETECTION_DESC, ) group.addoption( - "--suite-timeout", + "--session-timeout", action="store", - dest="suite_timeout", + dest="session_timeout", default=None, type=float, metavar="SECONDS", - help=SUITE_TIMEOUT_DESC, + help=SESSION_TIMEOUT_DESC, ) parser.addini("timeout", TIMEOUT_DESC) parser.addini("timeout_method", METHOD_DESC) @@ -159,12 +159,12 @@ def pytest_configure(config): config._env_timeout_func_only = settings.func_only config._env_timeout_disable_debugger_detection = settings.disable_debugger_detection - timeout = config.getoption("--suite-timeout") + timeout = config.getoption("--session-timeout") if timeout is not None: expire_time = time.time() + timeout else: expire_time = 0 - config.stash[SUITE_TIMEOUT_KEY] = expire_time + config.stash[SESSION_TIMEOUT_KEY] = expire_time @pytest.hookimpl(hookwrapper=True) @@ -184,11 +184,11 @@ def pytest_runtest_protocol(item): if is_timeout and settings.func_only is False: hooks.pytest_timeout_cancel_timer(item=item) - # check suite timeout - expire_time = item.session.config.stash[SUITE_TIMEOUT_KEY] + # check session timeout + expire_time = item.session.config.stash[SESSION_TIMEOUT_KEY] if expire_time and (expire_time < time.time()): - timeout = item.session.config.getoption("--suite-timeout") - item.session.shouldfail = f"suite-timeout: {timeout} sec exceeded" + timeout = item.session.config.getoption("--session-timeout") + item.session.shouldfail = f"session-timeout: {timeout} sec exceeded" @pytest.hookimpl(hookwrapper=True) @@ -223,9 +223,9 @@ def pytest_report_header(config): ) ) - suite_timeout = config.getoption("--suite-timeout") - if suite_timeout: - timeout_header.append("suite timeout: %ss" % suite_timeout) + session_timeout = config.getoption("--session-timeout") + if session_timeout: + timeout_header.append("session timeout: %ss" % session_timeout) if timeout_header: return timeout_header diff --git a/test_pytest_timeout.py b/test_pytest_timeout.py index 5a621fe..7989557 100644 --- a/test_pytest_timeout.py +++ b/test_pytest_timeout.py @@ -604,7 +604,8 @@ def test_foo(): ] ) -def test_suite_timeout(pytester): + +def test_session_timeout(pytester): pytester.makepyfile( """ import time, pytest @@ -614,6 +615,6 @@ def test_foo(i): time.sleep(1) """ ) - result = pytester.runpytest_subprocess("--suite-timeout", "0.5") - result.stdout.fnmatch_lines(["*!! suite-timeout: 0.5 sec exceeded !!!*"]) + result = pytester.runpytest_subprocess("--session-timeout", "0.5") + result.stdout.fnmatch_lines(["*!! session-timeout: 0.5 sec exceeded !!!*"]) result.assert_outcomes(passed=1) From 063f077ddff2b88133fed10f76c6ea797f820399 Mon Sep 17 00:00:00 2001 From: Brian Okken <1568356+okken@users.noreply.github.com> Date: Wed, 7 Feb 2024 13:18:14 -0800 Subject: [PATCH 17/24] extend test_header to include session timeout. Make test_session_timeout more clear. --- test_pytest_timeout.py | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/test_pytest_timeout.py b/test_pytest_timeout.py index 7989557..ac4e9fa 100644 --- a/test_pytest_timeout.py +++ b/test_pytest_timeout.py @@ -22,9 +22,14 @@ def test_header(pytester): def test_x(): pass """ ) - result = pytester.runpytest_subprocess("--timeout=1") + result = pytester.runpytest_subprocess("--timeout=1", "--session-timeout=2") result.stdout.fnmatch_lines( - ["timeout: 1.0s", "timeout method:*", "timeout func_only:*"] + [ + "timeout: 1.0s", + "timeout method:*", + "timeout func_only:*", + "session timeout: 2.0s", + ] ) @@ -606,15 +611,29 @@ def test_foo(): def test_session_timeout(pytester): + # 2 tests, each with 0.5 sec timeouts + # each using a fixture with 0.5 sec setup and 0.5 sec teardown + # So about 1.5 seconds per test, ~3 sec total, + # Therefore: + # A timeout of 1.25 sec should happen during the teardown of the first test + # and the second test should NOT be run pytester.makepyfile( """ - import time, pytest + import time, pytest - @pytest.mark.parametrize('i', range(10)) - def test_foo(i): - time.sleep(1) - """ + @pytest.fixture() + def slow_setup_and_teardown(): + time.sleep(0.5) + yield + time.sleep(0.5) + + def test_one(slow_setup_and_teardown): + time.sleep(0.5) + + def test_two(slow_setup_and_teardown): + time.sleep(0.5) + """ ) - result = pytester.runpytest_subprocess("--session-timeout", "0.5") - result.stdout.fnmatch_lines(["*!! session-timeout: 0.5 sec exceeded !!!*"]) + result = pytester.runpytest_subprocess("--session-timeout", "1.25") + result.stdout.fnmatch_lines(["*!! session-timeout: 1.25 sec exceeded !!!*"]) result.assert_outcomes(passed=1) From 325b40c562ee1d8bcee8a30139e5b16f734150fb Mon Sep 17 00:00:00 2001 From: Brian Okken <1568356+okken@users.noreply.github.com> Date: Thu, 8 Feb 2024 16:43:44 -0800 Subject: [PATCH 18/24] docs + config setting support + test mod --- README.rst | 34 ++++++++++++++++++++++++++++++++++ pytest_timeout.py | 18 +++++++++++++----- test_pytest_timeout.py | 35 +++++++++++++++++++++++++++++------ 3 files changed, 76 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index ebbaf1a..85da61d 100644 --- a/README.rst +++ b/README.rst @@ -340,6 +340,39 @@ function: pytest.fail("+++ Timeout +++") + +Session Timeout +=============== + +The above mentioned timeouts are all per test function. You can also set a +session timeout in seconds. The following example shows a session timeout +of 10 minutes (600 seconds):: + + pytest --session-timeout=600 + +You can also set the session timeout the `pytest configuration file`__ + using the ``session_timeout`` option: + + .. code:: ini + + [pytest] + session_timeout = 600 + +Friendly timeouts +----------------- + +Session timeouts are "friendly" timeouts. The plugin checks the session time at the end of +each test function, and stops further tests from running if the session timeout is exceeded. + +Combining session and function +------------------------------ + +It works fine to combine both session and function timeouts. +For example, to limit test functions to 5 seconds and the full session to 100 seconds:: + + pytest --timeout=5 --session-timeout=100 + + Changelog ========= @@ -353,6 +386,7 @@ Unreleased This change also switches all output from ``sys.stderr`` to ``sys.stdout``. Thanks Pedro Algarvio. - Pytest 7.0.0 is now the minimum supported version. Thanks Pedro Algarvio. +- Add ``--session-timeout`` option and ``session_timeout`` setting. 2.2.0 ----- diff --git a/pytest_timeout.py b/pytest_timeout.py index 800d0cd..de4878a 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -20,6 +20,7 @@ __all__ = ("is_debugging", "Settings") SESSION_TIMEOUT_KEY = pytest.StashKey[float]() +SESSION_EXPIRE_KEY = pytest.StashKey[float]() HAVE_SIGALRM = hasattr(signal, "SIGALRM") @@ -104,6 +105,7 @@ def pytest_addoption(parser): type="bool", default=False, ) + parser.addini("session_timeout", SESSION_TIMEOUT_DESC) class TimeoutHooks: @@ -159,12 +161,18 @@ def pytest_configure(config): config._env_timeout_func_only = settings.func_only config._env_timeout_disable_debugger_detection = settings.disable_debugger_detection - timeout = config.getoption("--session-timeout") + timeout = config.getoption("session_timeout") + if timeout is None: + ini = config.getini("session_timeout") + if ini: + timeout = _validate_timeout(config.getini("session_timeout"), "config file") if timeout is not None: expire_time = time.time() + timeout else: expire_time = 0 - config.stash[SESSION_TIMEOUT_KEY] = expire_time + timeout = 0 + config.stash[SESSION_TIMEOUT_KEY] = timeout + config.stash[SESSION_EXPIRE_KEY] = expire_time @pytest.hookimpl(hookwrapper=True) @@ -185,9 +193,9 @@ def pytest_runtest_protocol(item): hooks.pytest_timeout_cancel_timer(item=item) # check session timeout - expire_time = item.session.config.stash[SESSION_TIMEOUT_KEY] + expire_time = item.session.config.stash[SESSION_EXPIRE_KEY] if expire_time and (expire_time < time.time()): - timeout = item.session.config.getoption("--session-timeout") + timeout = item.session.config.stash[SESSION_TIMEOUT_KEY] item.session.shouldfail = f"session-timeout: {timeout} sec exceeded" @@ -223,7 +231,7 @@ def pytest_report_header(config): ) ) - session_timeout = config.getoption("--session-timeout") + session_timeout = config.getoption("session_timeout") if session_timeout: timeout_header.append("session timeout: %ss" % session_timeout) if timeout_header: diff --git a/test_pytest_timeout.py b/test_pytest_timeout.py index ac4e9fa..175eda8 100644 --- a/test_pytest_timeout.py +++ b/test_pytest_timeout.py @@ -623,17 +623,40 @@ def test_session_timeout(pytester): @pytest.fixture() def slow_setup_and_teardown(): - time.sleep(0.5) + time.sleep(1) yield - time.sleep(0.5) + time.sleep(1) def test_one(slow_setup_and_teardown): - time.sleep(0.5) + time.sleep(1) def test_two(slow_setup_and_teardown): - time.sleep(0.5) + time.sleep(1) """ ) - result = pytester.runpytest_subprocess("--session-timeout", "1.25") - result.stdout.fnmatch_lines(["*!! session-timeout: 1.25 sec exceeded !!!*"]) + result = pytester.runpytest_subprocess("--session-timeout", "2") + result.stdout.fnmatch_lines(["*!! session-timeout: 2.0 sec exceeded !!!*"]) + result.assert_outcomes(passed=1) + + +def test_ini_session_timeout(pytester): + pytester.makepyfile( + """ + import time + + def test_one(): + time.sleep(2) + + def test_two(): + time.sleep(2) + """ + ) + pytester.makeini( + """ + [pytest] + session_timeout = 1 + """ + ) + result = pytester.runpytest_subprocess() + result.stdout.fnmatch_lines(["*!! session-timeout: 1.0 sec exceeded !!!*"]) result.assert_outcomes(passed=1) From eef422c62f0a2b76fbb64b7d6de9cb3b624a5fdf Mon Sep 17 00:00:00 2001 From: Brian Okken <1568356+okken@users.noreply.github.com> Date: Thu, 8 Feb 2024 16:45:43 -0800 Subject: [PATCH 19/24] docs update --- README.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 85da61d..82809a6 100644 --- a/README.rst +++ b/README.rst @@ -350,8 +350,7 @@ of 10 minutes (600 seconds):: pytest --session-timeout=600 -You can also set the session timeout the `pytest configuration file`__ - using the ``session_timeout`` option: +You can also set the session timeout the `pytest configuration file`__ using the ``session_timeout`` option: .. code:: ini From a19403d3a3b72e26a939b0fe8b3ad9cee6b470a2 Mon Sep 17 00:00:00 2001 From: Brian Okken <1568356+okken@users.noreply.github.com> Date: Thu, 8 Feb 2024 16:50:18 -0800 Subject: [PATCH 20/24] readme --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 82809a6..75faa8d 100644 --- a/README.rst +++ b/README.rst @@ -350,7 +350,7 @@ of 10 minutes (600 seconds):: pytest --session-timeout=600 -You can also set the session timeout the `pytest configuration file`__ using the ``session_timeout`` option: +You can also set the session timeout the pytest configuration file using the ``session_timeout`` option: .. code:: ini From c6962b8194784fabe4aaa22ac5d5c2cc2a43825d Mon Sep 17 00:00:00 2001 From: Brian Okken <1568356+okken@users.noreply.github.com> Date: Sun, 25 Feb 2024 08:58:31 -0800 Subject: [PATCH 21/24] describe session timeout better --- README.rst | 20 +++++++++++++------- test_pytest_timeout.py | 12 +++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 75faa8d..33848c4 100644 --- a/README.rst +++ b/README.rst @@ -344,9 +344,15 @@ function: Session Timeout =============== -The above mentioned timeouts are all per test function. You can also set a -session timeout in seconds. The following example shows a session timeout -of 10 minutes (600 seconds):: +The above mentioned timeouts are all per test function. +The "per test function" timeouts will stop an individual test +from taking too long. We may also want to limit the time of the entire +set of tests running in one session. A session all of the tests +that will be run with one invokation of pytest. + +A session timeout is set with `--session-timeout` and is in seconds. + +The following example shows a session timeout of 10 minutes (600 seconds):: pytest --session-timeout=600 @@ -357,14 +363,14 @@ You can also set the session timeout the pytest configuration file using the ``s [pytest] session_timeout = 600 -Friendly timeouts +Cooperative timeouts ----------------- -Session timeouts are "friendly" timeouts. The plugin checks the session time at the end of +Session timeouts are cooperative timeouts. The plugin checks the session time at the end of each test function, and stops further tests from running if the session timeout is exceeded. -Combining session and function ------------------------------- +Combining session and function timeouts +--------------------------------------- It works fine to combine both session and function timeouts. For example, to limit test functions to 5 seconds and the full session to 100 seconds:: diff --git a/test_pytest_timeout.py b/test_pytest_timeout.py index 175eda8..e768d72 100644 --- a/test_pytest_timeout.py +++ b/test_pytest_timeout.py @@ -611,12 +611,9 @@ def test_foo(): def test_session_timeout(pytester): - # 2 tests, each with 0.5 sec timeouts - # each using a fixture with 0.5 sec setup and 0.5 sec teardown - # So about 1.5 seconds per test, ~3 sec total, - # Therefore: - # A timeout of 1.25 sec should happen during the teardown of the first test - # and the second test should NOT be run + # This is designed to timeout during hte first test to ensure + # - the first test still runs to completion + # - the second test is not started pytester.makepyfile( """ import time, pytest @@ -636,7 +633,8 @@ def test_two(slow_setup_and_teardown): ) result = pytester.runpytest_subprocess("--session-timeout", "2") result.stdout.fnmatch_lines(["*!! session-timeout: 2.0 sec exceeded !!!*"]) - result.assert_outcomes(passed=1) + # This would be 2 passed if the second test was allowed to run + result.assert_outcomes(passed=1) def test_ini_session_timeout(pytester): From 38c5f24e73cb73ea8a368024bf171921e8e500e7 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 7 Mar 2024 21:47:03 +0100 Subject: [PATCH 22/24] Tweak docs a little --- README.rst | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 33848c4..f45ae36 100644 --- a/README.rst +++ b/README.rst @@ -366,14 +366,25 @@ You can also set the session timeout the pytest configuration file using the ``s Cooperative timeouts ----------------- -Session timeouts are cooperative timeouts. The plugin checks the session time at the end of -each test function, and stops further tests from running if the session timeout is exceeded. +Session timeouts are cooperative timeouts. pytest-timeout checks the +session time at the end of each test function, and stops further tests +from running if the session timeout is exceeded. The session will +results in a test failure if this occurs. + +In particular this means if a test does not finish of itself, it will +only be interrupted if there is also a function timeout set. A +session timeout is not enough to ensure that a test-suite is +guaranteed to finish. Combining session and function timeouts --------------------------------------- -It works fine to combine both session and function timeouts. -For example, to limit test functions to 5 seconds and the full session to 100 seconds:: +It works fine to combine both session and function timeouts. In fact +when using a session timeout it is recommended to also provide a +function timeout. + +For example, to limit test functions to 5 seconds and the full session +to 100 seconds:: pytest --timeout=5 --session-timeout=100 @@ -392,6 +403,7 @@ Unreleased Thanks Pedro Algarvio. - Pytest 7.0.0 is now the minimum supported version. Thanks Pedro Algarvio. - Add ``--session-timeout`` option and ``session_timeout`` setting. + Thanks Brian Okken. 2.2.0 ----- From 48179d984d4a716b426dbf04e6b290d620518743 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 7 Mar 2024 21:55:00 +0100 Subject: [PATCH 23/24] Prep release --- README.rst | 4 ++-- setup.cfg | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index f45ae36..df8666d 100644 --- a/README.rst +++ b/README.rst @@ -392,8 +392,8 @@ to 100 seconds:: Changelog ========= -Unreleased ----------- +2.3.0 +----- - Fix debugger detection for recent VSCode, this compiles pydevd using cython which is now correctly detected. Thanks Adrian Gielniewski. diff --git a/setup.cfg b/setup.cfg index e49f697..7474ab8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ name = pytest-timeout description = pytest plugin to abort hanging tests long_description = file: README.rst -version = 2.2.0 +version = 2.3.0 author = Floris Bruynooghe author_email = flub@devork.be url = https://github.com/pytest-dev/pytest-timeout From 04432f5b10eec31433482e1b191c0d71f19acff5 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 7 Mar 2024 22:01:15 +0100 Subject: [PATCH 24/24] Some fixes to readme, bump version The last version is just never going to be published. That's fine. --- README.rst | 8 +++++++- setup.cfg | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index df8666d..5fc7c3d 100644 --- a/README.rst +++ b/README.rst @@ -364,7 +364,7 @@ You can also set the session timeout the pytest configuration file using the ``s session_timeout = 600 Cooperative timeouts ------------------ +-------------------- Session timeouts are cooperative timeouts. pytest-timeout checks the session time at the end of each test function, and stops further tests @@ -392,6 +392,12 @@ to 100 seconds:: Changelog ========= +2.3.1 +----- + +- Fixup some build errors, mostly README syntax which stopped twine + from uploading. + 2.3.0 ----- diff --git a/setup.cfg b/setup.cfg index 7474ab8..914ad08 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,8 @@ name = pytest-timeout description = pytest plugin to abort hanging tests long_description = file: README.rst -version = 2.3.0 +long_description_content_type = text/x-rst +version = 2.3.1 author = Floris Bruynooghe author_email = flub@devork.be url = https://github.com/pytest-dev/pytest-timeout