Начальная версия rack_coverage
This commit is contained in:
18
.rackcoveragerc
Normal file
18
.rackcoveragerc
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[rack-coverage]
|
||||||
|
# Пакеты для сканирования
|
||||||
|
packages =
|
||||||
|
net.xeaf.rack
|
||||||
|
net.xeaf.rack.foo
|
||||||
|
|
||||||
|
# То, что мы выбрасываем (маски исключения)
|
||||||
|
exclude =
|
||||||
|
*.__*
|
||||||
|
*.get_absolute_url
|
||||||
|
net.xeaf.rack.core.tests
|
||||||
|
net.xeaf.rack.core.views.experimental.*
|
||||||
|
|
||||||
|
# То, что мы ПРИНУДИТЕЛЬНО возвращаем назад (маски включения)
|
||||||
|
# Этот список имеет наивысший приоритет!
|
||||||
|
include =
|
||||||
|
*.__init__
|
||||||
|
*.__call__
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса CoverageStructureVisitor
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
|
||||||
|
|
||||||
|
class CoverageStructureVisitor(ast.NodeVisitor):
|
||||||
|
"""
|
||||||
|
Обходчик синтаксического дерева кода проекта
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, module_dot_path: str):
|
||||||
|
"""
|
||||||
|
Инициализация
|
||||||
|
|
||||||
|
:param module_dot_path: Путь к модулю
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
self.module_dot_path = module_dot_path
|
||||||
|
self.class_stack = []
|
||||||
|
self.results = []
|
||||||
|
|
||||||
|
def visit_ClassDef(self, class_node) -> None:
|
||||||
|
"""
|
||||||
|
Обход класса
|
||||||
|
|
||||||
|
:param class_node: Узел класса
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.class_stack.append(class_node.name)
|
||||||
|
self.generic_visit(class_node)
|
||||||
|
self.class_stack.pop()
|
||||||
|
|
||||||
|
def visit_FunctionDef(self, func_node) -> None:
|
||||||
|
"""
|
||||||
|
Обход функции
|
||||||
|
|
||||||
|
:param func_node: Узел функции
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._handle_function(func_node)
|
||||||
|
|
||||||
|
def visit_AsyncFunctionDef(self, async_func_node) -> None:
|
||||||
|
"""
|
||||||
|
Обход асинхронной функции
|
||||||
|
|
||||||
|
:param async_func_node: Узел асинхронной функции
|
||||||
|
"""
|
||||||
|
self._handle_function(async_func_node)
|
||||||
|
|
||||||
|
def _handle_function(self, func_node) -> None:
|
||||||
|
"""
|
||||||
|
Общий класс обработки функции
|
||||||
|
|
||||||
|
:param func_node: Узел функции
|
||||||
|
"""
|
||||||
|
|
||||||
|
func_name = func_node.name
|
||||||
|
class_name = self.class_stack[-1] if self.class_stack else None
|
||||||
|
|
||||||
|
if self.class_stack:
|
||||||
|
class_chain = ".".join(self.class_stack)
|
||||||
|
full_path = f"{self.module_dot_path}.{class_chain}.{func_name}"
|
||||||
|
else:
|
||||||
|
full_path = f"{self.module_dot_path}.{func_name}"
|
||||||
|
|
||||||
|
self.results.append((func_name, class_name, full_path))
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса CoverageFileVisitor
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
|
||||||
|
|
||||||
|
class CoverageTestFileVisitor(ast.NodeVisitor):
|
||||||
|
"""
|
||||||
|
Обходчик синтаксического дерева файла тестов
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
Инициализация
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
self.test_classes = set()
|
||||||
|
|
||||||
|
def visit_ClassDef(self, class_node) -> None:
|
||||||
|
"""
|
||||||
|
Обходчик классов
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.test_classes.add(class_node.name)
|
||||||
|
self.generic_visit(class_node)
|
||||||
352
net/xeaf/rack/management/commands/rack_coverage.py
Normal file
352
net/xeaf/rack/management/commands/rack_coverage.py
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса для команды rack_coverage
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import configparser
|
||||||
|
import fnmatch
|
||||||
|
import importlib
|
||||||
|
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 .coverage_structure_visitor import CoverageStructureVisitor
|
||||||
|
from .coverage_test_file_visitor import CoverageTestFileVisitor
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""
|
||||||
|
Проверяет соблюдение принципа один метод - один файл тестов
|
||||||
|
"""
|
||||||
|
|
||||||
|
# noinspection SpellCheckingInspection
|
||||||
|
CONFIG_FILE = '.rackcoveragerc'
|
||||||
|
""" Имя файла конфигурации """
|
||||||
|
|
||||||
|
CONFIG_SECTION = "rack-coverage"
|
||||||
|
""" Имя секции в файле конфигурации """
|
||||||
|
|
||||||
|
help = "Discover test coverage in the specified modules or the current directory."
|
||||||
|
""" Подсказка """
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
"""
|
||||||
|
Позиционный аргумент: путь к пакету от корня
|
||||||
|
|
||||||
|
:param parser: Параметры командной строки
|
||||||
|
"""
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"package_path",
|
||||||
|
type=str,
|
||||||
|
nargs="?",
|
||||||
|
help="Module paths to check; can be modulename, modulename.TestCase or modulename.TestCase.test_method"
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
|
||||||
|
# Конфигурация
|
||||||
|
config = self._load_config()
|
||||||
|
target_package = options["package_path"]
|
||||||
|
packages_to_check = [target_package] if target_package else config.get("packages", [])
|
||||||
|
|
||||||
|
exclude_patterns = config.get("exclude", [])
|
||||||
|
include_patterns = config.get("include", [])
|
||||||
|
|
||||||
|
if not packages_to_check:
|
||||||
|
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
|
||||||
|
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
|
||||||
|
continue
|
||||||
|
|
||||||
|
tests_root_name, tests_root_dir = tests_info
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING(
|
||||||
|
f"\n🔍 Анализ пакета [{package_name}] ➡️ Тесты обнаружены в [{tests_root_name}]..."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Индексируем файлы тестов в автоматически найденном корне для быстрого поиска
|
||||||
|
test_files_map: dict[str, Path] = {}
|
||||||
|
for root, _, files in os.walk(tests_root_dir):
|
||||||
|
for file in files:
|
||||||
|
if file.endswith(".py") and file != "__init__.py":
|
||||||
|
test_files_map[file] = Path(root) / file
|
||||||
|
|
||||||
|
# Рекурсивно обходим файлы боевого кода
|
||||||
|
for root, _, files in os.walk(package_dir):
|
||||||
|
current_path: Path = Path(root)
|
||||||
|
|
||||||
|
# Защита от зацикливания, если папка tests сидит внутри пакета
|
||||||
|
if "tests" in current_path.parts:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
if not file.endswith(".py") or file == "__init__.py":
|
||||||
|
continue
|
||||||
|
|
||||||
|
source_file: Path = current_path / file
|
||||||
|
relative_to_package: Path = source_file.relative_to(package_dir)
|
||||||
|
module_parts: tuple[str, ...] = relative_to_package.with_suffix("").parts
|
||||||
|
module_dot_path: str = f"{package_name}.{'.'.join(module_parts)}"
|
||||||
|
|
||||||
|
extracted_items: list[tuple[str, str | None, str]] = self._extract_classes_and_methods(
|
||||||
|
source_file, module_dot_path
|
||||||
|
)
|
||||||
|
|
||||||
|
for method_name, class_name, full_path in extracted_items:
|
||||||
|
# Двухуровневая фильтрация: include / exclude
|
||||||
|
is_included: bool = any(fnmatch.fnmatch(full_path, p) for p in include_patterns)
|
||||||
|
if not is_included:
|
||||||
|
if any(fnmatch.fnmatch(full_path, p) for p in exclude_patterns):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 3. СТРОИМ ИМЯ ФАЙЛА КЛАССА С ТЕСТАМИ
|
||||||
|
if class_name:
|
||||||
|
if method_name == "__init__":
|
||||||
|
target_file_name: str = f"{self._camel_to_snake(class_name)}_tests.py"
|
||||||
|
else:
|
||||||
|
target_file_name = (
|
||||||
|
f"{self._camel_to_snake(class_name)}_{self._camel_to_snake(method_name)}_tests.py"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
target_file_name = f"{self._camel_to_snake(method_name)}_tests.py"
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.ERROR(
|
||||||
|
f"Z Отсутствует файл тестов [{target_file_name}] для метода: {full_path}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
test_file_path: Path | None = test_files_map.get(target_file_name)
|
||||||
|
|
||||||
|
if not test_file_path:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.ERROR(
|
||||||
|
f"❌ Отсутствует файл тестов [{target_file_name}] для метода: {full_path}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
missing_files_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 4. СТРОИМ ОЖИДАЕМОЕ ИМЯ КЛАССА ТЕСТОВ
|
||||||
|
base_class: str = class_name if class_name else ""
|
||||||
|
if method_name == "__init__":
|
||||||
|
expected_class_name: str = f"{base_class}Tests"
|
||||||
|
else:
|
||||||
|
camel_method: str = "".join(
|
||||||
|
word.capitalize() for word in method_name.lstrip("_").split("_")
|
||||||
|
)
|
||||||
|
expected_class_name = f"{base_class}{camel_method}Tests"
|
||||||
|
|
||||||
|
# Парсим файл тестов и ищем в нём класс
|
||||||
|
try:
|
||||||
|
test_node: ast.Module = ast.parse(test_file_path.read_text(encoding="utf-8"))
|
||||||
|
test_visitor: CoverageTestFileVisitor = CoverageTestFileVisitor()
|
||||||
|
test_visitor.visit(test_node)
|
||||||
|
test_classes_in_file: set[str] = test_visitor.test_classes
|
||||||
|
except (SyntaxError, UnicodeDecodeError):
|
||||||
|
test_classes_in_file = set()
|
||||||
|
|
||||||
|
if expected_class_name not in test_classes_in_file:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.ERROR(
|
||||||
|
f"❌ В файле {test_file_path.relative_to(settings.BASE_DIR)} "
|
||||||
|
f"отсутствует класс [{expected_class_name}] для метода: {full_path}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
missing_classes_count += 1
|
||||||
|
else:
|
||||||
|
success_count += 1
|
||||||
|
|
||||||
|
# Вывод финального промышленного отчёта в терминал
|
||||||
|
total_errors: int = missing_files_count + missing_classes_count
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
if total_errors > 0:
|
||||||
|
raise CommandError(f"🛑 Проверка провалена. Суммарно пропущено проверок: {total_errors}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _extract_classes_and_methods(cls, file_path: Path, module_dot_path: str) -> list[tuple[str, str | None, str]]:
|
||||||
|
"""
|
||||||
|
Разбирает файл через AST, используя CoverageStructureVisitor
|
||||||
|
|
||||||
|
:param file_path: Путь к файлу
|
||||||
|
:param module_dot_path: Путь к модулю
|
||||||
|
|
||||||
|
:return: Список кортежей с именами классов, методов и именами файлов
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
node = ast.parse(file_path.read_text(encoding="utf-8"))
|
||||||
|
except (SyntaxError, UnicodeDecodeError):
|
||||||
|
return []
|
||||||
|
|
||||||
|
visitor = CoverageStructureVisitor(module_dot_path)
|
||||||
|
visitor.visit(node)
|
||||||
|
return visitor.results
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _extract_test_classes(cls, test_file_path: Path) -> set[str]:
|
||||||
|
"""
|
||||||
|
Парсит файл тестов и возвращает имена всех объявленных в нем классов
|
||||||
|
|
||||||
|
:param test_file_path: Путь к файлу с тестами
|
||||||
|
|
||||||
|
:return: Множество имен классов
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
node = ast.parse(test_file_path.read_text(encoding="utf-8"))
|
||||||
|
except (SyntaxError, UnicodeDecodeError):
|
||||||
|
return set()
|
||||||
|
|
||||||
|
visitor = CoverageTestFileVisitor()
|
||||||
|
visitor.visit(node)
|
||||||
|
return visitor.test_classes
|
||||||
|
|
||||||
|
def _autodiscover_tests_package(self, package_name: str) -> tuple[str, Path] | None:
|
||||||
|
"""
|
||||||
|
Умный алгоритм автоматического обнаружения тестовых пакетов
|
||||||
|
|
||||||
|
:param package_name: Имя пакета
|
||||||
|
|
||||||
|
:return: Кортеж из имени пакета тестов и пути к нему, или None, если не найден
|
||||||
|
"""
|
||||||
|
|
||||||
|
parts = package_name.split(".")
|
||||||
|
|
||||||
|
# Проходим циклом с конца имени пакета вверх до корня
|
||||||
|
for i in range(len(parts), 0, -1):
|
||||||
|
base_parts = parts[:i]
|
||||||
|
suffix_parts = parts[i:]
|
||||||
|
|
||||||
|
# Собираем гипотетическое имя пакета тестов
|
||||||
|
candidate_parts = base_parts + ["tests"] + suffix_parts
|
||||||
|
candidate_name = ".".join(candidate_parts)
|
||||||
|
|
||||||
|
# Проверяем, существует ли такой пакет физически на диске
|
||||||
|
candidate_dir = self._find_package_dir(candidate_name)
|
||||||
|
if candidate_dir:
|
||||||
|
return candidate_name, candidate_dir
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _camel_to_snake(cls, name: str) -> str:
|
||||||
|
"""
|
||||||
|
Переводит CamelCase в snake_case
|
||||||
|
|
||||||
|
:param name: Имя в CamelCase
|
||||||
|
|
||||||
|
:return: Имя в snake_case
|
||||||
|
"""
|
||||||
|
clean_name = name.lstrip("_")
|
||||||
|
clean_name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", clean_name)
|
||||||
|
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", clean_name).lower()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _snake_to_camel(cls, name: str) -> str:
|
||||||
|
"""
|
||||||
|
Переводит snake_case в CamelCase
|
||||||
|
|
||||||
|
:param name: Имя в snake_case
|
||||||
|
|
||||||
|
:return: Имя в CamelCase
|
||||||
|
"""
|
||||||
|
|
||||||
|
clean_name = name.lstrip("_")
|
||||||
|
return "".join(word.capitalize() for word in clean_name.split("_"))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _find_package_dir(cls, package_name: str) -> Path | None:
|
||||||
|
"""
|
||||||
|
Возвращает директорию пакета
|
||||||
|
|
||||||
|
:param package_name: Имя пакета
|
||||||
|
|
||||||
|
:return: Путь к директории пакета или None, если пакет не найден
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
module = importlib.import_module(package_name)
|
||||||
|
if hasattr(module, "__path__"):
|
||||||
|
paths = list(module.__path__)
|
||||||
|
if paths:
|
||||||
|
return Path(paths[0])
|
||||||
|
except (ModuleNotFoundError, ImportError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _load_config(self) -> dict:
|
||||||
|
"""
|
||||||
|
Загружает файл конфигурации
|
||||||
|
|
||||||
|
: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("rack-coverage"):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
raw_packages = config.get("rack-coverage", "packages", fallback="")
|
||||||
|
raw_exclude = config.get("rack-coverage", "exclude", fallback="")
|
||||||
|
raw_include = config.get("rack-coverage", "include", fallback="")
|
||||||
|
|
||||||
|
# Чистим строки от пробелов и переносов, превращая в списки
|
||||||
|
packages_list = [p.strip() for p in raw_packages.splitlines() if p.strip()]
|
||||||
|
exclude_list = [e.strip() for e in raw_exclude.splitlines() if e.strip()]
|
||||||
|
include_list = [i.strip() for i in raw_include.splitlines() if i.strip()]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"packages": packages_list,
|
||||||
|
"exclude": exclude_list,
|
||||||
|
"include": include_list
|
||||||
|
}
|
||||||
|
|
||||||
|
except configparser.Error as err:
|
||||||
|
self.stdout.write(self.style.ERROR(f"Error reading configuration file {self.CONFIG_FILE}: {err}"))
|
||||||
|
return {}
|
||||||
80
net/xeaf/rack/management/commands/rack_coverage2.py
Normal file
80
net/xeaf/rack/management/commands/rack_coverage2.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# 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 {}
|
||||||
|
|
||||||
75
net/xeaf/rack/management/commands/rack_coverage_visitor.py
Normal file
75
net/xeaf/rack/management/commands/rack_coverage_visitor.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса RackCoverageVisitor
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import configparser
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class RackCoverageVisitor(ast.NodeVisitor):
|
||||||
|
"""
|
||||||
|
Реализует методы для рекурсивного обхода дерева элементов
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, module_dot_path: str):
|
||||||
|
"""
|
||||||
|
Инициализация
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
self.module_dot_path = module_dot_path
|
||||||
|
self.class_stack = []
|
||||||
|
self.results = []
|
||||||
|
|
||||||
|
def visit_ClassDef(self, class_node) -> None:
|
||||||
|
"""
|
||||||
|
Обработка класса
|
||||||
|
|
||||||
|
:param class_node: Узел класса
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.class_stack.append(class_node.name)
|
||||||
|
self.generic_visit(class_node)
|
||||||
|
self.class_stack.pop()
|
||||||
|
|
||||||
|
def visit_FunctionDef(self, func_node) -> None:
|
||||||
|
"""
|
||||||
|
Обработка функции
|
||||||
|
|
||||||
|
:param func_node: Узел функции
|
||||||
|
"""
|
||||||
|
self._handle_function(func_node)
|
||||||
|
|
||||||
|
def visit_AsyncFunctionDef(self, async_func_node) -> None:
|
||||||
|
"""
|
||||||
|
Обработка асинхронной функции
|
||||||
|
|
||||||
|
:param async_func_node: Узел асинхронной функции
|
||||||
|
"""
|
||||||
|
self._handle_function(async_func_node)
|
||||||
|
|
||||||
|
def _handle_function(self, func_node) -> None:
|
||||||
|
"""
|
||||||
|
Общий метод для обработки функций
|
||||||
|
|
||||||
|
:param func_node: Узел функции
|
||||||
|
"""
|
||||||
|
|
||||||
|
func_name = func_node.name
|
||||||
|
if self.class_stack:
|
||||||
|
class_chain = ".".join(self.class_stack)
|
||||||
|
full_path = f"{self.module_dot_path}.{class_chain}.{func_name}"
|
||||||
|
else:
|
||||||
|
full_path = f"{self.module_dot_path}.{func_name}"
|
||||||
|
|
||||||
|
self.results.append((func_name, full_path))
|
||||||
|
|
||||||
12
net/xeaf/rack/tests/core/__init__.py
Normal file
12
net/xeaf/rack/tests/core/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Пакет описания тестов классов ядра
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .core_enum_choices_tests import CoreEnumChoicesTests
|
||||||
|
from .core_enum_keys_tests import CoreEnumKeysTests
|
||||||
7
net/xeaf/rack/tests/core/core_enum_choices_tests.py
Normal file
7
net/xeaf/rack/tests/core/core_enum_choices_tests.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from net.xeaf.rack.core.testing import CoreSimpleTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class CoreEnumChoicesTests(CoreSimpleTestCase):
|
||||||
|
|
||||||
|
def test_enum_choices(self):
|
||||||
|
self.assertEqual(1, 2)
|
||||||
7
net/xeaf/rack/tests/core/core_enum_keys_tests.py
Normal file
7
net/xeaf/rack/tests/core/core_enum_keys_tests.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from net.xeaf.rack.core.testing import CoreSimpleTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class CoreEnumKeysTests(CoreSimpleTestCase):
|
||||||
|
|
||||||
|
def test_enum_keys(self):
|
||||||
|
self.assertEqual(1, 2)
|
||||||
Reference in New Issue
Block a user