Улучшение отчета для rack_coverage

This commit is contained in:
2026-07-17 22:32:42 +03:00
parent 579d014dde
commit 11b332be2d
3 changed files with 79 additions and 103 deletions

View File

@@ -35,3 +35,40 @@ class CoreCommand(BaseCommand):
"""
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}")

View File

@@ -12,19 +12,20 @@ import ast
import configparser
import fnmatch
import importlib
import sys
import os
import re
from pathlib import Path
from django.conf import settings
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_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."
""" Подсказка """
success_count: int = 0
""" Счетчик успешно проверенных методов """
missing_files_count: int = 0
""" Счетчик не найденных файлов с тестами """
missing_classes_count: int = 0
""" Счетчик не найденных классов с тестами """
skipped_packages_count: int = 0
""" Счетчик пропущенных пакетов """
def add_arguments(self, parser):
"""
Позиционный аргумент: путь к пакету от корня
@@ -67,23 +80,19 @@ class Command(BaseCommand):
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:
package_dir: Path | None = self._find_package_dir(package_name)
if not package_dir:
self.stdout.write(self.style.WARNING(f"⚠️ Пакет '{package_name}' пропущен: не найдена его директория"))
skipped_packages_count += 1
self.skipped_packages_count += 1
continue
# Смарт-автообнаружение зеркального пакета тестов вверх по иерархии
tests_info: tuple[str, Path] | None = self._autodiscover_tests_package(package_name)
if not tests_info:
self.stdout.write(self.style.ERROR(f"❌ Не удалось программно найти пакет тестов для: {package_name}"))
skipped_packages_count += 1
self.skipped_packages_count += 1
continue
tests_root_name, tests_root_dir = tests_info
@@ -152,7 +161,7 @@ class Command(BaseCommand):
f"❌ Отсутствует файл тестов [{target_file_name}] для метода: {full_path}"
)
)
missing_files_count += 1
self.missing_files_count += 1
continue
# 4. СТРОИМ ОЖИДАЕМОЕ ИМЯ КЛАССА ТЕСТОВ
@@ -181,26 +190,36 @@ class Command(BaseCommand):
f"отсутствует класс [{expected_class_name}] для метода: {full_path}"
)
)
missing_classes_count += 1
self.missing_classes_count += 1
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)
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:
self.stdout.write(self.style.ERROR(f" ❌ Отсутствует файлов тестов: {missing_files_count}"))
if missing_classes_count > 0:
self.stdout.write(self.style.ERROR(f" ❌ Отсутствует классов внутри файлов тестов: {missing_classes_count}"))
self.stdout.write("=" * 60 + "\n")
def _final_report(self):
"""
Выводит финальный отчёт в терминал
"""
total_errors: int = self.missing_files_count + self.missing_classes_count
self._report_separator(first_new_line=True)
self.stdout.write(self.style.SUCCESS(f"Test coverage report:"))
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:
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
def _extract_classes_and_methods(cls, file_path: Path, module_dot_path: str) -> list[tuple[str, str | None, str]]:

View File

@@ -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 {}