gpt4 book ai didi

memory-leaks - 使用pytest进行pyqt测试中的内存泄漏

转载 作者:行者123 更新时间:2023-12-04 04:42:45 27 4
gpt4 key购买 nike

有没有人能解释为什么以下内存泄漏(内存和其他内核对象,如 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!

如果以下任一条件为真,则没有泄漏:

  1. 如果我在 测试方法返回之前 None-ify View (取消最后一行的注释)
  2. 如果我将 View 设为本地 View 函数而不是 self 的成员(鉴于修复 #1 不足为奇)
  3. 如果我移除装饰器并且而不是在函数的顶部有一个“while True”(所以测试本身运行一次,但窗口会一遍又一遍地重新创建)

有趣的是,如果我进行以下任何修改,泄漏不会消失:

  1. 我在函数的开头而不是结尾将 View 设置为 None(在测试方法开头注释掉一行)
  2. 我没有对测试方法进行参数化,而是创建了许多测试方法(100 多个,使用生成测试模块的小 python 脚本很容易完成),或许多测试类,许多测试模块(这就是我注意到问题的方式,我们有一个巨大的测试套件,由 100 个测试模块组成,每个测试模块都有几个类,每个模块都有许多测试方法——直到最近,测试套件中的内存泄漏才被注意到,当时测试的数量变得足够大,以至于操作系统现在用完了 GDI在 pytest 完成运行所有测试之前处理!)。
  3. 我用 app.closeAllWindows() 替换了对 app.exit() 的单次调用(我认为这可能是此 MCVE 中的问题)

我们应用程序中的实际测试需要在 setup_method() 中创建一些对象,因此我们无法避免将 PyQt 对象分配给测试实例的数据成员。所以现在对我们来说唯一可行的解​​决方案是将每个测试方法编辑为 None-ify 方法创建的 PyQt 对象,但这很容易出错,更不用说费力了(尽管比目前的情况要好)。我希望有更好的方法。

最佳答案

我们使用的解决方案可能会使其他人受益,因此我将其作为答案发布(尽管我刚刚在 pytest 的 3.0.4 版本中看到问题可能已得到解决)。首先介绍一下背景:

  • 我们有很多测试(将近 1000 个)是在我们仍然使用 nosetests 作为测试驱动程序时创建的
  • 我们最终使用 nose2pytest 插件 (https://pypi.python.org/pypi/nose2pytest) 将测试套件迁移到 pytest
  • 我们在测试类上有很多设置/拆卸方法,可以为测试类的所有测试方法创建相同的对象。通过在 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 gig,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 之后,意味着这是又一个简单的步骤。否则我们将不得不在 setup fixture 中添加一个 yield,将 teardown 的主体代码移动到 yield 之后,并删除 teardown 方法。
  3. 我们在套件的 conftest.py 中定义了 cleanup_attribs fixture :

    @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 之后的部分在测试方法运行之后运行,如果设置也产生了,则在设置完全完成之后运行。 fixture 首先获取测试用例的当前字典,然后在 yield 之后找到添加的内容并将其删除。

此设置到位后,测试套件最多使用几百个 GDI 句柄和几百兆内存,这是一个巨大的差异。这使我们能够合并两个测试套件,因为它们不再耗尽内存和 GDI 句柄。

关于memory-leaks - 使用pytest进行pyqt测试中的内存泄漏,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40478600/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com