From 11b332be2d700278eaa8bb8aba6fdd61003688c3 Mon Sep 17 00:00:00 2001 From: "Nick V. Anokhin" Date: Fri, 17 Jul 2026 22:32:42 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BE=D1=82=D1=87=D0=B5=D1=82=D0=B0=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20rack=5Fcoverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- net/xeaf/rack/core/core_command.py | 37 +++++++++ .../rack/management/commands/rack_coverage.py | 65 +++++++++------ .../management/commands/rack_coverage2.py | 80 ------------------- 3 files changed, 79 insertions(+), 103 deletions(-) delete mode 100644 net/xeaf/rack/management/commands/rack_coverage2.py diff --git a/net/xeaf/rack/core/core_command.py b/net/xeaf/rack/core/core_command.py index 6f49602..294c347 100644 --- a/net/xeaf/rack/core/core_command.py +++ b/net/xeaf/rack/core/core_command.py @@ -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}") diff --git a/net/xeaf/rack/management/commands/rack_coverage.py b/net/xeaf/rack/management/commands/rack_coverage.py index 13352a7..ff1599c 100644 --- a/net/xeaf/rack/management/commands/rack_coverage.py +++ b/net/xeaf/rack/management/commands/rack_coverage.py @@ -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]]: diff --git a/net/xeaf/rack/management/commands/rack_coverage2.py b/net/xeaf/rack/management/commands/rack_coverage2.py deleted file mode 100644 index 10809d7..0000000 --- a/net/xeaf/rack/management/commands/rack_coverage2.py +++ /dev/null @@ -1,80 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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 {} -