Один инстанс для всех процессов при параллельном запуске тестов (py.test xdist)

При паралельном запуске тестов pytest c с плагином xdist создаются отдельные процессы для колекции тестов
Можно как то передать один инстанс для каждого процесса
Пример


# conftest.py

import pytest

class App:
    def __init__(self):
        print 'init'

@pytest.fixture(scope="session")
def app():
    return App()

# some_test.py

import pytest

def test_exists(app):
    assert False, app

def test_exists2(app):
    assert False, app

def test_exists3(app):
    assert False, app

при запуске в два потока py.test -n2 some_test.py
для тестов будут созданы разные инстанссы aap

Увы стандартными доступными методами такое сделать нельзя. В таком случае у вас будет отдельный экземпляр класса для каждого запущенного процесса. Уже была такая тема:

И есть даже дефект создан по этому поводу:

https://bitbucket.org/hpk42/pytest/issue/175/way-to-control-how-pytest-xdist-runs-tests

Тут надо делать какой-то workaround. Я лично не экспериментировал, но все таки считаю что этого можно добиться через хуки, которые вызываются до xdist плагина. Как-то надо будет время выделить для этого задачи ( давно уже висит в todo )

В общем, выделил немного времени на то чтобы посмотреть воркараунд, заодно и добавлю практический пример для урока по юнит тестированию Поиск 🔍 организации или лица - Предоставление сведений из ЕГРЮЛ/ЕГРИП в электронном виде.

Заключение, просто это сделать не получиться (может быть и можно как-то, но надо больше времени на эксперименты)

Это больше быстрый костыль нежели решение, но все же лучше иметь что-то чем ничего.

Есть одно ограничение, из-за того что xdist плагин использует execnet модуль для сериализации, в нем реализовано сериализация только примитивных типов и к сожалению экземпляры объектов не могут быть сериализированны. Т.е. нельзя напрямую присвоить экземпляр класса для нового созданного node node.slaveinput["app"] = node.config.app потому что при сериализации Вы получите ошибку

raise DumpError("can't serialize %s" % (tp,))
DumpError: can't serialize <class 'conftest.App'>

Для этого я быстро воспользовался pickle модулем чтобы превратить объект в строку node.slaveinput["app"] = pickle.dumps(node.config.app). Т.е данный костыль сработает только если Вы будете инициализировать shared object единожды и вам не нужно потом менять этот объект.

conftest.py

import pytest
from random import randint
import logging
import pickle
 
logging.basicConfig(filename="log.log", level=logging.INFO)
 
class App(object):
    def __init__(self):
        self.a = randint(0, 100)
        logging.info("initialized {}".format(self))
 
    def method(self):
        logging.info("method in app that print {}".format(self.a))
 
def pytest_configure(config):
    if not hasattr(config, "slaveinput"):
        config.app = App()
 
def pytest_configure_node(node):
    node.slaveinput["app"] = pickle.dumps(node.config.app)
 
@pytest.fixture(scope="session")
def app(request):
    return pickle.loads(request.config.slaveinput["app"])

Сам файл с тестами никак не меняется и выглядит точно также.

test_something.py

import pytest
 
def test_exists(app):
    app.method()
 
def test_exists2(app):
    app.method()
 
def test_exists3(app):
    app.method()

И как видно из логов, инициализация происходит один раз и экземпляр класса для каждого потока выдает правильные одинаковые данные (но фактически из-за того что я использовал pickle экземпляры будут разные, хотя поведение у них и будет одинаковым, описал этот момент выше).

log.log

INFO:root:initialized <conftest.App object at 0x02B07470>
INFO:root:method in app that print 46
INFO:root:method in app that print 46
INFO:root:method in app that print 46

Все файлы и логи можно найти здесь:
https://gist.github.com/polusok/4e71f7e3d3dbf437cc25

1 лайк

О прикольно.

Я возможно упускаю, но conftest.py (все которые есть), запускаются до распараллеливания. Т.е. если несколько таких пошаренных тяжелых операций, то они будут выполнены в одном потоке, что по факту увеличит время.

В моем случае тестовый класс, в setupClass, подымается виртуалочки и подгатавливаются, причем каждый тест - свои требования, свои образы. И больше подходит сгруппировать тесты на одном слейве.

Советуют копать сюда: https://bitbucket.org/hpk42/pytest-xdist/src/5ba5bdb77d302b621508bde6fdca37349f7d17d7/xdist/dsession.py?at=default#cl-153
и определить новый метод “распределения”.

Я пока просто ограничился, что nose делает, то что нужно :smile_cat:

Ну да, conftest это как обычный плагин для pytest.

Ведь вся суть поставленной задачи была - инициализация fixture один раз, а не для каждого слейва. Да увеличит время, ну так у тебя уже становиться другая задача, делать распараллеливание подготовки fixture. Для каждого слейва можно определять отдельную логику через pytest_configure_node(node)

Ну мне тоже как бы не надо, просто помогаю решить проблемы :smile:

А вообще, если nose делает, то что нужно, то зачем париться.

тогда что делать если понадобится поменять объект? есть возможность установить связь ноды с мастером?

Увы не могу ответить на этот вопрос сейчас, нужно еще думать и экспериментировать.

Возможно я смотрю на проблему не стой стороны.
Дело в том что при запуске тестов паралельно, на каждый процесс создается окружение приложения (для работы с базой создавать тестовые данные)
Но само приложение забирает много памяти и серверу становится плохо.
Если иницыализировать приложение один раз в мастере и дергает его когда нужно то думаю это бы решило проблему с жором памяти.

Есть хорошее решение, предложенное здесь:

http://developer.paylogic.com/articles/pytest-xdist-and-session-scoped-fixtures.html