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/.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/.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/README.rst b/README.rst index ddf025c..5fc7c3d 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 ================== @@ -329,9 +340,77 @@ function: pytest.fail("+++ Timeout +++") + +Session Timeout +=============== + +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 + +You can also set the session timeout the pytest configuration file using the ``session_timeout`` option: + + .. code:: ini + + [pytest] + 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 +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. 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 + + Changelog ========= +2.3.1 +----- + +- Fixup some build errors, mostly README syntax which stopped twine + from uploading. + +2.3.0 +----- + +- 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. +- 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 ----- diff --git a/pytest_timeout.py b/pytest_timeout.py index 674aab6..de4878a 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -8,10 +8,10 @@ """ import inspect import os -import shutil import signal import sys import threading +import time import traceback from collections import namedtuple @@ -19,6 +19,8 @@ __all__ = ("is_debugging", "Settings") +SESSION_TIMEOUT_KEY = pytest.StashKey[float]() +SESSION_EXPIRE_KEY = pytest.StashKey[float]() HAVE_SIGALRM = hasattr(signal, "SIGALRM") @@ -44,6 +46,11 @@ When specified, disables debugger detection. breakpoint(), pdb.set_trace(), etc. will be interrupted by the timeout. """.strip() +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() # bdb covers pdb, ipdb, and possibly others # pydevd covers PyCharm, VSCode, and possibly others @@ -80,6 +87,15 @@ def pytest_addoption(parser): action="store_true", help=DISABLE_DEBUGGER_DETECTION_DESC, ) + group.addoption( + "--session-timeout", + action="store", + dest="session_timeout", + default=None, + type=float, + metavar="SECONDS", + help=SESSION_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) @@ -89,6 +105,7 @@ def pytest_addoption(parser): type="bool", default=False, ) + parser.addini("session_timeout", SESSION_TIMEOUT_DESC) class TimeoutHooks: @@ -144,6 +161,19 @@ 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") + 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 + timeout = 0 + config.stash[SESSION_TIMEOUT_KEY] = timeout + config.stash[SESSION_EXPIRE_KEY] = expire_time + @pytest.hookimpl(hookwrapper=True) def pytest_runtest_protocol(item): @@ -162,6 +192,12 @@ def pytest_runtest_protocol(item): if is_timeout and settings.func_only is False: hooks.pytest_timeout_cancel_timer(item=item) + # check session timeout + expire_time = item.session.config.stash[SESSION_EXPIRE_KEY] + if expire_time and (expire_time < time.time()): + timeout = item.session.config.stash[SESSION_TIMEOUT_KEY] + item.session.shouldfail = f"session-timeout: {timeout} sec exceeded" + @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(item): @@ -183,15 +219,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, ) - ] + ) + + session_timeout = config.getoption("session_timeout") + if session_timeout: + timeout_header.append("session timeout: %ss" % session_timeout) + if timeout_header: + return timeout_header @pytest.hookimpl(tryfirst=True) @@ -236,8 +280,13 @@ 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 @@ -440,11 +489,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) @@ -456,6 +506,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: @@ -463,30 +514,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(): @@ -498,31 +550,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))) diff --git a/setup.cfg b/setup.cfg index be6b63f..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.2.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 @@ -31,7 +32,7 @@ classifiers = [options] py_modules = pytest_timeout install_requires = - pytest>=5.0.0 + pytest>=7.0.0 python_requires = >=3.7 [options.entry_points] diff --git a/test_pytest_timeout.py b/test_pytest_timeout.py index da735eb..e768d72 100644 --- a/test_pytest_timeout.py +++ b/test_pytest_timeout.py @@ -16,30 +16,26 @@ ) -@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", "--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", + ] ) @have_sigalrm -def test_sigalrm(testdir): - testdir.makepyfile( +def test_sigalrm(pytester): + pytester.makepyfile( """ import time @@ -47,12 +43,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 +56,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 +65,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 +82,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 +93,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 +106,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 +122,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 +130,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 +147,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 +168,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 +194,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 +221,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 +238,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 +252,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 +266,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 +280,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 +294,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 +308,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 +322,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 +335,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 +357,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 +381,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 +401,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 +428,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 +459,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 +472,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 +505,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 +518,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 +549,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 +568,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 +592,7 @@ def pytest_timeout_cancel_timer(item): return True """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -602,10 +601,60 @@ def test_foo(): pass """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest_subprocess("-s") result.stdout.fnmatch_lines( [ "pytest_timeout_set_timer", "pytest_timeout_cancel_timer", ] ) + + +def test_session_timeout(pytester): + # 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 + + @pytest.fixture() + def slow_setup_and_teardown(): + time.sleep(1) + yield + time.sleep(1) + + def test_one(slow_setup_and_teardown): + time.sleep(1) + + def test_two(slow_setup_and_teardown): + time.sleep(1) + """ + ) + result = pytester.runpytest_subprocess("--session-timeout", "2") + result.stdout.fnmatch_lines(["*!! session-timeout: 2.0 sec exceeded !!!*"]) + # This would be 2 passed if the second test was allowed to run + 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) diff --git a/tox.ini b/tox.ini index 966ea49..8dbfa6a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,9 @@ [pytest] -minversion = 2.8 +minversion = 7.0 addopts = -ra [tox] -envlist = py37,py38,py39,py310,py311,pypy3 +envlist = py37,py38,py39,py310,py311,py312,pypy3 [testenv] deps = pytest @@ -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)