mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-08-21 11:23:47 +08:00
260 lines
9.7 KiB
Python
260 lines
9.7 KiB
Python
"""
|
|
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
|
|
|
This file is part of sunnypilot and is licensed under the MIT License.
|
|
See the LICENSE.md file in the root directory for more details.
|
|
|
|
Per-bug regression tests for the Raylib-vs-schema parity audit. Each test
|
|
isolates one of the gating bugs that the design-overhaul branch fixes so a
|
|
future regression is loud and obvious. These tests are intentionally narrow
|
|
and additive — they do not replace the broader test_settings_schema.py.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from typing import Any
|
|
|
|
from openpilot.common.parameterized import parameterized
|
|
|
|
from openpilot.sunnypilot.sunnylink.tools.generate_settings_schema import (
|
|
DEFINITION_PATH,
|
|
TORQUE_VERSIONS_PATH,
|
|
_build_torque_options,
|
|
_load_torque_versions,
|
|
generate_schema,
|
|
)
|
|
from openpilot.sunnypilot.sunnylink.tools.validate_settings_ui import validate as validate_settings_ui
|
|
from openpilot.common.test import OpenpilotTestCase
|
|
|
|
|
|
def _walk_items(schema: dict[str, Any]):
|
|
"""Yield every item dict from the schema."""
|
|
|
|
def _yield(item: dict[str, Any]):
|
|
yield item
|
|
for sub in item.get("sub_items", []):
|
|
yield from _yield(sub)
|
|
|
|
for panel in schema.get("panels", []):
|
|
for section in panel.get("sections", []):
|
|
for item in section.get("items", []):
|
|
yield from _yield(item)
|
|
for sp in section.get("sub_panels", []):
|
|
for item in sp.get("items", []):
|
|
yield from _yield(item)
|
|
for item in panel.get("items", []):
|
|
yield from _yield(item)
|
|
for sp in panel.get("sub_panels", []):
|
|
for item in sp.get("items", []):
|
|
yield from _yield(item)
|
|
for brand in schema.get("vehicle_settings", {}).values():
|
|
items = brand.get("items", []) if isinstance(brand, dict) else brand
|
|
for item in items:
|
|
yield from _yield(item)
|
|
|
|
|
|
def _find_item(schema: dict[str, Any], key: str) -> dict[str, Any] | None:
|
|
for item in _walk_items(schema):
|
|
if item.get("key") == key:
|
|
return item
|
|
return None
|
|
|
|
|
|
def _find_section(schema: dict[str, Any], panel_id: str, section_id: str) -> dict[str, Any] | None:
|
|
for panel in schema.get("panels", []):
|
|
if panel.get("id") != panel_id:
|
|
continue
|
|
for section in panel.get("sections", []):
|
|
if section.get("id") == section_id:
|
|
return section
|
|
return None
|
|
|
|
|
|
def _flatten_rule_types(rules: list[dict[str, Any]] | None) -> set[str]:
|
|
out: set[str] = set()
|
|
|
|
def _walk(rule: dict[str, Any]) -> None:
|
|
out.add(rule.get("type", ""))
|
|
if rule.get("type") == "not" and "condition" in rule:
|
|
_walk(rule["condition"])
|
|
elif rule.get("type") in ("any", "all"):
|
|
for c in rule.get("conditions", []):
|
|
_walk(c)
|
|
|
|
for rule in rules or []:
|
|
_walk(rule)
|
|
return out
|
|
|
|
|
|
def _references_capability_field(rules: list[dict[str, Any]] | None, field: str) -> bool:
|
|
found = False
|
|
|
|
def _walk(rule: dict[str, Any]) -> None:
|
|
nonlocal found
|
|
if rule.get("type") == "capability" and rule.get("field") == field:
|
|
found = True
|
|
elif rule.get("type") == "not" and "condition" in rule:
|
|
_walk(rule["condition"])
|
|
elif rule.get("type") in ("any", "all"):
|
|
for c in rule.get("conditions", []):
|
|
_walk(c)
|
|
|
|
for rule in rules or []:
|
|
_walk(rule)
|
|
return found
|
|
|
|
|
|
def _has_toyota_virtual_cruise_gate(rules: list[dict[str, Any]] | None) -> bool:
|
|
def _walk(rule: dict[str, Any]) -> bool:
|
|
if rule.get("type") == "all":
|
|
conditions = rule.get("conditions", [])
|
|
has_capability = any(
|
|
c.get("type") == "capability" and c.get("field") == "toyota_virtual_cruise_speed_available" and c.get("equals") is True for c in conditions
|
|
)
|
|
has_param = any(c.get("type") == "param" and c.get("key") == "ToyotaVirtualCruiseSpeed" and c.get("equals") is True for c in conditions)
|
|
if has_capability and has_param:
|
|
return True
|
|
|
|
if rule.get("type") == "not" and "condition" in rule:
|
|
return _walk(rule["condition"])
|
|
if rule.get("type") in ("any", "all"):
|
|
return any(_walk(c) for c in rule.get("conditions", []))
|
|
return False
|
|
|
|
return any(_walk(rule) for rule in rules or [])
|
|
|
|
|
|
def schema():
|
|
return generate_schema()
|
|
|
|
|
|
class TestMadsBrandGates(OpenpilotTestCase):
|
|
def test_mads_main_cruise_has_brand_gate(self, schema):
|
|
"""MadsMainCruiseAllowed must gate on brand and tesla_has_vehicle_bus."""
|
|
item = _find_item(schema, "MadsMainCruiseAllowed")
|
|
assert item is not None
|
|
assert _references_capability_field(item.get("enablement"), "brand")
|
|
assert _references_capability_field(item.get("enablement"), "tesla_has_vehicle_bus")
|
|
|
|
def test_mads_unified_engagement_has_brand_gate(self, schema):
|
|
"""MadsUnifiedEngagementMode must mirror MadsMainCruiseAllowed brand-gate."""
|
|
item = _find_item(schema, "MadsUnifiedEngagementMode")
|
|
assert item is not None
|
|
assert _references_capability_field(item.get("enablement"), "brand")
|
|
assert _references_capability_field(item.get("enablement"), "tesla_has_vehicle_bus")
|
|
|
|
|
|
class TestTestManeuversSection(OpenpilotTestCase):
|
|
def test_lateral_maneuver_mode_in_test_maneuvers(self, schema):
|
|
section = _find_section(schema, "developer", "test_maneuvers")
|
|
assert section is not None, "developer.test_maneuvers section missing"
|
|
keys = {item["key"] for item in section.get("items", [])}
|
|
assert "LateralManeuverMode" in keys
|
|
assert "LongitudinalManeuverMode" in keys
|
|
|
|
def test_test_maneuvers_section_requires_attestation(self, schema):
|
|
section = _find_section(schema, "developer", "test_maneuvers")
|
|
assert section is not None
|
|
assert section.get("attestation_required") is True
|
|
|
|
def test_test_maneuvers_section_visibility_gate(self, schema):
|
|
section = _find_section(schema, "developer", "test_maneuvers")
|
|
assert section is not None
|
|
visibility = section.get("visibility")
|
|
assert visibility, "test_maneuvers must have visibility gate"
|
|
vis_refs = json.dumps(visibility)
|
|
assert "is_development" in vis_refs
|
|
assert "is_sp_release" in vis_refs
|
|
enablement = section.get("enablement") or []
|
|
enable_refs = json.dumps(enablement)
|
|
assert "ShowAdvancedControls" in enable_refs, "test_maneuvers must gate ShowAdvancedControls via enablement"
|
|
|
|
|
|
class TestValidator(OpenpilotTestCase):
|
|
def test_validator_accepts_real_json(self):
|
|
"""settings_ui.json passes the repository's production schema validator."""
|
|
self.assertTrue(validate_settings_ui(DEFINITION_PATH))
|
|
|
|
|
|
class TestTorqueOptionGeneration(OpenpilotTestCase):
|
|
def test_torque_versions_match_generated_options(self, schema):
|
|
versions = _load_torque_versions()
|
|
assert versions, "latcontrol_torque_versions.json must have at least one version"
|
|
expected = _build_torque_options(versions)
|
|
item = _find_item(schema, "TorqueControlTune")
|
|
assert item is not None, "TorqueControlTune item must be present"
|
|
assert item.get("options") == expected
|
|
|
|
def test_torque_versions_path_resolves(self):
|
|
assert os.path.exists(TORQUE_VERSIONS_PATH), f"latcontrol_torque_versions.json not found at {TORQUE_VERSIONS_PATH}"
|
|
|
|
|
|
class TestReleaseBranchGates(OpenpilotTestCase):
|
|
@parameterized.expand(
|
|
[
|
|
"EnableGithubRunner",
|
|
"QuickBootToggle",
|
|
],
|
|
names=["key"],
|
|
)
|
|
def test_sp_dev_items_gate_on_is_sp_release(self, schema, key):
|
|
"""sunnypilot dev items must hide on sunnypilot release branches (is_sp_release gate)."""
|
|
item = _find_item(schema, key)
|
|
assert item is not None, f"{key} not found in schema"
|
|
rules = (item.get("visibility") or []) + (item.get("enablement") or [])
|
|
assert _references_capability_field(rules, "is_sp_release"), f"{key} missing is_sp_release gate"
|
|
|
|
|
|
class TestSpuriousOffroadGatesDropped(OpenpilotTestCase):
|
|
def test_disengage_on_accelerator_has_no_offroad_only(self, schema):
|
|
item = _find_item(schema, "DisengageOnAccelerator")
|
|
assert item is not None
|
|
assert "offroad_only" not in _flatten_rule_types(item.get("enablement"))
|
|
|
|
def test_dynamic_experimental_has_no_offroad_only(self, schema):
|
|
item = _find_item(schema, "DynamicExperimentalControl")
|
|
assert item is not None
|
|
assert "offroad_only" not in _flatten_rule_types(item.get("enablement"))
|
|
|
|
|
|
class TestNotEngagedReplacement(OpenpilotTestCase):
|
|
@parameterized.expand(
|
|
[
|
|
"AlphaLongitudinalEnabled",
|
|
"ToyotaEnforceStockLongitudinal",
|
|
"ToyotaStopAndGoHack",
|
|
],
|
|
names=["key"],
|
|
)
|
|
def test_offroad_only_replaced_with_not_engaged(self, schema, key):
|
|
"""These items should use not_engaged, not offroad_only."""
|
|
item = _find_item(schema, key)
|
|
assert item is not None, f"{key} not found"
|
|
rule_types = _flatten_rule_types(item.get("enablement"))
|
|
assert "offroad_only" not in rule_types, f"{key} still uses offroad_only"
|
|
assert "not_engaged" in rule_types, f"{key} missing not_engaged"
|
|
|
|
|
|
class TestToyotaVirtualCruiseSpeed(OpenpilotTestCase):
|
|
def test_vehicle_toggle_contract(self, schema):
|
|
toyota = schema["vehicle_settings"]["toyota"]
|
|
item = next((item for item in toyota["items"] if item.get("key") == "ToyotaVirtualCruiseSpeed"), None)
|
|
|
|
assert item is not None
|
|
assert item["widget"] == "toggle"
|
|
assert item.get("needs_onroad_cycle") is True
|
|
assert _references_capability_field(item.get("visibility"), "toyota_virtual_cruise_speed_available")
|
|
assert _references_capability_field(item.get("enablement"), "has_longitudinal_control")
|
|
assert "not_engaged" in _flatten_rule_types(item.get("enablement"))
|
|
|
|
def test_custom_acc_section_links_virtual_cruise_opt_in(self, schema):
|
|
section = _find_section(schema, "cruise", "custom_acc_increments")
|
|
assert section is not None
|
|
assert _has_toyota_virtual_cruise_gate(section.get("enablement"))
|
|
|
|
item = _find_item(schema, "CustomAccIncrementsEnabled")
|
|
assert item is not None
|
|
assert _has_toyota_virtual_cruise_gate(item.get("enablement"))
|