aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_patch_targets_are_live.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-23 22:25:48 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-24 16:45:37 +0200
commit73e04868614160ab6cf98b1c1206f3a9b3840c02 (patch)
tree5f037b96cb29b4f8c6b0d7257458f2e8ad9186a0 /packages/meshbay-node/tests/test_patch_targets_are_live.py
parent328b01a2dd545d70a078db8df1e91b02d65bfc9c (diff)
downloadmeshbay-73e04868614160ab6cf98b1c1206f3a9b3840c02.tar.gz
test(node): a patched module attribute must be read where it is patched
A function reads its globals from the module that defines it, so patching a name a module only re-exports changes nothing and the test passes anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests/test_patch_targets_are_live.py')
-rw-r--r--packages/meshbay-node/tests/test_patch_targets_are_live.py118
1 files changed, 118 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_patch_targets_are_live.py b/packages/meshbay-node/tests/test_patch_targets_are_live.py
new file mode 100644
index 0000000..c1dd905
--- /dev/null
+++ b/packages/meshbay-node/tests/test_patch_targets_are_live.py
@@ -0,0 +1,118 @@
+"""
+A monkeypatch on a module attribute only works where that module *reads* the
+attribute. A function looks its globals up in the module it is defined in, so
+patching a name that a module merely imports and passes on — a re-export —
+replaces a binding nothing reads: the code under test keeps the original, and
+the test goes on passing for a reason that has nothing to do with it.
+
+`monkeypatch.setattr` on a name that does not exist at all raises, which is
+loud and fine. This test covers the quiet case: every module attribute a test
+in this suite patches must be read by that module's own code, or defined there.
+"""
+
+import ast
+import importlib
+import types
+from pathlib import Path
+
+TESTS = Path(__file__).resolve().parent
+
+
+def _module_aliases(tree: ast.Module) -> dict[str, str]:
+ """alias -> dotted name, for every name a test module binds by import."""
+ out = {}
+ for node in ast.walk(tree):
+ if isinstance(node, ast.Import):
+ for a in node.names:
+ if a.asname:
+ out[a.asname] = a.name
+ else:
+ out[a.name.split(".")[0]] = a.name.split(".")[0]
+ elif isinstance(node, ast.ImportFrom) and node.module and not node.level:
+ for a in node.names:
+ out[a.asname or a.name] = f"{node.module}.{a.name}"
+ return out
+
+
+def _resolve(dotted: str):
+ """The object a dotted name designates, or None."""
+ parts = dotted.split(".")
+ for i in range(len(parts), 0, -1):
+ try:
+ obj = importlib.import_module(".".join(parts[:i]))
+ except ImportError:
+ continue
+ try:
+ for p in parts[i:]:
+ obj = getattr(obj, p)
+ except AttributeError:
+ return None
+ return obj
+ return None
+
+
+def _patches(path: Path):
+ """(module_object, attribute, line) for every replacement of a module
+ attribute: `monkeypatch.setattr`, `patch`, `patch.object`, or a plain
+ assignment."""
+ tree = ast.parse(path.read_text(encoding="utf-8"))
+ aliases = _module_aliases(tree)
+ for node in ast.walk(tree):
+ # `module.NAME = stand_in`, restored by hand in a `finally`.
+ if isinstance(node, ast.Assign):
+ for t in node.targets:
+ if isinstance(t, ast.Attribute) and isinstance(t.value, ast.Name) \
+ and t.value.id in aliases:
+ yield _resolve(aliases[t.value.id]), t.attr, node.lineno
+ continue
+ if not isinstance(node, ast.Call) or not node.args:
+ continue
+ f = node.func
+ name = (f.attr if isinstance(f, ast.Attribute)
+ else f.id if isinstance(f, ast.Name) else "")
+ first = node.args[0]
+ if name in ("setattr", "object") and len(node.args) >= 2 \
+ and isinstance(first, ast.Name) \
+ and isinstance(node.args[1], ast.Constant) \
+ and isinstance(node.args[1].value, str):
+ target = _resolve(aliases.get(first.id, ""))
+ yield target, node.args[1].value, node.lineno
+ elif name in ("setattr", "patch") and isinstance(first, ast.Constant) \
+ and isinstance(first.value, str) and "." in first.value:
+ mod, _, attr = first.value.rpartition(".")
+ yield _resolve(mod), attr, node.lineno
+
+
+def _reads_or_defines(module: types.ModuleType, attr: str) -> bool:
+ tree = ast.parse(Path(module.__file__).read_text(encoding="utf-8"))
+ for node in ast.walk(tree):
+ if isinstance(node, ast.Name) and node.id == attr and \
+ isinstance(node.ctx, ast.Load):
+ return True
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) \
+ and node.name == attr:
+ return True
+ if isinstance(node, (ast.Assign, ast.AnnAssign, ast.AugAssign)):
+ targets = node.targets if isinstance(node, ast.Assign) else [node.target]
+ if any(isinstance(t, ast.Name) and t.id == attr for t in targets):
+ return True
+ return False
+
+
+def test_every_patched_module_attribute_is_read_where_it_is_patched():
+ dead, checked = [], 0
+ for path in sorted(TESTS.glob("*.py")):
+ for target, attr, line in _patches(path):
+ if not isinstance(target, types.ModuleType) or not getattr(
+ target, "__file__", None):
+ continue # a class or an instance: an attribute lookup, always live
+ if not target.__name__.startswith("meshbay_"):
+ continue # the standard library and dependencies are not ours to police
+ checked += 1
+ if not _reads_or_defines(target, attr):
+ dead.append(f"{path.name}:{line} {target.__name__}.{attr}")
+ assert checked > 50, "the scan found almost nothing to check — did the patterns change?"
+ assert not dead, (
+ "these patches replace a name the module only imports; the code under "
+ "test reads it somewhere else, so the patch changes nothing:\n "
+ + "\n ".join(dead))