Улучшение отчета для rack_coverage
This commit is contained in:
@@ -35,3 +35,40 @@ class CoreCommand(BaseCommand):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
return settings.DEBUG or 'test' in sys.argv
|
return settings.DEBUG or 'test' in sys.argv
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _report_separator(cls, length: int = 70, double: bool = True, first_new_line: bool = False, last_new_line: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Выводит разделитель отчёта
|
||||||
|
|
||||||
|
:param length Длина разделителя
|
||||||
|
:param double Выводить ли двойной разделитель
|
||||||
|
:param first_new_line Выводить ли первую пустую строку
|
||||||
|
:param last_new_line Выводить ли последнюю пустую строку
|
||||||
|
"""
|
||||||
|
|
||||||
|
if first_new_line:
|
||||||
|
print()
|
||||||
|
|
||||||
|
if double:
|
||||||
|
print('=' * length)
|
||||||
|
else:
|
||||||
|
print('-' * length)
|
||||||
|
|
||||||
|
if last_new_line:
|
||||||
|
print()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _report_int_value(cls, name: str, value: int, length: int = 70) -> None:
|
||||||
|
"""
|
||||||
|
Выводит значение отчёта
|
||||||
|
|
||||||
|
:param name Название
|
||||||
|
:param value Значение
|
||||||
|
:param length Общая длина
|
||||||
|
"""
|
||||||
|
|
||||||
|
p_len = len(str(value)) + len(name) + 2
|
||||||
|
points = "." * (length - p_len - 1)
|
||||||
|
|
||||||
|
print(f"{name}: {points} {value}")
|
||||||
|
|||||||
@@ -12,19 +12,20 @@ import ast
|
|||||||
import configparser
|
import configparser
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import importlib
|
import importlib
|
||||||
|
import sys
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management import CommandError
|
from django.core.management import CommandError
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CoreCommand
|
||||||
from .coverage_structure_visitor import CoverageStructureVisitor
|
from .coverage_structure_visitor import CoverageStructureVisitor
|
||||||
from .coverage_test_file_visitor import CoverageTestFileVisitor
|
from .coverage_test_file_visitor import CoverageTestFileVisitor
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(CoreCommand):
|
||||||
"""
|
"""
|
||||||
Проверяет соблюдение принципа один метод - один файл тестов
|
Проверяет соблюдение принципа один метод - один файл тестов
|
||||||
"""
|
"""
|
||||||
@@ -39,6 +40,18 @@ class Command(BaseCommand):
|
|||||||
help = "Discover test coverage in the specified modules or the current directory."
|
help = "Discover test coverage in the specified modules or the current directory."
|
||||||
""" Подсказка """
|
""" Подсказка """
|
||||||
|
|
||||||
|
success_count: int = 0
|
||||||
|
""" Счетчик успешно проверенных методов """
|
||||||
|
|
||||||
|
missing_files_count: int = 0
|
||||||
|
""" Счетчик не найденных файлов с тестами """
|
||||||
|
|
||||||
|
missing_classes_count: int = 0
|
||||||
|
""" Счетчик не найденных классов с тестами """
|
||||||
|
|
||||||
|
skipped_packages_count: int = 0
|
||||||
|
""" Счетчик пропущенных пакетов """
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
"""
|
"""
|
||||||
Позиционный аргумент: путь к пакету от корня
|
Позиционный аргумент: путь к пакету от корня
|
||||||
@@ -67,23 +80,19 @@ class Command(BaseCommand):
|
|||||||
raise CommandError("No package specified for verification.")
|
raise CommandError("No package specified for verification.")
|
||||||
|
|
||||||
# Метрики и счётчики
|
# Метрики и счётчики
|
||||||
success_count: int = 0
|
|
||||||
missing_files_count: int = 0
|
|
||||||
missing_classes_count: int = 0
|
|
||||||
skipped_packages_count: int = 0
|
|
||||||
|
|
||||||
for package_name in packages_to_check:
|
for package_name in packages_to_check:
|
||||||
package_dir: Path | None = self._find_package_dir(package_name)
|
package_dir: Path | None = self._find_package_dir(package_name)
|
||||||
if not package_dir:
|
if not package_dir:
|
||||||
self.stdout.write(self.style.WARNING(f"⚠️ Пакет '{package_name}' пропущен: не найдена его директория"))
|
self.stdout.write(self.style.WARNING(f"⚠️ Пакет '{package_name}' пропущен: не найдена его директория"))
|
||||||
skipped_packages_count += 1
|
self.skipped_packages_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Смарт-автообнаружение зеркального пакета тестов вверх по иерархии
|
# Смарт-автообнаружение зеркального пакета тестов вверх по иерархии
|
||||||
tests_info: tuple[str, Path] | None = self._autodiscover_tests_package(package_name)
|
tests_info: tuple[str, Path] | None = self._autodiscover_tests_package(package_name)
|
||||||
if not tests_info:
|
if not tests_info:
|
||||||
self.stdout.write(self.style.ERROR(f"❌ Не удалось программно найти пакет тестов для: {package_name}"))
|
self.stdout.write(self.style.ERROR(f"❌ Не удалось программно найти пакет тестов для: {package_name}"))
|
||||||
skipped_packages_count += 1
|
self.skipped_packages_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tests_root_name, tests_root_dir = tests_info
|
tests_root_name, tests_root_dir = tests_info
|
||||||
@@ -152,7 +161,7 @@ class Command(BaseCommand):
|
|||||||
f"❌ Отсутствует файл тестов [{target_file_name}] для метода: {full_path}"
|
f"❌ Отсутствует файл тестов [{target_file_name}] для метода: {full_path}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
missing_files_count += 1
|
self.missing_files_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 4. СТРОИМ ОЖИДАЕМОЕ ИМЯ КЛАССА ТЕСТОВ
|
# 4. СТРОИМ ОЖИДАЕМОЕ ИМЯ КЛАССА ТЕСТОВ
|
||||||
@@ -181,26 +190,36 @@ class Command(BaseCommand):
|
|||||||
f"отсутствует класс [{expected_class_name}] для метода: {full_path}"
|
f"отсутствует класс [{expected_class_name}] для метода: {full_path}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
missing_classes_count += 1
|
self.missing_classes_count += 1
|
||||||
else:
|
else:
|
||||||
success_count += 1
|
self.success_count += 1
|
||||||
|
|
||||||
# Вывод финального промышленного отчёта в терминал
|
# Вывод финального промышленного отчёта в терминал
|
||||||
total_errors: int = missing_files_count + missing_classes_count
|
self._final_report()
|
||||||
|
|
||||||
self.stdout.write("\n" + "=" * 60)
|
def _final_report(self):
|
||||||
self.stdout.write(self.style.SUCCESS(f"📊 ИТОГОВЫЙ ОТЧЁТ ПОКРЫТИЯ МЕТОДОВ:"))
|
"""
|
||||||
self.stdout.write(f" ✅ Успешно проверено и покрыто методов: {success_count}")
|
Выводит финальный отчёт в терминал
|
||||||
if skipped_packages_count > 0:
|
"""
|
||||||
self.stdout.write(self.style.WARNING(f" ⚠️ Пропущено / не найдено пакетов: {skipped_packages_count}"))
|
|
||||||
if missing_files_count > 0:
|
total_errors: int = self.missing_files_count + self.missing_classes_count
|
||||||
self.stdout.write(self.style.ERROR(f" ❌ Отсутствует файлов тестов: {missing_files_count}"))
|
|
||||||
if missing_classes_count > 0:
|
self._report_separator(first_new_line=True)
|
||||||
self.stdout.write(self.style.ERROR(f" ❌ Отсутствует классов внутри файлов тестов: {missing_classes_count}"))
|
self.stdout.write(self.style.SUCCESS(f"Test coverage report:"))
|
||||||
self.stdout.write("=" * 60 + "\n")
|
self._report_separator(double=False)
|
||||||
|
self._report_int_value("Success", self.success_count)
|
||||||
|
if self.skipped_packages_count > 0:
|
||||||
|
self._report_int_value("Skipped packages", self.skipped_packages_count)
|
||||||
|
if self.missing_files_count > 0:
|
||||||
|
self._report_int_value("Missing test files", self.missing_files_count)
|
||||||
|
if self.missing_classes_count > 0:
|
||||||
|
self._report_int_value("Missing test classes", self.missing_classes_count)
|
||||||
|
self._report_separator(double=False, last_new_line=False)
|
||||||
|
|
||||||
if total_errors > 0:
|
if total_errors > 0:
|
||||||
raise CommandError(f"🛑 Проверка провалена. Суммарно пропущено проверок: {total_errors}")
|
self._report_int_value("Test coverage failed, total number of errors", total_errors)
|
||||||
|
self._report_separator(double=False, last_new_line=False)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _extract_classes_and_methods(cls, file_path: Path, module_dot_path: str) -> list[tuple[str, str | None, str]]:
|
def _extract_classes_and_methods(cls, file_path: Path, module_dot_path: str) -> list[tuple[str, str | None, str]]:
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
# DRF Rack
|
|
||||||
# Библиотека классов расширений для Django REST Framework
|
|
||||||
#
|
|
||||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
|
||||||
# Все права защищены. Лицензия: MIT
|
|
||||||
|
|
||||||
"""
|
|
||||||
Описание класса для команды rack_coverage
|
|
||||||
"""
|
|
||||||
|
|
||||||
import ast
|
|
||||||
import configparser
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.management.base import no_translations
|
|
||||||
|
|
||||||
from net.xeaf.rack.core import CoreCommand
|
|
||||||
|
|
||||||
|
|
||||||
class Command(CoreCommand):
|
|
||||||
"""
|
|
||||||
Осуществляет проверку покрытия тестами
|
|
||||||
"""
|
|
||||||
|
|
||||||
# noinspection SpellCheckingInspection
|
|
||||||
CONFIG_FILE = '.rackcoveragerc'
|
|
||||||
""" Имя файла конфигурации """
|
|
||||||
|
|
||||||
CONFIG_SECTION = "rack-coverage"
|
|
||||||
""" Имя секции в файле конфигурации """
|
|
||||||
|
|
||||||
@no_translations
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
"""
|
|
||||||
Проверяет соблюдение принципа один метод - один файл тестов
|
|
||||||
|
|
||||||
:params args: Позиционные аргументы
|
|
||||||
:params options: Дополнительные параметры
|
|
||||||
"""
|
|
||||||
|
|
||||||
config = self._load_config()
|
|
||||||
|
|
||||||
self.stdout.write(self.style.HTTP_INFO(f"Loading configuration file: {config}"))
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _load_config(self) -> dict[str, list[str]]:
|
|
||||||
"""
|
|
||||||
Читает файл конфигурации
|
|
||||||
|
|
||||||
:return: Словарь с настройками
|
|
||||||
"""
|
|
||||||
|
|
||||||
rc_path = Path(settings.BASE_DIR) / self.CONFIG_FILE
|
|
||||||
if not rc_path.exists():
|
|
||||||
return {}
|
|
||||||
|
|
||||||
config = configparser.ConfigParser()
|
|
||||||
try:
|
|
||||||
config.read(rc_path, encoding="utf-8")
|
|
||||||
if not config.has_section(self.CONFIG_SECTION):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Парсим строки, очищая их от пробелов и переносов
|
|
||||||
raw_apps = config.get(self.CONFIG_SECTION, "apps", fallback="")
|
|
||||||
raw_ignore = config.get(self.CONFIG_SECTION, "ignore", fallback="")
|
|
||||||
|
|
||||||
apps_list = [a.strip() for a in raw_apps.splitlines() if a.strip()]
|
|
||||||
ignore_list = [i.strip() for i in raw_ignore.splitlines() if i.strip()]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"apps": apps_list,
|
|
||||||
"ignore": ignore_list
|
|
||||||
}
|
|
||||||
|
|
||||||
except configparser.Error as err:
|
|
||||||
self.stdout.write(self.style.ERROR(f"Error reading configuration file {self.CONFIG_FILE}: {err}"))
|
|
||||||
return {}
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user