You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
95 lines
3.9 KiB
95 lines
3.9 KiB
"""Tests for explorer.py decomposition (P3-001).
|
|
|
|
Acceptance criteria:
|
|
- explorer.py must be under 1500 lines.
|
|
- Tab modules must define their build functions locally (not re-export from explorer).
|
|
- No circular imports between explorer.py and analysis.tabs.
|
|
"""
|
|
|
|
import ast
|
|
import inspect
|
|
import pathlib
|
|
|
|
|
|
class TestExplorerDecomposition:
|
|
"""RED test: explorer.py must be under 1500 lines."""
|
|
|
|
def test_explorer_line_count_under_1500(self):
|
|
path = pathlib.Path("explorer.py")
|
|
lines = path.read_text(encoding="utf-8").splitlines()
|
|
assert len(lines) < 1500, (
|
|
f"explorer.py has {len(lines)} lines; target is < 1500. "
|
|
f"Extract tab functions and rendering helpers into analysis/tabs/."
|
|
)
|
|
|
|
def test_tab_modules_define_functions_locally(self):
|
|
"""Each tab module must define its build_*_tab without delegating to explorer."""
|
|
tabs = [
|
|
("analysis/tabs/compass.py", "build_compass_tab"),
|
|
("analysis/tabs/trajectories.py", "build_trajectories_tab"),
|
|
("analysis/tabs/search.py", "build_search_tab"),
|
|
("analysis/tabs/browser.py", "build_browser_tab"),
|
|
("analysis/tabs/components.py", "build_svd_components_tab"),
|
|
("analysis/tabs/quiz.py", "build_mp_quiz_tab"),
|
|
]
|
|
for module_path, func_name in tabs:
|
|
source = pathlib.Path(module_path).read_text(encoding="utf-8")
|
|
tree = ast.parse(source)
|
|
func_def = None
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.FunctionDef) and node.name == func_name:
|
|
func_def = node
|
|
break
|
|
assert func_def is not None, (
|
|
f"{module_path} must define {func_name}"
|
|
)
|
|
# Ensure it's not a one-liner stub that imports from explorer
|
|
body = func_def.body
|
|
assert len(body) > 3, (
|
|
f"{module_path}.{func_name} looks like a stub ({len(body)} lines). "
|
|
f"Extract the real implementation from explorer.py."
|
|
)
|
|
|
|
def test_rendering_helpers_extracted(self):
|
|
"""Rendering helpers should not live in explorer.py."""
|
|
helpers = [
|
|
"_render_scree_plot",
|
|
"_build_party_axis_figure",
|
|
"_render_party_axis_chart",
|
|
"_render_party_axis_chart_1d",
|
|
"_render_svd_time_trajectory",
|
|
"_render_voting_results",
|
|
"_add_y_direction_annotations",
|
|
]
|
|
source = pathlib.Path("explorer.py").read_text(encoding="utf-8")
|
|
tree = ast.parse(source)
|
|
defined = {node.name for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)}
|
|
for helper in helpers:
|
|
assert helper not in defined, (
|
|
f"{helper} should be extracted from explorer.py "
|
|
f"into analysis/tabs/_rendering.py"
|
|
)
|
|
|
|
def test_no_circular_import_tabs_to_explorer(self):
|
|
"""Tab modules must not import from explorer."""
|
|
tab_modules = [
|
|
"analysis/tabs/compass.py",
|
|
"analysis/tabs/trajectories.py",
|
|
"analysis/tabs/search.py",
|
|
"analysis/tabs/browser.py",
|
|
"analysis/tabs/components.py",
|
|
"analysis/tabs/quiz.py",
|
|
"analysis/tabs/_rendering.py",
|
|
]
|
|
for module_path in tab_modules:
|
|
if not pathlib.Path(module_path).exists():
|
|
continue
|
|
source = pathlib.Path(module_path).read_text(encoding="utf-8")
|
|
assert "from explorer import" not in source, (
|
|
f"{module_path} imports from explorer.py — "
|
|
f"move shared helpers to explorer_data.py or _rendering.py instead"
|
|
)
|
|
assert "import explorer" not in source, (
|
|
f"{module_path} imports explorer module — "
|
|
f"move shared helpers to explorer_data.py or _rendering.py instead"
|
|
)
|
|
|