Улучшение отчета для 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 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 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]]:

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