due to circular imports issues and me going out of my way to make it work 'cleanly', the previous backwards compatibility for FrappeTestCase unfortunately did not work on the manual cli test runner 'run-tests' While not generally not affecting CI (which is precedented by the framwork's best practices to use 'run-parallel-test'), this broke some manual developer workflows The restauration of FrappeTestCase in these scenario now unfortunately involves a plain copy of almost an entire implementation into the dumpster. On the one hand, this doesn not accurately reflect the rather minuscule differences between IntegrationTestCase and FrappeTestCase, but on the other hand, it shields and freezes the old api should IntegrationTestCase evolve futher
151 lines
5 KiB
Python
151 lines
5 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 _:
|
|
from frappe.deprecation_dumpster import deprecation_warning
|
|
|
|
deprecation_warning(
|
|
"2024-20-08",
|
|
"v17",
|
|
"discovery and categorization of FrappeTestCase will be removed from this runner",
|
|
)
|
|
category = "deprecated-old-style-unspecified"
|
|
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"""
|