diff --git a/pytest_timeout.py b/pytest_timeout.py index 674aab6..5c207b3 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -44,12 +44,15 @@ When specified, disables debugger detection. breakpoint(), pdb.set_trace(), etc. will be interrupted by the timeout. """.strip() +SKIP_DESC = """ +When set to True, timeout trigger pytest.skip() instead of pytest.fail(). +""".strip() # bdb covers pdb, ipdb, and possibly others # pydevd covers PyCharm, VSCode, and possibly others KNOWN_DEBUGGING_MODULES = {"pydevd", "bdb", "pydevd_frame_evaluator"} Settings = namedtuple( - "Settings", ["timeout", "method", "func_only", "disable_debugger_detection"] + "Settings", ["timeout", "method", "func_only", "disable_debugger_detection", "skip"] ) @@ -80,15 +83,22 @@ def pytest_addoption(parser): action="store_true", help=DISABLE_DEBUGGER_DETECTION_DESC, ) + group.addoption( + "--timeout-skip", + dest="timeout_skip", + action="store_true", + help=SKIP_DESC, + ) parser.addini("timeout", TIMEOUT_DESC) parser.addini("timeout_method", METHOD_DESC) parser.addini("timeout_func_only", FUNC_ONLY_DESC, type="bool", default=False) parser.addini( "timeout_disable_debugger_detection", - DISABLE_DEBUGGER_DETECTION_DESC, + SKIP_DESC, type="bool", default=False, ) + parser.addini("skip", SKIP_DESC, type="bool", default=False) class TimeoutHooks: @@ -135,7 +145,8 @@ def pytest_configure(config): "evaluating any fixtures used in the test. The " "*disable_debugger_detection* keyword, when set to True, disables " "debugger detection, allowing breakpoint(), pdb.set_trace(), etc. " - "to be interrupted", + "to be interrupted. The *skip* keyword, when set to True, the timeout " + "trigger pytest.skip() instead of pytest.fail().", ) settings = get_env_settings(config) @@ -143,6 +154,7 @@ def pytest_configure(config): config._env_timeout_method = settings.method config._env_timeout_func_only = settings.func_only config._env_timeout_disable_debugger_detection = settings.disable_debugger_detection + config._env_timeout_skip = settings.skip @pytest.hookimpl(hookwrapper=True) @@ -190,6 +202,7 @@ def pytest_report_header(config): config._env_timeout, config._env_timeout_method, config._env_timeout_func_only, + config._env_timeout_skip, ) ] @@ -328,8 +341,13 @@ def get_env_settings(config): disable_debugger_detection = _validate_disable_debugger_detection( ini, "config file" ) + skip = config.getvalue("skip") + if skip is None: + ini = config.getini("skip") + if ini: + method = _validate_skip(ini, "config file") - return Settings(timeout, method, func_only, disable_debugger_detection) + return Settings(timeout, method, func_only, disable_debugger_detection, skip) def _get_item_settings(item, marker=None): @@ -345,6 +363,7 @@ def _get_item_settings(item, marker=None): disable_debugger_detection = _validate_disable_debugger_detection( settings.disable_debugger_detection, "marker" ) + skip = _validate_func_only(settings.skip, "marker") if timeout is None: timeout = item.config._env_timeout if method is None: @@ -353,7 +372,9 @@ def _get_item_settings(item, marker=None): func_only = item.config._env_timeout_func_only if disable_debugger_detection is None: disable_debugger_detection = item.config._env_timeout_disable_debugger_detection - return Settings(timeout, method, func_only, disable_debugger_detection) + if skip is None: + func_only = item.config._env_timeout_skip + return Settings(timeout, method, func_only, disable_debugger_detection, skip) def _parse_marker(marker): @@ -372,6 +393,8 @@ def _parse_marker(marker): method = val elif kw == "func_only": func_only = val + elif kw == "skip": + func_only = val else: raise TypeError("Invalid keyword argument for timeout marker: %s" % kw) if len(marker.args) >= 1 and timeout is not NOTSET: @@ -390,7 +413,9 @@ def _parse_marker(marker): method = None if func_only is NOTSET: func_only = None - return Settings(timeout, method, func_only, None) + if skip is NOTSET: + skip = None + return Settings(timeout, method, func_only, None, skip) def _validate_timeout(timeout, where): @@ -429,6 +454,14 @@ def _validate_disable_debugger_detection(disable_debugger_detection, where): return disable_debugger_detection +def _validate_func_only(skip, where): + if skip is None: + return None + if not isinstance(skip, bool): + raise ValueError("Invalid skip value %s from %s" % (skip, where)) + return skip + + def timeout_sigalrm(item, settings): """Dump stack of threads and raise an exception. @@ -436,6 +469,7 @@ def timeout_sigalrm(item, settings): current to stderr and then raise an AssertionError, thus terminating the test. """ + skip = settings.skip if not settings.disable_debugger_detection and is_debugging(): return __tracebackhide__ = True @@ -445,7 +479,10 @@ def timeout_sigalrm(item, settings): dump_stacks() if nthreads > 1: write_title("Timeout", sep="+") - pytest.fail("Timeout >%ss" % settings.timeout) + if skip: + pytest.skip("Timeout >%ss" % settings.timeout) + else: + pytest.fail("Timeout >%ss" % settings.timeout) def timeout_timer(item, settings):