153 lines
5.1 KiB
Python
153 lines
5.1 KiB
Python
"""
|
|
This module provides functionality for discovering and organizing tests in the Frappe framework.
|
|
|
|
Key components:
|
|
- discover_all_tests: Discovers all tests for specified app(s)
|
|
- discover_doctype_tests: Discovers tests for specific DocType(s)
|
|
- discover_module_tests: Discovers tests for specific module(s)
|
|
- _add_module_tests: Helper function to add tests from a module to the test runner
|
|
|
|
The module uses various strategies to find and categorize tests, including:
|
|
- Walking through app directories
|
|
- Importing test modules
|
|
- Categorizing tests (e.g., unit, integration)
|
|
- Filtering tests based on configuration
|
|
|
|
It also includes error handling and logging to facilitate debugging and provide informative error messages.
|
|
|
|
Usage:
|
|
These functions are typically called by the test runner to populate the test suite before execution.
|
|
"""
|
|
|
|
import importlib
|
|
import logging
|
|
import os
|
|
import unittest
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
import frappe
|
|
from frappe.tests import IntegrationTestCase, UnitTestCase
|
|
|
|
from .utils import debug_timer
|
|
|
|
if TYPE_CHECKING:
|
|
from .runner import TestRunner
|
|
|
|
logger = logging.getLogger("frappe.testing.discovery")
|
|
|
|
|
|
@debug_timer
|
|
def discover_all_tests(apps: list[str], runner) -> "TestRunner":
|
|
"""Discover all tests for the specified app(s)"""
|
|
logger.debug(f"Discovering tests for apps: {apps}")
|
|
if isinstance(apps, str):
|
|
apps = [apps]
|
|
try:
|
|
for app in apps:
|
|
app_path = Path(frappe.get_app_path(app))
|
|
for path, folders, files in os.walk(app_path):
|
|
folders[:] = [f for f in folders if not f.startswith(".")]
|
|
for dontwalk in ("node_modules", "locals", "public", "__pycache__"):
|
|
if dontwalk in folders:
|
|
folders.remove(dontwalk)
|
|
if os.path.sep.join(["doctype", "doctype", "boilerplate"]) in path:
|
|
continue
|
|
path = Path(path)
|
|
for file in [
|
|
path.joinpath(filename)
|
|
for filename in files
|
|
if filename.startswith("test_")
|
|
and filename.endswith(".py")
|
|
and filename != "test_runner.py"
|
|
]:
|
|
module_name = f"{'.'.join(file.relative_to(app_path.parent).parent.parts)}.{file.stem}"
|
|
_add_module_tests(runner, app, module_name)
|
|
except Exception as e:
|
|
logger.error(f"Error discovering all tests for {apps}: {e!s}")
|
|
raise TestRunnerError(f"Failed to discover tests for {apps}: {e!s}") from e
|
|
return runner
|
|
|
|
|
|
@debug_timer
|
|
def discover_doctype_tests(doctypes: list[str], runner, app: str, force: bool = False) -> "TestRunner":
|
|
"""Discover tests for the specified doctype(s)"""
|
|
if isinstance(doctypes, str):
|
|
doctypes = [doctypes]
|
|
_app = app
|
|
for doctype in doctypes:
|
|
try:
|
|
module = frappe.db.get_value("DocType", doctype, "module")
|
|
if not module:
|
|
raise TestRunnerError(f"Invalid doctype {doctype}")
|
|
|
|
# Check if the DocType belongs to the specified app
|
|
doctype_app = frappe.db.get_value("Module Def", module, "app_name")
|
|
if app is None:
|
|
_app = doctype_app
|
|
elif doctype_app != app:
|
|
raise TestRunnerError(
|
|
f"Mismatch between specified app '{app}' and doctype app '{doctype_app}'"
|
|
)
|
|
test_module = frappe.modules.utils.get_module_name(doctype, module, "test_")
|
|
force and frappe.db.delete(doctype)
|
|
_add_module_tests(runner, _app, test_module)
|
|
except Exception as e:
|
|
logger.error(f"Error discovering tests for {doctype}: {e!s}")
|
|
raise TestRunnerError(f"Failed to discover tests for {doctype}: {e!s}") from e
|
|
return runner
|
|
|
|
|
|
@debug_timer
|
|
def discover_module_tests(modules: list[str], runner, app: str) -> "TestRunner":
|
|
"""Discover tests for the specified test module"""
|
|
if isinstance(modules, str):
|
|
modules = [modules]
|
|
_app = app
|
|
try:
|
|
for module in modules:
|
|
module_app = module.split(".")[0]
|
|
if app is None:
|
|
_app = module_app
|
|
elif app != module_app:
|
|
raise TestRunnerError(f"Mismatch between specified app '{app}' and module app '{module_app}'")
|
|
_add_module_tests(runner, _app, module)
|
|
except Exception as e:
|
|
logger.error(f"Error discovering tests for {module}: {e!s}")
|
|
raise TestRunnerError(f"Failed to discover tests for {module}: {e!s}") from e
|
|
return runner
|
|
|
|
|
|
def _add_module_tests(runner, app: str, module: str):
|
|
module = importlib.import_module(module)
|
|
if runner.cfg.case:
|
|
test_suite = unittest.TestLoader().loadTestsFromTestCase(getattr(module, runner.cfg.case))
|
|
else:
|
|
test_suite = unittest.TestLoader().loadTestsFromModule(module)
|
|
|
|
for test in runner._iterate_suite(test_suite):
|
|
if runner.cfg.tests and test._testMethodName not in runner.cfg.tests:
|
|
continue
|
|
match test:
|
|
case IntegrationTestCase():
|
|
category = "integration"
|
|
case UnitTestCase():
|
|
category = "unit"
|
|
case _:
|
|
category = "unspecified-category"
|
|
if any(b.__name__ == "FrappeTestCase" for b in test.__class__.__bases__):
|
|
from frappe.deprecation_dumpster import deprecation_warning
|
|
|
|
deprecation_warning(
|
|
"2024-20-08",
|
|
"v17",
|
|
"accurate categorization of FrappeTestCase will be removed from this runner",
|
|
)
|
|
category = "old-frappe-test-class-category"
|
|
if runner.cfg.selected_categories and category not in runner.cfg.selected_categories:
|
|
continue
|
|
runner.per_app_categories[app][category].addTest(test)
|
|
|
|
|
|
class TestRunnerError(Exception):
|
|
"""Custom exception for test runner errors"""
|