diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index 83b747be..6847c549 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -20,7 +20,7 @@ the last section. | ISO 7401 step-steer / 4138 understeer / 3888-2 DLC — run + measured | — | | L1↔L2↔L3 cross-model consistency where physics overlaps | Dependent axles (twist-beam / solid beam) — configs are stubs | | FMI round-trip Δ=0 (machine precision); ISO 8608 PSD RMS per class | L3 unsprung lateral-transfer term (small) | -| Full suite: **492/492 ctest green** (`validation` preset, as of 2026-09-18) | — | +| Full suite: **492/492 ctest green** (as of 2026-09-18; `validation` preset — see the currency block below) | — | | Drivetrain engine inertia (open-diff carrier coupling) | — | | **ISO step-steer signature gated in CI** (`ctest -R IsoBaseline`, sedan L2 LuGre) | DLC moose gate is a preset property, not a defect (see note) | @@ -52,7 +52,7 @@ tests: 492/492 config: cmake --preset validation && cmake --build --preset validation && ctest --preset validation presets: CMakePresets.json@fb1f0c198a0201fcce2543a9f5b2242a255fb97a toolchain: cmake 3.31.10 -commit: 1fc790e +commit: 13e28de date: 2026-09-18 excluded: gui_v3_e2e (GUI v3 test group formally deferred -- VDSIM_BUILD_GUI_V3_TESTS=OFF) excluded: gui_v3_api_smoke (GUI v3 test group formally deferred -- VDSIM_BUILD_GUI_V3_TESTS=OFF) diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index ca1ed366..818ef8b1 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -26,6 +26,7 @@ if(pybind11_FOUND) ${CMAKE_SOURCE_DIR}/python/vdsim_trace.py ${CMAKE_SOURCE_DIR}/python/vdsim_render.py ${CMAKE_SOURCE_DIR}/python/vdsim_render3d.py + ${CMAKE_SOURCE_DIR}/python/vdsim_preset.py ${CMAKE_SOURCE_DIR}/cosim/protocol.py ${CMAKE_SOURCE_DIR}/examples/opendrive.py ${CMAKE_SOURCE_DIR}/examples/rd5_route.py diff --git a/tests/scripts/test_render_presets.py b/tests/scripts/test_render_presets.py index 712f4ecd..1c53e1f3 100755 --- a/tests/scripts/test_render_presets.py +++ b/tests/scripts/test_render_presets.py @@ -13,8 +13,10 @@ """ from __future__ import annotations +import ast import json import math +import re import sys import tempfile from pathlib import Path @@ -267,6 +269,66 @@ def test_layout_guard_catches_an_escape(): plt.close(fig) +def _shipped_module_names(): + """Top-level module names the wheel installs, read from the install rules. + + The wheel manifest lives in ``python/CMakeLists.txt`` under + ``if(DEFINED SKBUILD)``; parsing it here keeps the check honest when that + list changes, instead of restating the list in a second place. + """ + text = (REPO / "python" / "CMakeLists.txt").read_text() + body = text.split("if(DEFINED SKBUILD)", 1)[1].split("endif()", 1)[0] + names = {"vdsim"} # install(TARGETS vdsim_py ...) + for m in re.finditer(r"\$\{CMAKE_SOURCE_DIR\}/([^\s)]+)", body): + rel = m.group(1) + stem = Path(rel).name + names.add(stem[:-3] if stem.endswith(".py") else stem) + return names + + +def _first_party_imports(py_path): + """Top-level names ``py_path`` imports that resolve inside this repo.""" + tree = ast.parse(py_path.read_text()) + wanted = set() + for node in ast.walk(tree): + if isinstance(node, ast.Import): + wanted.update(a.name.split(".")[0] for a in node.names) + elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module: + wanted.add(node.module.split(".")[0]) + here = set() + for name in wanted: + for d in ("python", "cosim", "examples"): + if (REPO / d / (name + ".py")).exists() or (REPO / d / name).is_dir(): + here.add(name) + break + return here + + +def test_wheel_ships_every_first_party_import(): + """A shipped module may not import a module the wheel leaves behind. + + Measured 2026-09-18 (pre-flight C-2): the wheel carried vdsim_render.py but + not vdsim_preset.py, so ``import vdsim_render`` raised ModuleNotFoundError + for every pip user while the source tree stayed green. The failure mode is + invisible in-tree, which is why it is pinned here rather than left to the + packaging job. + """ + shipped = _shipped_module_names() + check("vdsim_preset" in shipped, "the wheel ships vdsim_preset") + missing = [] + for name in sorted(shipped): + for d in ("python", "cosim", "examples"): + p = REPO / d / (name + ".py") + if p.exists(): + for dep in sorted(_first_party_imports(p)): + if dep not in shipped: + missing.append("%s imports %s" % (name, dep)) + break + check(missing == [], + "every shipped module's first-party imports are shipped too (%s)" + % (", ".join(missing) if missing else "none missing")) + + def main(): test_builtin_surface() test_unknown_and_reserved_names_are_refused() @@ -277,6 +339,7 @@ def main(): test_required_channel_is_an_error() test_panel_geometry_is_fixed() test_layout_guard_catches_an_escape() + test_wheel_ships_every_first_party_import() if FAILURES: print("\n%d check(s) failed" % len(FAILURES)) for f in FAILURES: