子程序支援

通常覆蓋範圍會透過十分標準的 atexit 處理常式撰寫資料。但是,如果子程序未自行離開,則 atexit 處理常式可能會不會執行。發生這種情況的原因,最好留給冒險家透過瀏覽 Python 錯誤追蹤器來找出。

pytest-cov 支援子程序,並會變通處理這些 atexit 限制。不過,有幾個需要說明的潛在問題。

但首先,pytest-cov 的子程序支援是如何運作的?

pytest-cov 封裝會在安裝中注入一個 pytest-cov.pth。此檔案可有效地在每個 Python 啟動時執行以下動作

if 'COV_CORE_SOURCE' in os.environ:
    try:
        from pytest_cov.embed import init
        init()
    except Exception as exc:
        sys.stderr.write(
            "pytest-cov: Failed to setup subprocess coverage. "
            "Environ: {0!r} "
            "Exception: {1!r}\n".format(
                dict((k, v) for k, v in os.environ.items() if k.startswith('COV_CORE')),
                exc
            )
        )

Pytest 外掛元件會設定這個 COV_CORE_SOURCE 環境變數,因此繼承環境變數 (預設行為) 的任何子程序都會執行 pytest_cov.embed.init,而這會依這些變數設定涵蓋範圍

  • COV_CORE_SOURCE

  • COV_CORE_CONFIG

  • COV_CORE_DATAFILE

  • COV_CORE_BRANCH

  • COV_CORE_CONTEXT

您可能想知道為什麼它有 COV_CORE?嗯,這主要是基於歷史原因:很久以前,pytest-cov 依賴於一個實作 pytest-cov、nose-cov 和 nose2-cov 通用功能的 cov-core 封裝。依賴消失了,但慣例還保留著。這可以進行變更,但會中斷手動設定這些預期為內部但遺憾的是並非內部的環境變數的所有專案。

覆蓋範圍的子程序支援

現在您瞭解 pytest-cov 的運作方式後,您可以輕易找出使用 覆蓋範圍推薦 的處理子程序方式,方法是在 .pth 檔案或 sitecustomize.py 檔案中載入這項內容,將會中斷所有事物

import coverage; coverage.process_startup()  # this will break pytest-cov

不要這樣做,因為這樣會重新啟動覆蓋範圍,而且選項不正確。

如果您使用 multiprocessing

在 pytest-cov 4.0 中已移除對多重處理的內建支援。此支援大多是運作的,但在某些情況下會出現嚴重錯誤 (請參閱 問題 82408),而且會讓測試套件極不穩定且執行緩慢。

但是,覆蓋範圍中內建多重處理支援,而且您可以移轉到該支援。您只需在您偏好的組態檔案 (範例:.coveragerc) 中這麼做

[run]
concurrency = multiprocessing
parallel = true
sigterm = true

現在,附帶一提,使用 Pool.join() 適當地關閉您的 Pool 是一個好主意

from multiprocessing import Pool

def f(x):
    return x*x

if __name__ == '__main__':
    p = Pool(5)
    try:
        print(p.map(f, [1, 2, 3]))
    finally:
        p.close()  # Marks the pool as closed.
        p.join()   # Waits for workers to exit.

訊號處理常式

pytest-cov 提供訊號處理常式,主要用於您擁有自訂訊號處理,不允許 atexit 正確執行,以及現已移除的多重處理支援的特殊情況

  • pytest_cov.embed.cleanup_on_sigterm()

  • pytest_cov.embed.cleanup_on_signal(signum) (例如:cleanup_on_signal(signal.SIGHUP))

如果您使用多重處理

不建議在多處理程序中使用這些信號處理程式,因為註冊信號處理程式會造成程序組死鎖,請見:https://bugs.python.org/issue38227

如果你有自訂信號處理程式

pytest-cov 2.6 有個基礎的 pytest_cov.embed.cleanup_on_sigterm 可以用來註冊 SIGTERM 處理程式,用以刷新覆蓋率資料。

pytest-cov 2.7 新增了 pytest_cov.embed.cleanup_on_signal 函式並修改實作使其更健全:處理程式會呼叫上一個處理程式(如果你先前已經註冊過),且會重新進入(如果在執行處理程式時發送多餘的信號,則會暫緩這些信號)。

例如,如果你要在 SIGHUP 上重新載入,則應該執行類似的動作

import os
import signal

def restart_service(frame, signum):
    os.exec( ... )  # or whatever your custom signal would do
signal.signal(signal.SIGHUP, restart_service)

try:
    from pytest_cov.embed import cleanup_on_signal
except ImportError:
    pass
else:
    cleanup_on_signal(signal.SIGHUP)

請注意,cleanup_on_signalcleanup_on_sigterm 都會執行上一個信號處理程式。

或者你可以執行以下動作

import os
import signal

try:
    from pytest_cov.embed import cleanup
except ImportError:
    cleanup = None

def restart_service(frame, signum):
    if cleanup is not None:
        cleanup()

    os.exec( ... )  # or whatever your custom signal would do
signal.signal(signal.SIGHUP, restart_service)

如果你使用 Windows

在 Windows 上,你可以註冊一個 SIGTERM 處理程式,但它實際上並不執行。它會在 os.kill(os.getpid(), signal.SIGTERM)(將 SIGTERM 送到目前處理程序)的情況下執行,但對於大多數意圖和目的而言,它完全無用。

因此,這表示如果你使用多處理程序,則別無選擇,只能使用上面描述的關閉/加入模式。由於它依賴 SIGTERM,因此使用情境管理員 API 或 terminate 是無法執行的。

不過,你可以使用 SIGBREAK 的處理程式(有一些警告)

import os
import signal

def shutdown(frame, signum):
    # your app's shutdown or whatever
signal.signal(signal.SIGBREAK, shutdown)

try:
    from pytest_cov.embed import cleanup_on_signal
except ImportError:
    pass
else:
    cleanup_on_signal(signal.SIGBREAK)

大約有以下 警告

  • 你需要傳送 signal.CTRL_BREAK_EVENT

  • 它會傳送到整個處理程序群組,這可能會產生無法預見的後果