Всем привет, меня зовут Осипов Станислав. Я занимаюсь AppSec/DevOps с 2021 года. В этой статье я хочу рассказать как можно собрать покрытие Python приложения в runtime незавершая работу приложения.
Что было использовано в статье:
https://github.com/pallets/flask - Flask 3.03
https://github.com/nedbat/coveragepy - coverage 7.5.1
Подготовка Flask
Выполним установку Flask согласно инструкции:
https://github.com/pallets/flask/blob/main/docs/installation.rst
В директории приложения создадим app.py, взяв за основу пример из репозитория Flask, и добавим пару роутов:
import coverage, hashlib
from flask import Flask
#Регистрация rout'ов
def main():
@app.route("/")
def hello():
return "Main route"
@app.route("/1")
def hello1():
return "First route"
@app.route("/2")
def hello2():
return hashlib.sha256(b"Nobody inspects the spammish repetition").hexdigest()
#Запуск веб-сервера
app.run()
#Инициализация
app = Flask(__name__)
main()
Подготовка coverage
Установим модуль для сборки покрытия:
pip install coverage
Для отображения покрытия third-party модулей в отчёте закомментим следующие строки:
.venv/lib/python3.11/site-packages/coverage/inorout.py-16478:
if self.third_match.match(filename) and not self.source_in_third_match.match(filename):
.venv/lib/python3.11/site-packages/coverage/inorout.py:16579:
return "inside --source, but is third-party"
Инструментация кода
Согласно https://coverage.readthedocs.io/en/7.5.1/api.html сбор покрытия осуществляется следующим образом:
import coverage
cov = coverage.Coverage() #Инициализация модуля
cov.start() # Запуск сбора покрытия
# .. call your code ..
cov.stop() # Остановить сбор покрытия - в нашем случае является избыточным
cov.save() # Сохранить покрытие
cov.html_report() # Сгенерировать отчет в формате .html
Так как стоит задача обновлять покрытие при каждом запросе к приложению Flask, то необходимо внедрить функции по сборке покрытия в исходный код Flask.
Для этого в файле выполним следующие изменения:
.venv/lib/python3.11/site-packages/flask/app.py
Подключим модуль coverage указав флаги:
cover_pylib - отображать покрытие стандартых модулей Python
auto_data - продолжать запись покрытия в один файл
source - список директорий исходного кода, которые будут учитываться в отчете
Подключения модуля coverage
13a14
> import coverage
Встраивание модуля покрытия
Добавим инициализацию модуля coverage в аттрибуте класса Flask и метод запуска сбора покрытия:
253c253,254 #Добавляем инициализацию сбора покрытия в объект класса Flask
и запуск сбора покрытия в метод "run" класса Flask
<
---
> self.cov = coverage.Coverage(cover_pylib=True, auto_data=True, source=["./", ".venv"])
> self.cov.start()
Поиск метода для сохранения покрытия
Найдем метод, который будет вызываться при обработке запроса.
В случае Flask я выбрал метод make_response, который отвечает за отправку ответа на запорс.
В методе этом будем сохранять/обновлять покрытие
1230c1229,1230
<
---
> self.cov.save()
> self.cov.html_report()
Проверяем сбор покрытия в Runtime
Запустим приложение: python3 app.py
В браузере перейдем по адресу, содержащий main route - 127.0.0.1:5000
Откроем страницу, содержащую покрытие - firefox htmlcov/index.html
Покрытие: Составило 11% вместе исходным кодом сторонних зависимостей.
В браузере перейдем по адресу, содержащий второй route - 127.0.0.1:5000/2
Обновим страницу, содержащую покрытие
Покрытие: Увеличилось на 5 процентов после обращения к 2 rout'у.
Итоги
Благодаря встраиванию модуля сбора покрытия в исходный код Flask, удалось добиться обновления покрытия при каждом запросе к приложеню.
Благодарю за внимание!
Предложения, вопросы, замечания, конструктивная критика приветствуется в комментариях.
danilovmy
Все бы хорошо, и даже возьму на заметку...но.
Покрытие обычно относится к тестированию кода проекта. Инструменты покрытия кода, в данном случае coverage, помогают определить, насколько хорошо код тестирован путем анализа, какие части кода проекта выполняются во время Тестирования.
И если говорить о подсчете coverage в runtime получается, что проект это и есть "тест" для подсчета покрытия этим тестом проекта.
Я знаю, многие тестируют в runtime на клиентах, но, все же, best practices - это создать тест, который симулирует поведение клиента.
Вложенные библиотеки обычно не тестируют, отдавая тестирование на откуп создателям библиотек. А coverage считается относительно объема кода проекта.
Но если очень хочется посчитать покрытие импортируемых библиотек при запуске через командную строку используется ключ --source и после, например, имя импортируемого модуля --source flask.
Ну и конечно, если говорить про автоматизацию, то должен быть написан тест, который "запустит" app.py, сделает вызов по main route 127.0.0.1:5000 проверит что ответ верен (200 статус там и т.п.) и отключится. Желательно, чтобы тест еще сделал несколько других действий типа дернул фальшивый url и проверил что 404 или отправил post туда где только get и проверил статус ошибки.
А после автор тестов насладится прекрасной статистикой покрытия проекта открыв страницу, содержащую покрытие автоматическими тестами. ;)