2016-11-08 9 views
3

次の理由でメモリがリークする(GDIやユーザーハンドルなどのメモリや他のカーネルオブジェクトが繰り返し実行されるたびに、テスト終了まで戻ってこない)理由について説明している人はいませんか:pytestでのpyqtテストでのメモリリーク

import pytest 
from PyQt5.QtCore import QTimer 
from PyQt5.QtWidgets import QApplication, QGraphicsScene, QGraphicsView 

class TestCase: 
    @pytest.mark.parametrize('dummy', range(1000)) 
    def test_empty(self, dummy): 
     # self.view = None # does NOT fix the leak if uncommented! 
     self.app = QApplication.instance() 
     if self.app is None: 
      self.app = QApplication([]) 
     self.view = QGraphicsView() 
     self.view.setFixedSize(600, 400) 
     self.view.setScene(QGraphicsScene()) 
     self.view.show() 

     QTimer.singleShot(100, self.app.exit) 
     self.app.exec() 

     # self.view = None # FIXES the leak if uncommented! 

次のいずれかの条件がTrueになると何の漏れはありません。

  1. Iなし-IFY場合ビューテストメソッドが戻る前に(最後の行のコメントを解除)
  2. 私がデコレータを削除して、 の代わりに関数の先頭に "while True"を付けると、ビューのローカル(ビュー#1の驚くべきことではありません)が 関数のローカルになります。テスト 自体は一度実行されますが、ウィンドウは

興味深いことに、私は次の変更のいずれか作ればリークが消えない)以上にわたり再作成してます:

  1. を私はNoneにビューを設定します終了時ではなく関数の先頭にある(コメントアウトされた行は、テストメソッドをパラメータ化するのではなく、テストモジュールを生成する小さなPythonスクリプトで簡単に行える)、または多くのテストクラス、多くのテストモジュールを作成します(これは私が気づいた問題は、それぞれが多数のテストメソッドを持ついくつかのクラスを持つ100個のテストモジュールからなる巨大なテストスイートです。テストスイートのメモリーリークは、テストの数がpytestがすべてのテストを実行する前にGDIハンドルが不足しています)。 app.closeAllWindows()によって
  2. 私はapp.exitする単発の呼び出しを置き換えるには、()我々のアプリで

実際のテストは、いくつかのことを必要とし(私がこのMCVEに問題があったかもしれないと思いました)オブジェクトはsetup_method()で作成されるため、テストインスタンスのデータメンバーにPyQtオブジェクトを割り当てることは避けられません。だから私たちのための唯一の現実的な解決法は、メソッドによって作成されたPyQtオブジェクトがNone-ify PyQtオブジェクトになるように各テストメソッドを編集することですが、これはエラーが発生しやすくなります。私はもっ​​と良い方法があると思っています。

+0

ビューはシーンの所有権を取得しないため、参照を保持する必要があります。 – ekhumoro

+0

@ekhumoroはい、実際のコードはこれを行います。実際には、setScene()を使って行を削除することはできますが、まだ漏れがあります。 – Schollii

+0

も参照してくださいhttps://github.com/pytest-dev/pytest/issues/1649 – dbn

答えて

2

私たちが使用した解決策は他の人にとって有益かもしれませんので、私は答えとして投稿します(ただしpytestの3.0.4リリースで問題が解決されたかもしれません)。まず、背景のビット:

  • 我々は最終的にpytestにテストスイートを移行し、我々はまだテストドライバー
  • としてnosetestsを使用していた時に作成されたテスト(ほとんど1000)の多くを持っていますnose2pytestプラグインを使用する(https://pypi.python.org/pypi/nose2pytest
  • テストクラスのすべてのテストメソッドに対して同じオブジェクトを作成するために、テストクラスに多くのセットアップ/ティアダウンメソッドがあります。オブジェクトは自己の属性を作成することにより、試験クラスインスタンスメソッドに利用可能である:

    class TestCase: 
        def setup_method(self): 
         self.a = 123 
        def test_something(self): 
         ...use self.a... 
    

問題は、各試験法の終了時に、pytest収穫時に作成された自己の任意の属性いくつかのキャッシュに保存し、TestCaseインスタンスから削除します(少なくともpytest < 3.0.4)。この問題はもちろん、テストスイートが成長するにつれて、メモリ、GDIハンドル、USERハンドルなど特定の重要なリソースが解放されないことがあります。

最終的に、われわれのテストスイートは説明できないほど大きくなっていますが、しばらくの間走った後はいつも。最初は私たちはPyQtコードで間違っていたと思っていましたが、いくつかのテストを別のテストスイートに移動すると(別のpytestコマンドとして実行されても)クラッシュしなかったので、十分ではなかったし、我々はメンバーがリークに気づいた。これは、前述のpytestの動作(当時わかっていなかった)を考えると、驚くことではありません。私たちのスイートの1つでは、メモリは1.2ギガヘルツになり、GDIは10000に処理され、その時点でテストスイートがクラッシュします。実際、ウェブ上での検索では、デフォルトのmax GDI handles per Windows process is 10kがWindowsレジストリを参照して確認されています。

十分なバックグラウンドで、これにどのように取り組みましたか。

私たちは、次の変換の実装を完了しました。それはpytestが収穫する前に、テストメソッドによって追加された属性を自動的に削除するフィクスチャを作成しました。これは、いくつかの手順で達成された:

  1. 我々はsetup_teardown_each(self, request, cleanup_attribs)にすべてのsetup_method(self)と改名し、@pytest.fixture(autouse=True)でそれを飾りました。これは、正規表現検索置換では簡単でした。
  2. 私たちはyieldyieldに置き換えました。私たちの一貫したテストレイアウトのおかげで、def teardownがすべてのテストクラスに対してdef setup_methodの直後にあったということは、別の簡単なステップであったことを意味します。それ以外の場合は、セットアップフィクスチャに歩留まりを追加し、ティアダウンのボディコードを歩留まりに移動し、ティアダウンメソッドを削除する必要がありました。
  3. は、我々は中 cleanup_attribsフィクスチャを定義したスイートの conftest.py

    @pytest.fixture 
    def cleanup_attribs(request): 
        test_case = request.node.instance 
        attr_names = set(test_case.__dict__.keys()) 
        yield 
    
        # upon teardown: 
        attr_names_added = set(test_case.__dict__.keys()).difference(attr_names) 
        if not attr_names_added: 
         return 
    
        log.info('cleanup_attribs fixture removing {} from {}', attr_names_added, request.node.nodeid) 
        test_case = request.node.instance 
        for attr_name in attr_names_added: 
         delattr(test_case, attr_name) 
    
    この治具がsetup_teardown_each器具の依存性があるので、歩留まりの前の部分は、セットアップ前に実行されるので、これは動作します

、およびテストメソッドが実行された後に歩留まりが実行された後、セットアップが完了した後、セットアップが完全に終了した後。フィクスチャはまずテストケースの現在のdictを取得し、yield後に追加されたものを見つけて削除します。

これが実行された後、テストスイートでは、最大で数百個のGDIハンドルと数百メガバイトのメモリが使用されます。これにより、2つのテストスイートをマージすることができました。メモリとGDIの処理がなくなるためです。

関連する問題