6e2ccc8b15
version: sunnypilot v2026.002.000 (staging) date: 2026-05-27T04:05:25 master commit: dfc3c98b226da57a653daf57131a8a3d66166fcb
220 lines
7.8 KiB
Python
220 lines
7.8 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
|
|
|
|
import pytest
|
|
|
|
from openpilot.sunnypilot.sunnylink.tools.generate_settings_schema import (
|
|
DEFINITION_PATH,
|
|
TORQUE_VERSIONS_PATH,
|
|
_build_torque_options,
|
|
_load_torque_versions,
|
|
generate_schema,
|
|
)
|
|
|
|
|
|
SCHEMA_VALIDATOR_PATH = os.path.join(os.path.dirname(DEFINITION_PATH), "settings_ui.schema.json")
|
|
|
|
|
|
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
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def schema():
|
|
return generate_schema()
|
|
|
|
|
|
class TestMadsBrandGates:
|
|
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:
|
|
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:
|
|
def test_validator_accepts_real_json(self):
|
|
"""settings_ui.json validates against settings_ui.schema.json."""
|
|
jsonschema = pytest.importorskip("jsonschema")
|
|
with open(DEFINITION_PATH) as f:
|
|
data = json.load(f)
|
|
with open(SCHEMA_VALIDATOR_PATH) as f:
|
|
validator = json.load(f)
|
|
jsonschema.validate(instance=data, schema=validator)
|
|
|
|
|
|
class TestTorqueOptionGeneration:
|
|
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:
|
|
@pytest.mark.parametrize("key", [
|
|
"EnableGithubRunner",
|
|
"QuickBootToggle",
|
|
])
|
|
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:
|
|
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:
|
|
@pytest.mark.parametrize("key", [
|
|
"AlphaLongitudinalEnabled",
|
|
"ToyotaEnforceStockLongitudinal",
|
|
"ToyotaStopAndGoHack",
|
|
])
|
|
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"
|