fix(tests): skip uninstalled doctypes in test record dependency walk
The test runner walks link-field dependencies recursively to pre-generate test records via `get_missing_records_doctypes`. If any DocType in the transitive link graph belonged to an app not installed on the test site, the walk crashed with `DoesNotExistError`, aborting the entire suite before a single test ran. Treat such link targets as dead-end leaves instead: - `get_modules` now returns `(None, None)` when the DocType row does not exist, instead of falling through into `load_doctype_module` which raises. - `get_missing_records_doctypes` checks for `module is None`, logs a warning naming the parent DocType that linked to it, and returns without descending further. This restores the ability to run downstream test suites that link (directly or transitively) to optional/uninstalled apps without forcing every CI environment to know the full transitive link graph. Fixes #38747
This commit is contained in:
parent
166bb914c1
commit
38e140df22
2 changed files with 45 additions and 4 deletions
|
|
@ -1,7 +1,9 @@
|
|||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests.utils.generators import get_missing_records_doctypes, get_modules
|
||||
from frappe.utils.data import now_datetime
|
||||
|
||||
|
||||
|
|
@ -38,6 +40,30 @@ class TestTestUtils(IntegrationTestCase):
|
|||
with self.freeze_time(tomorrow):
|
||||
self.assertEqual(now_datetime(), tomorrow)
|
||||
|
||||
def test_get_modules_returns_none_for_missing_doctype(self):
|
||||
"""DocTypes from uninstalled apps should resolve to (None, None) instead of raising."""
|
||||
get_modules.cache_clear()
|
||||
try:
|
||||
module, test_module = get_modules("Definitely Not A Real DocType")
|
||||
finally:
|
||||
get_modules.cache_clear()
|
||||
self.assertIsNone(module)
|
||||
self.assertIsNone(test_module)
|
||||
|
||||
def test_get_missing_records_doctypes_skips_missing_doctype(self):
|
||||
"""Missing link targets should be skipped with a warning, not crash the walk."""
|
||||
get_modules.cache_clear()
|
||||
try:
|
||||
with self.assertLogs("frappe.testing.generators", level=logging.WARNING) as log_ctx:
|
||||
result = get_missing_records_doctypes("Definitely Not A Real DocType")
|
||||
finally:
|
||||
get_modules.cache_clear()
|
||||
self.assertEqual(result, [])
|
||||
self.assertTrue(
|
||||
any("Definitely Not A Real DocType" in line for line in log_ctx.output),
|
||||
f"Expected warning mentioning the missing doctype, got: {log_ctx.output}",
|
||||
)
|
||||
|
||||
|
||||
def tearDownModule():
|
||||
"""assertions for ensuring tests didn't leave state behind"""
|
||||
|
|
|
|||
|
|
@ -35,9 +35,13 @@ __all__ = [
|
|||
|
||||
|
||||
@cache
|
||||
def get_modules(doctype) -> tuple[str, ModuleType]:
|
||||
def get_modules(doctype) -> tuple[str | None, ModuleType | None]:
|
||||
"""Get the modules for the specified doctype"""
|
||||
module = frappe.db.get_value("DocType", doctype, "module")
|
||||
if not module:
|
||||
# DocType is not installed on this site (e.g. belongs to an uninstalled app);
|
||||
# treat it as a dead-end leaf rather than raising DoesNotExistError.
|
||||
return None, None
|
||||
try:
|
||||
test_module = load_doctype_module(doctype, module, "test_")
|
||||
if test_module:
|
||||
|
|
@ -49,7 +53,7 @@ def get_modules(doctype) -> tuple[str, ModuleType]:
|
|||
|
||||
|
||||
# @cache - don't cache the recursion, code depends on its recurn value declining
|
||||
def get_missing_records_doctypes(doctype, visited=None) -> list[str]:
|
||||
def get_missing_records_doctypes(doctype, visited=None, _parent=None) -> list[str]:
|
||||
"""Get the dependencies for the specified doctype in a depth-first manner"""
|
||||
|
||||
if visited is None:
|
||||
|
|
@ -62,7 +66,18 @@ def get_missing_records_doctypes(doctype, visited=None) -> list[str]:
|
|||
# Mark as visited
|
||||
visited.add(doctype)
|
||||
|
||||
_module, test_module = get_modules(doctype)
|
||||
module, test_module = get_modules(doctype)
|
||||
if module is None:
|
||||
# DocType is not installed on this site; skip it instead of crashing the
|
||||
# whole test run. This typically means a Link field points to a DocType
|
||||
# from an optional/uninstalled app.
|
||||
testing_logger.warning(
|
||||
"Skipping test record generation for %r (linked from %r): DocType not installed on this site",
|
||||
doctype,
|
||||
_parent,
|
||||
)
|
||||
return []
|
||||
|
||||
meta = frappe.get_meta(doctype)
|
||||
link_fields = meta.get_link_fields()
|
||||
|
||||
|
|
@ -79,7 +94,7 @@ def get_missing_records_doctypes(doctype, visited=None) -> list[str]:
|
|||
# Recursive depth-first traversal
|
||||
result = []
|
||||
for dep_doctype in unique_doctypes:
|
||||
result.extend(get_missing_records_doctypes(dep_doctype, visited))
|
||||
result.extend(get_missing_records_doctypes(dep_doctype, visited, _parent=doctype))
|
||||
|
||||
result.append(doctype)
|
||||
return result
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue