Начальная версия 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