pytest插件探索——pytest-xdist

背景

經(jīng)常做pytest插件開發(fā)的話, 一定會看到不少如下代碼片段:

def pytest_configure(config):
    ...
    # prevent ... on slave nodes (xdist)
    if not hasattr(config, 'slaveinput'):
        ...

其實這些代碼都是為了兼容一個叫pytest-xdist的插件的.簡單介紹一下這款插件, pytest-xdist這款插件允許用戶將測試并發(fā)執(zhí)行(進程級并發(fā)). 主要開發(fā)者是pytest目前的核心開發(fā)人員Bruno Oliveira, 截至寫作時, 該項目已有371個star, 應(yīng)用于4150個項目. 需要注意的是, 由于插件是動態(tài)決定測試用例執(zhí)行順序的,為了保證各個測試能在各個獨立線程里正確的執(zhí)行, 用例的作者應(yīng)該保證測試用例的獨立性(這也符合測試用例設(shè)計的最佳實踐).

流程

這里介紹了插件的執(zhí)行原理, 我作了簡單的翻譯并且加了一部分注解.

和大多數(shù)的分布式系統(tǒng)相似, xdist里有masterworker的概念.master負責(zé)整個測試任務(wù)的調(diào)度, 測試報告等工作, 而worker則是實際執(zhí)行測試的宿主進程.

具體的測試執(zhí)行的流程如下:

  1. 在test session的起始階段, xdist會spawn一個或者多個worker進程. masterworker間的通信基于 execnet 和它的gateways. worker的解釋器可以是本地或者遠程的.

  2. 收集測試項:

    每個worker是個迷你的pytest runner對象. workers這時會執(zhí)行一個完整test collection過程, 然后將結(jié)果發(fā)回到master(master本身不做測試收集工作).

  3. 測試收集檢查:

    master收到這些節(jié)點發(fā)回的結(jié)果后, 執(zhí)行一些sanity檢查以確保所有worker節(jié)點都收集到相同的測試項(包括順序). 當(dāng)所有的檢查都通過后, 再將這些測試項轉(zhuǎn)換為一個簡單的索引列表, 每個索引對應(yīng)一個測試項的在原來測試集中的位置. 這個方案可行的原因是所有的節(jié)點都保存著相同的測試集, 并且使用這種方式可以節(jié)省帶寬, 因為master只需要告知節(jié)點需要執(zhí)行的測試項對應(yīng)的索引, 而不用告知完整的測試項信息.

    FAQ環(huán)節(jié)其實提到, 在各個node上單獨執(zhí)行測試收集工作是因為如果在master上執(zhí)行測試收集,那么就需要作很多序列化處理, 因為worker是進程級的. 這會使問題復(fù)雜化, 并且使pytest變得不易于維護.

  4. 測試分發(fā):

    • 如果dist-modeeach, 那么這時master只需將完整的列表發(fā)送給每個節(jié)點.
    • 如果dist-modeload, 那么這時master會將大約25%的測試項以輪詢的方式發(fā)往各個worker. 剩余的測試項則會等待workers執(zhí)行完測試以后分發(fā), 見下文.

    注意: pytest_xdist_make_scheduler 這個hook可以用于實現(xiàn)自定義的分發(fā)邏輯.

  5. 測試執(zhí)行:

    workers 重寫了 pytest_runtestloop: pytest的默認實現(xiàn)基本上是循環(huán)執(zhí)行所有在session這個對象里面收集到的測試項, 但是在xdist里, workers實際上是等待master為其發(fā)送需要執(zhí)行的測試項的. 當(dāng)worker收到測試任務(wù), 就順序執(zhí)行 pytest_runtest_protocol. 值得注意的一個細節(jié)是:workers 必須始終保持至少一個測試項在的任務(wù)隊列里, 以兼容pytest_runtest_protocol(item, nextitem) hook的參數(shù)要求.為了將 nextitem傳給hook, worker會在執(zhí)行最后一個測試項前等待master的更多指令.如果它收到了更多測試項, 那么久可以安全的執(zhí)行 pytest_runtest_protocol , 因為這時nextitem參數(shù)已經(jīng)可以確定. 如果它收到一個 "shutdown"信號, 那么就將 nextitem 參數(shù)設(shè)為 None, 然后執(zhí)行pytest_runtest_protocol .

  6. 測試分發(fā)(Load模式):

    當(dāng)測試項在 workers里的開始/結(jié)束執(zhí)行時, 測試結(jié)果會發(fā)回到master, 這樣其他pytest hooks比如pytest_runtest_logstartpytest_runtest_logreport就可以正常執(zhí)行.master (處于loaddist-mode時)在節(jié)點執(zhí)行完一個測試后, 基于測試執(zhí)行時長以及每個節(jié)點剩余測試項綜合決定是否向這個節(jié)點發(fā)送更多的測試項.

  7. 測試結(jié)束:

    當(dāng)master沒有更多待執(zhí)行測試項時, 它會發(fā)送一個"shutdown"信號給所有workers, worker將剩余的測試項執(zhí)行完畢并退出進程. master則一直等待workers全部退出, 當(dāng)然此時任然需要處理諸如pytest_runtest_logreport等事件.

Best Practice

在了解了pytest-xdist的實現(xiàn)原理后, 為了保證開發(fā)的插件能夠正常與其配合(沒辦法, 這個插件太流行了), 建議在插件開發(fā)時:

  • 對于只需在master上執(zhí)行的代碼, 比如report類插件, 通常只需在master節(jié)點上初始化一遍并處理各個report對象. 我們可以通過判斷 hasattr(config, 'slaveinput')來確定是否為worker節(jié)點, 區(qū)分處理相邏輯;

  • 由于測試執(zhí)行實際是在各個worker節(jié)點上執(zhí)行的, 在pytest_runtest_makereport等hooks里要避免對象實例化操作, 因為你的實例化對象在序列化時會報錯, 比如某些測試使用了下面的conftest.py文件:

    import 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)
    

    那么當(dāng)你使用pytest -n執(zhí)行時, 就會報類似這樣的錯誤:

    INTERNALERROR> raise DumpError("can't serialize {}".format(tp))
    INTERNALERROR> execnet.gateway_base.DumpError: can't serialize <class 'conftest.SomeThing'>

    正確的做法是, 將需要保存的數(shù)據(jù)保存到report對象, 比如下面這段代碼可以將測試執(zhí)行的時間戳保存在report對象里, 之后worker便會將report同步給master節(jié)點:

    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
    
  • 目前發(fā)現(xiàn)除了自定義的類以外, 諸如datetime類型也是不能直接序列化的, 遇到這種情況可以考慮將其保存為timestamp, 之后再做類型轉(zhuǎn)換操作.

  • 還有一種典型的錯誤是, 將諸如pytest_runtest_makereport的hook函數(shù)寫成類的方法, 由于此類hook函數(shù)是在worker節(jié)點執(zhí)行的, 如果這個類只在master節(jié)點上進行了實例化, 相當(dāng)于寫了個無效的hook函數(shù), 而且這時雖然程序不會報任何錯, 這點要特別注意.

總之, 牢記config對象是進程間獨立的, 但是report對象之間的值可以互相同步的, 但是要避免同步對象;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容