Всем привет, меня зовут Осипов Станислав. Я занимаюсь AppSec/DevOps с 2021 года. В этой статье я хочу рассказать как можно собрать покрытие Python приложения в runtime незавершая работу приложения.

Соображения Midjourney по поводу статьи
Соображения Midjourney по поводу статьи

Что было использовано в статье:

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

  1. Запустим приложение: python3 app.py

  2. В браузере перейдем по адресу, содержащий main route - 127.0.0.1:5000

  3. Откроем страницу, содержащую покрытие - firefox htmlcov/index.html

Результат сбора покрытия
Результат сбора покрытия

Покрытие: Составило 11% вместе исходным кодом сторонних зависимостей.

  1. В браузере перейдем по адресу, содержащий второй route - 127.0.0.1:5000/2

  2. Обновим страницу, содержащую покрытие

Результат сбора покрытия
Результат сбора покрытия

Покрытие: Увеличилось на 5 процентов после обращения к 2 rout'у.

Итоги

Благодаря встраиванию модуля сбора покрытия в исходный код Flask, удалось добиться обновления покрытия при каждом запросе к приложеню.

Благодарю за внимание!
Предложения, вопросы, замечания, конструктивная критика приветствуется в комментариях.

Комментарии (1)


  1. danilovmy
    07.05.2024 19:36
    +1

    Все бы хорошо, и даже возьму на заметку...но.

    Покрытие обычно относится к тестированию кода проекта. Инструменты покрытия кода, в данном случае coverage, помогают определить, насколько хорошо код тестирован путем анализа, какие части кода проекта выполняются во время Тестирования.

    И если говорить о подсчете coverage в runtime получается, что проект это и есть "тест" для подсчета покрытия этим тестом проекта.

    Я знаю, многие тестируют в runtime на клиентах, но, все же, best practices - это создать тест, который симулирует поведение клиента.

    Вложенные библиотеки обычно не тестируют, отдавая тестирование на откуп создателям библиотек. А coverage считается относительно объема кода проекта.

    Но если очень хочется посчитать покрытие импортируемых библиотек при запуске через командную строку используется ключ --source и после, например, имя импортируемого модуля --source flask.

    Ну и конечно, если говорить про автоматизацию, то должен быть написан тест, который "запустит" app.py, сделает вызов по main route 127.0.0.1:5000 проверит что ответ верен (200 статус там и т.п.) и отключится. Желательно, чтобы тест еще сделал несколько других действий типа дернул фальшивый url и проверил что 404 или отправил post туда где только get и проверил статус ошибки.

    А после автор тестов насладится прекрасной статистикой покрытия проекта открыв страницу, содержащую покрытие автоматическими тестами. ;)