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 только в больших прогонах.
Почему это важно
В больших наборах диагностика, почти незаметная по отдельности, может начать доминировать во времени выполнения. Привязка переключателей к правильному этапу жизненного цикла предотвращает случайные замедления. В этой ситуации разница между «всеми найденными тестами» и «тестами, которые действительно запустятся», критична, когда задействованы селекторы и снятие выбора. Правильный хук гарантирует, что решение согласовано с тем, что действительно будет исполнено.
Выводы
Если поведение должно зависеть от размера активного набора тестов, опирайтесь на финализированные элементы, а не на изначально обнаруженный пул. Пост‑коллекционный хук позволяет оставлять дорогие возможности включёнными для прицельных отладочных прогонов и автоматически приглушать их, когда размер набора выходит за ваш порог. Итог — более быстрые циклы обратной связи без потери глубины диагностики, когда она действительно нужна.