2016-11-08 5 views
3

У кого-нибудь есть объяснение, почему следующая память утечек (память и другие объекты ядра, такие как GDI и пользовательские ручки, продолжают увеличиваться на каждой итерации и никогда не возвращаются вниз до тестовых выходов):Мемная утечка в тесте pyqt с pytest

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. Если я None-римента вид перед метод испытания возвращает (раскомментировать последнюю строку)
  2. Если я сделаю вид локальным для функции вместо члена self (не удивительно, данное исправление № 1)
  3. Если я удаляю декоратор и , вместо этого вместо «while True» в верхней части функции (так тест сам запускается один раз, но окно пересоздался снова и снова)

Интересно, что утечка не уходит, если я делать какие-либо из следующих модификаций:

  1. Я поставил точку зрения на None в начале функции, а не в конце (прокомментированная строка в ginning метода испытания)
  2. Вместо того, чтобы параметризовать метод тестирования, я создаю множество методов тестирования (100, легко выполняемых с помощью небольшого скрипта python, который генерирует тестовый модуль) или многих тестовых классов, многих тестовых модулей (так я заметил проблема состоит в том, что у нас есть огромный набор тестов, состоящий из 100 тестовых модулей с несколькими классами в каждом, каждый из которых имеет множество методов тестирования - утечка памяти в наборе тестов осталась незамеченной до недавнего времени, когда количество тестов стало достаточно большим, теперь заканчивается обработка дескрипторов GDI до завершения тестирования pytest!).
  3. я заменить вызов на одного выстрела в app.exit() по app.closeAllWindows() (я думал, что это, возможно, был вопрос в этом MCVE)

Фактические испытания в нашем приложении требуют, чтобы некоторые объекты создаются в setup_method(), поэтому мы не можем избежать назначения объектов PyQt членам данных тестового экземпляра. Итак, единственное практическое решение для нас - это отредактировать каждый метод тестирования объектам PyQt None-ify, созданным этими методами, но это будет подвержено ошибкам, не говоря уже о трудоемком (хотя и лучше, чем в текущей ситуации). Я надеюсь, что есть лучший способ.

+0

Вид не имеет права собственности на сцену, поэтому вы должны держать ссылку на него. – ekhumoro

+0

@ekhumoro Да, настоящий код делает это. Фактически, вы можете удалить строку с помощью setScene(), и у вас все еще будет утечка. – Schollii

+0

См. Также https://github.com/pytest-dev/pytest/issues/1649 – dbn

ответ

2

Решение, которое мы использовали, могло бы принести пользу другим, поэтому я отправляю его в качестве ответа (хотя я только что видел в выпуске 3.0.3 pytest, что проблема могла быть исправлена). Сначала немного фона:

  • у нас есть много тестов (почти 1000), которые были созданы в то время, когда мы все еще использовали nosetests как водитель-испытатель
  • мы в конце концов мигрировали тестовый набор для pytest используя плагин носа2pytest (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,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). Это было легко сделать с регулярным выражением search-replace.
  2. Мы заменили линии def teardown_method(self) на yield, которые благодаря нашей согласованной схеме тестирования, где для каждого испытательного класса 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 арматуры, поэтому часть перед тем выходом запускается перед установкой, и после запуска теста после того, как тест запускается, и, если настройка также дана, после завершения установки полностью завершается. Вначале прибор получает текущую версию тестового примера, и после урока он находит то, что было добавлено и удаляет.

После того, как это было введено, тестовый набор использует не более пары сотен ручек GDI и пару сотен мегаграмм, огромную разницу. Это позволило нам объединить два набора тестов, поскольку они больше не исчерпываются памятью и дескрипторами GDI.