2025, Nov 24 12:01

Как считать выбранные тесты в pytest и отключать логирование DataFrame

Показываем, как корректно считать выбранные тесты в pytest и через pytest_collection_finish отключать тяжёлое логирование DataFrame при больших прогонах.

Крупные наборы тестов со временем обрастают полезной, но тяжёлой диагностикой. Типичный пример — подробное логирование DataFrame: оно выручает, когда охотишься за нестабильным кейсом или разбираешься с одной‑двумя проверками, но превращается в узкое место, когда запускаются сотни тестов. Если такой диагностический поток съедает 30% реального времени, нужен простой переключатель, который будет автоматически отключать его, когда прогон включает, скажем, больше 100 тестов.

Постановка задачи

Идея проста: узнать, сколько тестов предстоит запустить, и если число превышает порог, выставить переменную окружения и превратить функцию логирования в no‑op. Первая попытка встраивает это в фазу коллекции pytest через хук, а функция логирования проверяет флаг.

def pytest_collection_modifyitems(config, collected):
    total_tests = len(collected)
    if total_tests > 100:
        print(f'Running {total_tests} tests (more than 100 tests), disabling dataframe logging.')
        os.environ['RUNNING_MORE_THAN_100_TESTS'] = '1'
    else:
        print(f'Running {total_tests} tests.')
def dump_dataframe(tbl: DataFrame, sink):
    if os.environ.get('RUNNING_MORE_THAN_100_TESTS'):
        sink.info('RUNNING_MORE_THAN_100_TESTS is set. dump_dataframe() skipped.')
        return
    else:
        # логируем df как обычно

Однако запуск pytest с выражением для отбора показывает расхождение между тем, что сообщает хук, и тем, что действительно будет выполнено. Вывод показывает: обнаруживаются сотни элементов, но затем большинство из них снимаются с выбора, и активным остаётся куда меньший набор. Дорогая часть логирования всё равно отключается так, будто запускались бы все.

Что на самом деле происходит

Приведённый выше хук выполняется до того, как применяются фильтры и снимается выбор. В этот момент pytest уже нашёл все потенциальные тесты, но ещё не сузил их по селекторам вроде -k. В итоге решение опирается на число до фильтрации, которое может быть значительно больше фактического количества выбранных элементов.

Подходящий хук для этой задачи

Исправление — перенести решение в момент, когда коллекция завершена и выбор применён. pytest предоставляет хук, который запускается после окончания коллекции, когда список элементов финализирован для выполнения. Использование этого хука гарантирует, что счётчик отражает именно те тесты, которые реально пойдут в запуск.

def pytest_collection_finish(session):
    selected_total = len(session.items)
    if selected_total > 100:
        print(f'Running {selected_total} tests (more than 100 tests), disabling dataframe logging.')
        os.environ['RUNNING_MORE_THAN_100_TESTS'] = '1'
    else:
        print(f'Running {selected_total} tests.')

С этим изменением переменная окружения выставляется лишь тогда, когда финализированный набор превышает порог. Проверка в логгере DataFrame остаётся прежней и корректно превращается в no‑op только в больших прогонах.

Почему это важно

В больших наборах диагностика, почти незаметная по отдельности, может начать доминировать во времени выполнения. Привязка переключателей к правильному этапу жизненного цикла предотвращает случайные замедления. В этой ситуации разница между «всеми найденными тестами» и «тестами, которые действительно запустятся», критична, когда задействованы селекторы и снятие выбора. Правильный хук гарантирует, что решение согласовано с тем, что действительно будет исполнено.

Выводы

Если поведение должно зависеть от размера активного набора тестов, опирайтесь на финализированные элементы, а не на изначально обнаруженный пул. Пост‑коллекционный хук позволяет оставлять дорогие возможности включёнными для прицельных отладочных прогонов и автоматически приглушать их, когда размер набора выходит за ваш порог. Итог — более быстрые циклы обратной связи без потери глубины диагностики, когда она действительно нужна.