2016-11-08 120 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. 如果我無 - IFY視圖之前測試方法返回(取消註釋的最後一行)
  2. 如果我使視圖本地到 函數而不是自己的成員(並不奇怪給定修復#1)
  3. 如果我刪除裝飾器而 而不是在函數頂部有一個「while True」(so測試 本身運行一次,但窗戶被重新一遍又一遍)

有趣的是,泄漏不會消失,如果我做任何如下修改:

  1. 我視圖設置爲無在函數的開始處而不是在結尾處(註釋掉)軋製的測試方法)
  2. 不用參數化測試方法,我創建了許多測試方法(100,很容易用一個生成測試模塊的小python腳本完成),或許多測試類,許多測試模塊(這就是我注意到的問題是,我們有一個巨大的測試套件,每個測試套件包含100個測試模塊,每個測試模塊包含多個類,每個類都有很多測試方法 - 測試套件中的內存泄漏直到最近測試的數量變得足夠大以至於操作系統現在pytest在pytest完成運行所有測試之前用完了)。
  3. 我更換單次呼叫app.exit()由app.closeAllWindows()(我想這可能是這個問題在這個MCVE)

在我們的應用程序的實際測試需要一些在setup_method()中創建對象,因此我們無法避免將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版本中看到問題可能已得到修復)。首先一點背景知識:

  • 我們有很多的測試(幾乎是1000),當我們還在使用nosetests作爲測試車手
  • 我們最終遷移測試套件,pytest同時創建的使用nose2pytest插件(https://pypi.python.org/pypi/nose2pytest
  • 我們對測試類有很多setup/teardown方法來爲測試類的所有測試方法創建相同的對象。的對象是可用的測試類的實例方法通過在自創建屬性:

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

的問題是,在每個測試方法的末尾,pytest收穫這是在創建自的任何屬性測試方法,將其存儲在高速緩存中的一些,以及從測試用例實例(至少對於pytest < 3.0.4)中移除。這個問題當然是,隨着測試套件的增長,某些關鍵資源不會被釋放:內存,GDI句柄,USER句柄等。

最終,我們的測試套件變得足夠大,以至於無法解釋但崩潰總是跑完一段時間後。起初我們以爲是我們在PyQt的代碼是做錯了什麼,但發現移動一些測試,單獨測試套件(作爲一個單獨的pytest命令來運行)沒有引起任何崩潰,所以我們有住了一段時間,直到甚至是不夠的,我們注意到成員泄漏。考慮到上面描述的pytest行爲(當時我們不知道),這並不奇怪。在我們的其中一個套件中,內存將達到1.2個演出,而GDI將處理到10000個,此時測試套件將崩潰。事實上,在網上搜索表明,默認max GDI handles per Windows process is 10k,通過查看Windows註冊表確認。

足夠的背景知識,我們現在該怎麼解決這一點。

所以我們剛剛完成實現以下轉變,它使一個巨大的差別:我們創建了一個固定pytest都有機會收穫之前自動刪除由測試方法添加的任何屬性。這是在幾個步驟來實現:

  1. 我們改名每setup_method(self)setup_teardown_each(self, request, cleanup_attribs)@pytest.fixture(autouse=True)裝飾它。用正則表達式搜索替換很容易。
  2. 我們用yield取代了def teardown_method(self)行,這要歸功於我們一貫的測試佈局,其中def teardown就在def setup_method之後,這意味着這是另一個簡單的步驟。否則,我們不得不在安裝夾具中添加一個良率,將拆卸的主體代碼移到yield之後,並刪除拆卸方法。
  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夾具的依賴,所以產量前部安裝之前運行,並且在測試方法運行後,如果設置也完成,則在完成設置完成後運行成品。夾具首先獲得測試用例的當前字典,並在收益後找到添加的內容並將其刪除。

此之後,到位,測試套件使用最多幾百GDI處理和幾百兆MEM,一個巨大的差異。這允許我們合併兩個測試套件,因爲它們不再耗盡內存和GDI處理。