背景
經常做pytest插件開發的話, 一定會看到不少如下代碼片段:
1
2
3
4
5
|
def
pytest_configure(config):
...
# prevent ... on slave nodes (xdist)
if
not
hasattr
(config,
'slaveinput'
):
...
|
其實這些代碼都是為了兼容一個叫pytest-xdist
的插件的.簡單介紹一下這款插件, pytest-xdist這款插件允許用戶將測試並發執行(進程級並發). 主要開發者是pytest目前的核心開發人員Bruno Oliveira, 截至寫作時, 該項目已有371個start, 應用於4150個項目. 需要注意的是, 由於插件是動態決定測試用例執行順序的,為了保證各個測試能在各個獨立線程里正確的執行, 用例的作者應該保證測試用例的獨立性(這也符合測試用例設計的最佳實踐).
流程
這里介紹了插件的執行原理, 我作了簡單的翻譯並且加了一部分注解.
和大多數的分布式系統相似, xdist
里有master和worker的概念.master負責整個測試任務的調度, 測試報告等工作, 而worker則是實際執行測試的宿主進程.
具體的測試執行的流程如下:
-
在test session的起始階段,
xdist
會spawn一個或者多個worker進程. master和worker間的通信基於 execnet 和它的gateways. worker的解釋器可以是本地或者遠程的. -
收集測試項:
每個worker是個迷你的
pytest runner
對象. workers這時會執行一個完整test collection過程, 然后將結果發回到master(master本身不做測試收集工作). -
測試收集檢查:
master收到這些節點發回的結果后, 執行一些sanity檢查以確保所有worker節點都收集到相同的測試項(包括順序). 當所有的檢查都通過后, 再將這些測試項轉換為一個簡單的索引列表, 每個索引對應一個測試項的在原來測試集中的位置. 這個方案可行的原因是所有的節點都保存着相同的測試集, 並且使用這種方式可以節省帶寬, 因為master只需要告知節點需要執行的測試項對應的索引, 而不用告知完整的測試項信息.
FAQ環節其實提到, 在各個node上單獨執行測試收集工作是因為如果在master上執行測試收集,那么就需要作很多序列化處理, 因為worker是進程級的. 這會使問題復雜化, 並且使pytest變得不易於維護.
-
測試分發:
- 如果
dist-mode
是each, 那么這時master只需將完整的列表發送給每個節點. - 如果
dist-mode
是load, 那么這時master會將大約25%的測試項以輪詢的方式發往各個worker. 剩余的測試項則會等待workers執行完測試以后分發, 見下文.
注意:
pytest_xdist_make_scheduler
這個hook可以用於實現自定義的分發邏輯. - 如果
-
測試執行:
workers 重寫了
pytest_runtestloop
: pytest的默認實現基本上是循環執行所有在session
這個對象里面收集到的測試項, 但是在xdist
里, workers實際上是等待master為其發送需要執行的測試項的. 當worker收到測試任務, 就順序執行pytest_runtest_protocol
. 值得注意的一個細節是:workers 必須始終保持至少一個測試項在的任務隊列里, 以兼容pytest_runtest_protocol(item, nextitem)
hook的參數要求.為了將nextitem
傳給hook, worker會在執行最后一個測試項前等待master的更多指令.如果它收到了更多測試項, 那么久可以安全的執行pytest_runtest_protocol
, 因為這時nextitem
參數已經可以確定. 如果它收到一個 "shutdown"信號, 那么就將nextitem
參數設為None
, 然后執行pytest_runtest_protocol
. -
測試分發(Load模式):
當測試項在 workers里的開始/結束執行時, 測試結果會發回到master, 這樣其他pytest hooks比如
pytest_runtest_logstart
和pytest_runtest_logreport
就可以正常執行.master (處於load的dist-mode
時)在節點執行完一個測試后, 基於測試執行時長以及每個節點剩余測試項綜合決定是否向這個節點發送更多的測試項. -
測試結束:
當master沒有更多待執行測試項時, 它會發送一個"shutdown"信號給所有workers, worker將剩余的測試項執行完畢並退出進程. master則一直等待workers全部退出, 當然此時任然需要處理諸如
pytest_runtest_logreport
等事件.
Best Practice
在了解了pytest-xdist
的實現原理后, 為了保證開發的插件能夠正常與其配合(沒辦法, 這個插件太流行了), 建議在插件開發時:
-
對於只需在master上執行的代碼, 比如
report
類插件, 通常只需在master節點上初始化一遍並處理各個report對象. 我們可以通過判斷hasattr(config, 'slaveinput')
來確定是否為worker節點, 區分處理相邏輯; -
由於測試執行實際是在各個worker節點上執行的, 在
pytest_runtest_makereport
等hooks里要避免對象實例化操作, 因為你的實例化對象在序列化時會報錯, 比如某些測試使用了下面的conftest.py
文件:12345678910111213141516import
pytest
class
SomeThing(
object
):
pass
@pytest
.hookimpl(hookwrapper
=
True
)
def
pytest_runtest_makereport(item, call):
outcome
=
yield
report
=
outcome.get_result()
report.something
=
SomeThing()
def
pytest_runtest_logreport(report):
print
(
'something: %r'
%
report.something)
那么當你使用pytest -n執行時, 就會報類似這樣的錯誤:
INTERNALERROR> raise DumpError("can't serialize {}".format(tp))
INTERNALERROR> execnet.gateway_base.DumpError: can't serialize <class 'conftest.SomeThing'>正確的做法是, 將需要保存的數據保存到
report
對象, 比如下面這段代碼可以將測試執行的時間戳保存在report
對象里, 之后worker便會將report
同步給master節點: -
123456
def
pytest_runtest_makereport(item, call):
outcome
=
yield
report
=
outcome.get_result()
if
report.when
=
=
"call"
:
report.call_start
=
call.start
report.call_end
=
call.stop
-
目前發現除了自定義的類以外, 諸如
datetime
類型也是不能直接序列化的, 遇到這種情況可以考慮將其保存為timestamp, 之后再做類型轉換操作. -
還有一種典型的錯誤是, 將諸如
pytest_runtest_makereport
的hook函數寫成類的方法, 由於此類hook函數是在worker節點執行的, 如果這個類只在master節點上進行了實例化, 相當於寫了個無效的hook函數, 而且這時雖然程序不會報任何錯, 這點要特別注意.
總之, 牢記
config
對象是進程間獨立的, 但是report
對象之間的值可以互相同步的, 但是要避免同步對象; -