Files
StarPilot/openpilot/starpilot/tests/test_control_host_boundary.py
T
2026-10-24 23:26:26 -05:00

194 lines
9.5 KiB
Python

"""Native upstream host neutralization under supplied state, not mode qualification.
The input messages are synthetic, explicitly supplied through SubMaster.update_msgs.
The real Controls constructor, longitudinal controller, publication and native IPC
run in a fresh child process. No manager, vehicle, policy coordinator or CAN runs.
"""
import argparse
import ctypes
import hashlib
import importlib
import importlib.machinery
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
from openpilot.common.basedir import BASEDIR
def probe(system_longitudinal, output):
# Check isolation before importing libraries which own process-global IPC state.
prefix = os.environ.get("OPENPILOT_PREFIX", "")
if not prefix.startswith("host-boundary-") or not os.environ.get("PARAMS_ROOT") or "CEREAL_FAKE" in os.environ:
raise RuntimeError("An isolated native host-boundary test environment is required")
from opendbc.car import gen_empty_fingerprint
from opendbc.car.tesla.interface import CarInterface
from opendbc.car.tesla.values import CAR
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.common import params as params_module
from openpilot.selfdrive.controls.controlsd import Controls
native = {}
for name in ("capnp.lib.capnp", "msgq.ipc_pyx"):
module_file = importlib.import_module(name).__file__
if module_file is None:
raise RuntimeError(f"Native extension has no file: {name}")
path = Path(module_file).resolve()
if not any(str(path).endswith(suffix) for suffix in importlib.machinery.EXTENSION_SUFFIXES):
raise RuntimeError(f"Host boundary requires a native extension: {name}")
native[name] = {"path": str(path), "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}
if not isinstance(params_module.lib, ctypes.CDLL):
raise RuntimeError("Host boundary requires native Params")
params_library = Path(params_module.lib._name).resolve()
native["Params C ABI"] = {"path": str(params_library), "sha256": hashlib.sha256(params_library.read_bytes()).hexdigest()}
params = Params()
fingerprint = gen_empty_fingerprint()
fingerprint[2][0x293] = 8
cp = CarInterface.get_params(CAR.TESLA_MODEL_Y, fingerprint, [], system_longitudinal, False, False)
car_params_bytes = cp.to_bytes()
params.put("CarParams", car_params_bytes, block=True)
controls = Controls()
subscriber = messaging.sub_sock("carControl", timeout=1000)
observations = []
phases = (
("active", True, True, False, False, 1.0),
("longitudinal_override", True, True, True, False, 1.0),
("lateral_fault", True, True, False, True, 1.0),
("disabled", False, False, False, False, 1.0),
("disabled_negative_plan", False, False, False, False, -2.0),
("explicitly_reenabled", True, True, False, False, 1.0),
)
frame = 0
for name, enabled, active, override, steering_fault, target in phases:
for _ in range(12):
timestamp = 1_000_000_000 + frame * 10_000_000
frame += 1
messages = []
def message(service, size=None, timestamp=timestamp, messages=messages):
msg = messaging.new_message(service, size, valid=True, logMonoTime=timestamp)
messages.append(msg)
return getattr(msg, service)
state = message("carState")
state.canValid = True # supplied synthetic host input, not observed bus health
state.vEgo = state.vEgoRaw = 10.0
state.vCruise = state.vCruiseCluster = 100.0
state.steeringAngleDeg = 0.0
state.steerFaultTemporary = steering_fault
state.cruiseState.enabled = True
state.cruiseState.available = True
state.gearShifter = "drive"
state.seatbeltUnlatched = False
host = message("selfdriveState")
host.enabled, host.active = enabled, active
host.state = "enabled" if enabled else "disabled"
parameters = message("vehicleParameters")
parameters.stiffnessFactor = 1.0
parameters.steerRatio = cp.steerRatio
message("longitudinalPlan").aTarget = target
message("modelV2").action.desiredCurvature = 0.0001
message("lateralDelay").lateralDelay = 0.1
message("carOutput")
events = message("onroadEvents", 1 if override else 0)
if override:
events[0].name = "gasPressedOverride"
events[0].overrideLongitudinal = True
controls.sm.update_msgs(timestamp / 1e9, [msg.as_reader() for msg in messages])
command, lateral_log = controls.state_control()
controls.publish(command, lateral_log)
received = messaging.recv_one(subscriber)
if received is None:
raise RuntimeError("Native carControl publication was not received")
emitted = received.carControl
observations.append({"phase": name, "frame": frame, "plan_acceleration": target,
"enabled": emitted.enabled, "lateral": emitted.latActive, "longitudinal": emitted.longActive,
"acceleration": emitted.actuators.accel, "steering_angle": emitted.actuators.steeringAngleDeg,
"cancel": emitted.cruiseControl.cancel, "override": emitted.cruiseControl.override,
"valid": received.valid})
source_paths = ("openpilot/selfdrive/controls/controlsd.py", "openpilot/selfdrive/controls/lib/longcontrol.py",
"openpilot/starpilot/tests/test_control_host_boundary.py")
report = {"scope": "synthetic upstream host neutralization and native publication only",
"vehicle_qualification": "not_established", "system_longitudinal": system_longitudinal,
"identity": "Model Y candidate deliberately supplied; EPS firmware and physical hardware not identified",
"native": native,
"car_params_sha256": hashlib.sha256(car_params_bytes).hexdigest(),
"sources": {path: hashlib.sha256((Path(BASEDIR) / path).read_bytes()).hexdigest() for path in source_paths},
"observations": observations}
Path(output).write_text(json.dumps(report, indent=2) + "\n")
class TestControlHostBoundary(unittest.TestCase):
def check_profile(self, system_longitudinal):
ipc_root = "/tmp" if sys.platform == "darwin" else "/dev/shm"
with tempfile.TemporaryDirectory() as temporary, tempfile.TemporaryDirectory(prefix="msgq_host-boundary-", dir=ipc_root) as ipc:
output = Path(temporary) / "observations.json"
environment = dict(os.environ, PARAMS_ROOT=str(Path(temporary) / "params"),
OPENPILOT_PREFIX=Path(ipc).name.removeprefix("msgq_"))
for name in ("CEREAL_FAKE", "CEREAL_FAKE_PREFIX", "SIMULATION"):
environment.pop(name, None)
command = [sys.executable, "-m", "openpilot.starpilot.tests.test_control_host_boundary", "--probe",
"--owner", "system" if system_longitudinal else "stock", "--output", str(output)]
completed = subprocess.run(command, cwd=BASEDIR, env=environment, capture_output=True, text=True, timeout=30)
self.assertEqual(completed.returncode, 0, completed.stdout + completed.stderr)
report = json.loads(output.read_text())
self.assertEqual(len(report["observations"]), 72)
for row in report["observations"]:
with self.subTest(phase=row["phase"], frame=row["frame"]):
disabled = row["phase"].startswith("disabled")
longitudinal = system_longitudinal and row["phase"] not in ("longitudinal_override", "disabled", "disabled_negative_plan")
self.assertTrue(row["valid"])
self.assertEqual(row["enabled"], not disabled)
self.assertEqual(row["lateral"], not disabled and row["phase"] != "lateral_fault")
self.assertEqual(row["longitudinal"], longitudinal)
self.assertEqual(row["cancel"], disabled)
self.assertEqual(row["override"], system_longitudinal and row["phase"] == "longitudinal_override")
if longitudinal:
self.assertGreater(row["acceleration"], 0.0)
else:
self.assertEqual(row["acceleration"], 0.0)
def test_controls_import_does_not_load_model_execution(self):
program = """
import importlib.abc
import sys
class NoModelExecution(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname == 'openpilot.selfdrive.modeld.modeld' or fullname == 'tinygrad' or fullname.startswith('tinygrad.'):
raise AssertionError('Controls must not import model execution: ' + fullname)
return None
sys.meta_path.insert(0, NoModelExecution())
from openpilot.selfdrive.controls.controlsd import LAT_SMOOTH_SECONDS
assert type(LAT_SMOOTH_SECONDS) is float and LAT_SMOOTH_SECONDS == 0.0
"""
completed = subprocess.run([sys.executable, '-B', '-c', program], cwd=BASEDIR,
capture_output=True, text=True, timeout=30)
self.assertEqual(completed.returncode, 0, completed.stdout + completed.stderr)
def test_inactive_system_longitudinal_neutralizes_warmed_controller_and_plan(self):
self.check_profile(True)
def test_stock_longitudinal_never_gets_host_acceleration(self):
with patch.dict(os.environ, {"CEREAL_FAKE": "1", "CEREAL_FAKE_PREFIX": "must-not-be-used"}):
self.check_profile(False)
if __name__ == "__main__":
if "--probe" in sys.argv:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--probe", action="store_true")
parser.add_argument("--owner", choices=("stock", "system"), required=True)
parser.add_argument("--output", type=Path, required=True)
arguments = parser.parse_args()
probe(arguments.owner == "system", arguments.output)
else:
unittest.main()