Улучшение отчета для rack_coverage
This commit is contained in:
@@ -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}")
|
||||
|
||||
@@ -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]]:
|
||||
|
||||
@@ -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