pytestプラグイン探索-pytest-xdist

4868 ワード

背景


pytestプラグインの開発を頻繁に行うと、次のようなコードクリップがたくさん見られます.
def pytest_configure(config):
    ...
    # prevent ... on slave nodes (xdist)
    if not hasattr(config, 'slaveinput'):
        ...


これらのコードはpytest-xdistというプラグインと互換性があるためです.このプラグインを簡単に紹介します.pytest-xdistこのプラグインはユーザーがテストを同時に実行することを許可します(プロセスレベルの同時実行).主な開発者はpytestの現在の核心開発者Bruno Oliveiraであり、執筆時までに371個のstarがあり、4150個のプロジェクトに応用された.なお、プラグインは試験用例の実行順序を動的に決定するため、各試験が各独立スレッドで正しく実行できることを保証するために、用例の著者は試験用例の独立性を保証すべきである(これも試験用例設計のベストプラクティスに合致する).

プロセス


ここではプラグインの実行原理を紹介し、簡単な翻訳を行い、一部の注釈を加えた.
ほとんどの分布式システムと似ており、xdistにはmasterとworkerの概念がある.masterはテストタスク全体のスケジューリング、テストレポートなどの仕事を担当し、workerは実際にテストを実行する宿主プロセスである.
具体的なテスト実行の流れは以下の通りです.
  • testセッションの開始段階において、xdistは1つ以上のworkerプロセスをspawnする.マスターとworker間の通信はexecnetとそのgatewaysに基づいている.workerの解釈器はローカルまたはリモートであることができる.
  • 収集試験項目:各workerはミニpytest runnerオブジェクトである.workersは、完全なtest collectionプロセスを実行し、結果をmasterに返します(master自体はテスト収集を行いません).
  • 試験収集検査:masterはこれらのノードから返信する結果を受け取った後、sanity検査をいくつか実行して、すべてのworkerノードが同じ試験項目(順序を含む)を収集することを確保する.すべての検査が合格すると、これらの試験項目を簡単なインデックスリストに変換し、各インデックスは1つの試験項目の元の試験セットの位置に対応する.このスキームは、すべてのノードが同じテストセットを保存するため、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が順次実行する.注意すべき1つの詳細は、ワークersがpytest_runtest_protocol(item, nextitem) hookのパラメータ要件と互換性を持つために、少なくとも1つのテスト項目があるタスクキューに常に保持されなければならないことである.nextitemをhookに渡すために、workerは最後のテスト項目を実行する前にmasterのより多くの命令を待つ.より多くの試験項目が受信場合、pytest_runtest_protocolパラメータが決定できるため、nextitemを安全に実行することができる.「shutdown」信号を受信と、nextitemのパラメータをNoneに設定し、pytest_runtest_protocolを実行する.
  • 試験配布(Loadモード):試験項目がworkersでの開始/終了時に試験結果がmasterに戻る、pytest_runtest_logstartpytest_runtest_logreportのような他のpytest hooksが正常に実行できる.master(loadのdist-modeの場合)は、ノードが1つのテストを実行する後、テスト実行時間の長さおよび各ノードの残りのテスト項目に基づいて、このノードにより多くのテスト項目を送信か否かを総合的に決定する.
  • 試験終了: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ファイルが使用されています.
    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)
    
    では、pytest-nを使用して実行すると、このようなエラーが報告されます.
    INTERNALERROR> raise DumpError("can't serialize {}".format(tp)) INTERNALERROR> execnet.gateway_base.DumpError: can't serialize
    正しい方法は、保存する必要があるデータをreportオブジェクトに保存することです.例えば、次のコードは、テスト実行のタイムスタンプをreportオブジェクトに保存することができます.その後、workerはreportをmasterノード:
    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関数をクラスに書く方法もあるが、このクラスがmasterノード上でのみインスタンス化されている場合、無効なhook関数が書かれていることに相当し、プログラムはエラーを報告することはないので、特に注意しなければならない.

  • 要するに、configオブジェクトはプロセス間で独立しているが、reportオブジェクト間の値は互いに同期することができるが、同期オブジェクトは避けなければならない.