2016-11-08 195 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处理。