有没有人解释为什么以下泄漏内存(内存和其他内核对象,如GDI和用户句柄在每次迭代时都会不断增加,并且在测试退出之前永远不会回落):
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!
如果满足以下任何条件,则无泄漏:
有趣的是,如果我做出以下任何修改,泄漏不会消失:
我们app中的实际测试要求在setup_method()中创建一些对象,因此我们无法避免将PyQt对象分配给测试实例的数据成员。因此,对我们来说唯一实用的解决方案是将每个测试方法编辑为由方法创建的None-ify PyQt对象,但这很容易出错,更不用说费力了(尽管比当前情况更好)。我希望有更好的方法。
答案 0 :(得分:2)
我们使用的解决方案可能会让其他人受益,因此我将其作为答案发布(尽管我在pytest的3.0.4版本中看到问题可能已得到解决)。首先是一些背景知识:
我们在测试类上有很多setup / teardown方法,可以为测试类的所有测试方法创建相同的对象。通过在self上创建属性,对象可用于测试类实例方法:
class TestCase:
def setup_method(self):
self.a = 123
def test_something(self):
...use self.a...
问题在于,在每个测试方法结束时,pytest会收集在测试方法期间创建的self的任何属性,将其存储在某个缓存中,并将其从TestCase实例中删除(至少对于pytest< 3.0)。 4)。当然,问题在于随着测试套件的增长,某些关键资源不会被释放:内存,GDI句柄,USER句柄等。
最终,我们的测试套件变得足够大,以至于无法解释,但总是在运行一段时间后崩溃。起初我们认为这是我们在PyQt代码中做错的事情,但发现将一些测试移动到一个单独的测试套件(作为一个单独的pytest命令运行)并没有导致任何崩溃,所以我们住了一段时间,直到那个还不够,我们发现会员泄漏了。考虑到上面描述的pytest行为(我们当时不知道),这并不奇怪。在我们的一个套件中,内存将达到1.2演出,GDI处理到10000,此时测试套件将崩溃。实际上,在web上搜索表明默认max GDI handles per Windows process is 10k,通过查看Windows注册表得到确认。
足够的背景,现在我们如何解决这个问题。
所以我们刚刚完成了以下转换,它产生了很大的不同:我们创建了一个fixture,可以在pytest有机会收集它们之前自动删除测试方法添加的任何属性。这是通过几个步骤实现的:
setup_method(self)
重命名为setup_teardown_each(self, request, cleanup_attribs)
并使用@pytest.fixture(autouse=True)
进行装饰。使用正则表达式搜索替换很容易做到这一点。 def teardown_method(self)
行替换为yield
,这要归功于我们一致的测试布局,对于每个测试类def teardown
就在def setup_method
之后,意味着这是另一个简单的步骤。否则我们将不得不在设置夹具中添加一个yield,将teardown的body代码移动到yield之后,并删除拆卸方法。 我们在套件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)
这是有效的,因为此fixture是setup_teardown_each fixture的依赖项,因此yield之前的部分在设置之前运行,yield之后的部分在测试方法运行之后运行,如果设置也生成,则在安装完成之后。夹具首先获得测试用例的当前dict,并且在yield之后它找到添加的内容并将其删除。
在此之后,测试套件最多使用了几百个GDI手柄和几百兆内存,这是一个巨大的差异。这允许我们合并两个测试套件,因为它们不再耗尽内存和GDI句柄。