seitime-frappe/frappe/testing/discovery.py
David Arnold 4000cba810
fix: compat FrappeTestCase (#28367)
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
2024-11-05 18:16:22 +01:00

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"""