Merge pull request #38748 from barredterra/fix/test-runner-missing-doctype

fix(tests): skip uninstalled doctypes in test record dependency walk
This commit is contained in:
Ejaaz Khan 2026-04-23 23:21:48 +05:30 committed by GitHub
commit be6e85fe9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 45 additions and 4 deletions

View file

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

View file

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