From dd8a3a3006e15dcb2fd877461233ec86b5361259 Mon Sep 17 00:00:00 2001 From: Robert Horvath Date: Fri, 16 Oct 2026 00:37:43 +0900 Subject: [PATCH] Exercise a bundled simulation before release packaging --- .github/workflows/release.yml | 2 +- app/bundle_smoke.py | 69 +++++++++++++++++++++++++++++++++++ common/deploy/deploy.py | 21 +++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 app/bundle_smoke.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fad01bc4..d45fd85d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,7 +48,7 @@ jobs: python -m pip install --upgrade pip python -m pip install -r requirements.txt -r common/deploy/requirements.txt - - name: Build and package BobSim + - name: Build, test bundled simulation, and package BobSim shell: bash run: | version="${{ inputs.version }}" diff --git a/app/bundle_smoke.py b/app/bundle_smoke.py new file mode 100644 index 00000000..68f66e1c --- /dev/null +++ b/app/bundle_smoke.py @@ -0,0 +1,69 @@ +"""Exercise edited runtime inputs and archived results inside the built app.""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys + +import numpy as np +import pandas as pd +import yaml + +from app import app, runtime +from app.visual import reduced_scene_payload +from common.physics_provenance import ggv_physics_sha256 +from engines.dynpy.parameters import G, load_reduced_vehicle_parameters + + +def main() -> None: + """## Bundled Simulation Check + + Run a short edited-vehicle simulation in the release check's temporary home. + """ + if not getattr(sys, "frozen", False) or os.environ.get("BOBSIM_BUNDLE_SMOKE") != "1": + raise RuntimeError("This check requires a bundled app and an isolated smoke-test home.") + root = app.ROOT + assert root != runtime.PACKAGE_ROOT + assert len(ggv_physics_sha256(runtime.PACKAGE_ROOT)) == 64 + vehicle = yaml.safe_load((root / "vehicle.yml").read_text()) + vehicle["vehicle"]["name"] = "Bundle smoke vehicle" + vehicle["sprung_mass"]["mass_kg"] += 20 + for key, table in vehicle["aero"].items(): + if "_table_" in key: + vehicle["aero"][key] = np.zeros_like(table).tolist() + app.save_raw_config("vehicle", yaml.safe_dump(vehicle)) + workflow = "reduced-step-steer" + config = dict(model_dof=3, speed_mps=8, roadwheel_step_deg=0.5, + step_time_s=0.05, rise_time_s=0.02, stop_time_s=0.2, + sample_period_s=0.01, kinematics_mode="lookup") + app.save_raw_config(workflow, yaml.safe_dump(config)) + inputs = app.capture_workflow_inputs(workflow) + subprocess.run([sys.executable, "--run-module", "app.python_simulations", workflow], + cwd=root, check=True, timeout=180) + saved = app.save_active_results(workflow, input_snapshot=inputs)["saved"] + files = {item["label"]: root / item["path"] for item in saved["files"]} + summary = json.loads(files["Summary"].read_text()) + assert summary["success"] and summary["model_dof"] == 3 + assert files["Report"].read_bytes().startswith(b"%PDF") + frame = pd.read_csv(files["Signals"]) + assert len(frame) == 21 and np.isclose(frame["time"].iloc[-1], 0.2) + assert np.isclose(frame["velX"].iloc[0], 8) + params = load_reduced_vehicle_parameters(root / "vehicle.yml") + loads = frame[[f"tireFz{corner}" for corner in ("FL", "FR", "RL", "RR")]] + assert np.allclose(loads.sum(axis=1), params.mass_kg * G, atol=0.01) + archived_vehicle = root / saved["vehicle_snapshot"] + assert yaml.safe_load(archived_vehicle.read_text()) == vehicle + plot = app.result_series_payload(files["Signals"].relative_to(root).as_posix(), + x_axis="time", signals=["accY"]) + assert len(plot["x"]) == 21 + scene = reduced_scene_payload(archived_vehicle, files["Signals"], 3) + assert scene.buffer and scene.header["grip"] + assert "unavailable" not in scene.header["representation"].lower() + (root / "bundle-smoke-passed.json").write_text(json.dumps(summary), encoding="utf-8") + print("Bundled simulation, report, signals, and replay checks passed.", flush=True) + + +if __name__ == "__main__": + main() diff --git a/common/deploy/deploy.py b/common/deploy/deploy.py index 9037f2f9..a4f360bd 100644 --- a/common/deploy/deploy.py +++ b/common/deploy/deploy.py @@ -12,6 +12,7 @@ import subprocess import sys import tarfile +import tempfile from typing import Iterable import zipfile @@ -696,6 +697,25 @@ def upload_github_release(version: str | None, tag: str | None, files: Iterable[ ) +def check_bundle(mode: str) -> None: + """## Bundled Workflow Check + + Parameters + ---------- + mode : str + Built artifact layout, onefile or onedir. + """ + executable = _artifact_path(DIST_ROOT / mode, mode) + if executable.suffix == ".app": + executable /= "Contents/MacOS/BobSim" + with tempfile.TemporaryDirectory(prefix="bobsim-bundle-smoke-") as directory: + env = {**os.environ, "BOBSIM_HOME": directory, "BOBSIM_BUNDLE_SMOKE": "1"} + subprocess.run([str(executable), "--run-module", "app.bundle_smoke"], + cwd=directory, env=env, check=True, timeout=300) + if not (Path(directory) / "bundle-smoke-passed.json").is_file(): + raise RuntimeError("The packaged simulation check did not finish.") + + def build_release( mode: str, version: str | None, @@ -706,6 +726,7 @@ def build_release( ) -> None: clean_generated_artifacts(include_deploy=True) build_pyinstaller(mode, install_deps, skip_conflict_check) + check_bundle(mode) archive, checksum_path = package_release(mode, version) if upload_release: upload_github_release(version, tag, [archive, checksum_path])