pytest-timeout - 当测试超时时失败而不是杀死整个测试运行

4
我知道在pytest-timeout中,我可以为每个测试用例指定超时时间,但是单个失败会终止整个测试运行而不是只是失败的测试用例。
我是否被迫自己解决这个问题,或者有现成的工具可以提供这个功能?
2个回答

3

我很久以前就研究过这个问题,也得出结论,自制的解决方案会更好。

我的插件会终止整个pytest进程,但可以轻松调整为仅使单个(当前)测试失败。以下是调整后的草稿:

import pytest
import signal


class Termination(SystemExit):
    pass


class TimeoutExit(BaseException):
    pass


def _terminate(signum, frame):
    raise Termination("Runner is terminated from outside.")


def _timeout(signum, frame):
    raise TimeoutExit("Runner timeout is reached, runner is terminating.")


@pytest.hookimpl
def pytest_addoption(parser):
    parser.addoption(
        '--timeout', action='store', dest='timeout', type=int, default=None,
        help="number of seconds before each test failure")


@pytest.hookimpl
def pytest_configure(config):
    # Install the signal handlers that we want to process.
    signal.signal(signal.SIGTERM, _terminate)
    signal.signal(signal.SIGALRM, _timeout)


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_protocol(item, nextitem):

    # Set the per-test timeout (an alarm signal).
    if item.config.option.timeout is not None:
        signal.alarm(item.config.option.timeout)

    try:
        # Run the setup, test body, and teardown stages.
        yield
    finally:
        # Disable the alarm when the test passes or fails.
        # I.e. when we get into the framework's body.
        signal.alarm(0)

当你执行kill -ALRM $pid或每个测试因预设的闹钟超时而单独失败时,只有当前测试会失败,但其他测试将继续执行。
而这个TimeoutExit不会被执行except Exception: pass的库所抑制,因为它继承自BaseException
因此,在这方面,它类似于SystemExit。但是,与SystemExitKeyboardInterruption不同,pytest不会捕获它,并且不会在此类异常上退出。
无论测试在警报发生时做什么操作(即使它进行time.sleep(...)等任何信号),异常都将注入其中。
记住,您只能为进程设置一个单独的警报(操作系统限制)。这也使其与pytest-timeout不兼容,因为它也使用ALRM信号来实现相同的目的。
如果要同时设置全局和每个测试的超时时间,必须实现智能警报管理器,该管理器将跟踪几个警报,将OS警报设置为最早的警报,并在接收到警报信号时决定调用哪个处理程序。
kill -TERM $pid或简单的kill $pid(优雅的终止)情况下,由于它继承自SystemExit,因此将立即终止-这通常不会被代码或pytest捕获。
后一种情况主要演示了如何对不同的信号设置不同的反应。您可以对USR1和USR2及其他可捕获的信号执行类似的操作。
对于快速测试,请将插件代码放在conftest.py文件中(伪插件)。
考虑以下测试文件:
import time

def test_this():
    try:
        time.sleep(10)
    except Exception:
        pass

def test_that():
    pass

在没有设置超时的情况下运行pytest不会有任何效果,两个测试都会通过:

$ pytest -s -v
.........
collected 2 items                                                                                                                                                                 

test_me.py::test_this PASSED
test_me.py::test_that PASSED

======= 2 passed in 10.02 seconds =======

使用超时运行它会导致第一个测试失败,但通过第二个测试:

$ pytest -s -v --timeout=5
.........
collected 2 items                                                                                                                                                                 

test_me.py::test_this FAILED
test_me.py::test_that PASSED

============== FAILURES ==============
______________ test_this _____________

    def test_this():
        try:
>           time.sleep(10)

test_me.py:5: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

signum = 14, frame = <frame object at 0x106b3c428>

    def _timeout(signum, frame):
>       raise TimeoutExit("Runner timeout is reached, runner is terminating.")
E       conftest.pytest_configure.<locals>.TimeoutExit: Runner timeout is reached, runner is terminating.

conftest.py:24: TimeoutExit
======= 1 failed, 1 passed in 5.11 seconds =======

{btsdaf} - WloHu
为什么不与上游开发人员交流并改进pytest-timeout呢? - guettli
在这里请求改进 https://bitbucket.org/pytest-dev/pytest-timeout/issues/39/use-timeout-to-make-a-test-fails-not-to - Andrea Bisello

0
这一点从一开始就得到了 pytest-timeout 的全面支持,您可以使用在 pytest-timeout 的 readme 中描述的 signal 方法。请仔细阅读 readme,因为它带有一些注意事项。实际上,它是使用 SIGALRM 实现的,正如其他答案所建议的那样,但它已经存在,所以不需要重新实现。

有没有一个解决方案可以在Windows机器上执行这种行为?它明确指出,在没有SIGALRM的操作系统上,它将无法工作。 - LeanMan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接