Files
StarPilot/tools/ci/tests/test_schema_policy.py
T
2026-10-11 01:03:39 -06:00

274 lines
16 KiB
Python

"""Compatibility regressions the reserved-event policy must catch in CI."""
import copy
from pathlib import Path
from tempfile import TemporaryDirectory
import unittest
from tools.ci.schema_policy import read_inputs, validate
class SchemaPolicyTest(unittest.TestCase):
def setUp(self):
self.schemas, self.policy, self.sync = read_inputs()
def test_current_fork_uses_only_reserved_schema_slots(self):
self.assertEqual(validate(self.schemas, self.policy, self.sync), [])
def test_adding_a_car_state_field_is_rejected(self):
name = 'opendbc_repo/opendbc/car/car.capnp'
self.schemas[name] = self.schemas[name].replace(b'struct CarState {', b'struct CarState {\n forkValue @62 :Float32;')
self.assertTrue(any('car.capnp' in error for error in validate(self.schemas, self.policy, self.sync)))
def test_other_opendbc_schemas_are_pinned(self):
for name in ('opendbc_repo/opendbc/car/rlog.capnp', 'opendbc_repo/opendbc/car/include/c++.capnp'):
with self.subTest(name=name):
schemas = self.schemas.copy()
schemas[name] += b'\n# unexpected upstream schema edit\n'
self.assertTrue(any(name in error for error in validate(schemas, self.policy, self.sync)))
def test_new_schema_under_either_owned_root_requires_policy(self):
for name in ('openpilot/cereal/new.capnp', 'opendbc_repo/opendbc/car/new.capnp'):
with self.subTest(name=name):
schemas = self.schemas.copy()
schemas[name] = b'@0xdeadbeefdeadbeef;\n'
self.assertTrue(any(name in error for error in validate(schemas, self.policy, self.sync)))
def test_input_discovery_includes_new_schema(self):
with TemporaryDirectory() as directory:
root = Path(directory)
for name, data in self.schemas.items():
path = root / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(data)
(root / 'upstream-sync.json').write_text('{"dependencies": []}')
new_schema = root / 'opendbc_repo/opendbc/car/new.capnp'
new_schema.write_bytes(b'@0xdeadbeefdeadbeef;\n')
discovered, _, _ = read_inputs(root)
self.assertIn('opendbc_repo/opendbc/car/new.capnp', discovered)
def test_reserved_custom_extension_and_alias_are_allowed(self):
# This source-policy test permits extension/aliasing; the separate compiled
# contract protects the existing message's API and field layout.
original = b'struct AolAxisState @0xfc6241ed8877b611 {'
self.assertIn(original, self.schemas['openpilot/cereal/custom.capnp'])
self.assertIn(b'aolAxisState @142 :Custom.AolAxisState;', self.schemas['openpilot/cereal/log.capnp'])
self.schemas['openpilot/cereal/custom.capnp'] = self.schemas['openpilot/cereal/custom.capnp'].replace(
original, b'struct FixtureState @0xfc6241ed8877b611 {\n note @12 :Text = "{fixture}";')
self.schemas['openpilot/cereal/log.capnp'] = self.schemas['openpilot/cereal/log.capnp'].replace(
b'aolAxisState @142 :Custom.AolAxisState;', b'fixtureState @142 :Custom.FixtureState;')
self.assertEqual(validate(self.schemas, self.policy, self.sync), [])
def test_restored_map_event_cannot_be_retargeted(self):
self.assertFalse({17, 18, 19}.intersection(self.policy['historical_empty_slots']))
schemas = self.schemas.copy()
schemas['openpilot/cereal/log.capnp'] = schemas['openpilot/cereal/log.capnp'].replace(
b'mapdExtendedOut @143 :Custom.MapdExtendedOut;', b'mapdExtendedOut @143 :Custom.MapdIn;')
self.assertTrue(any('event @143' in error for error in validate(schemas, self.policy, self.sync)))
def test_aol_raw_slots_remain_data_with_original_ordinals(self):
for old, new in ((b'aolSafetyWire @124 :Data;', b'aolSafetyWire @124 :Custom.AolAxisState;'),
(b'aolIntentWire @125 :Data;', b'aolIntentWire @126 :Data;')):
with self.subTest(old=old):
schemas = self.schemas.copy()
self.assertIn(old, schemas['openpilot/cereal/log.capnp'])
schemas['openpilot/cereal/log.capnp'] = schemas['openpilot/cereal/log.capnp'].replace(old, new)
self.assertTrue(validate(schemas, self.policy, self.sync))
def test_historical_slot_rename_and_field_are_rejected(self):
path = 'openpilot/cereal/custom.capnp'
reserved_by_name = {slot['struct']: slot for slot in self.policy['reserved']}
for index in self.policy['historical_empty_slots']:
name = f'CustomReserved{index}'
type_id = reserved_by_name[name]['type_id']
original = f'struct {name} @{type_id} {{'.encode()
self.assertIn(original, self.schemas[path])
for replacement in (f'struct DifferentEvent @{type_id} {{'.encode(), original + b'\n newValue @0 :UInt32;'):
with self.subTest(index=index, replacement=replacement):
schemas = self.schemas.copy()
schemas[path] = schemas[path].replace(original, replacement)
self.assertTrue(any(f'historical slot {index}' in error for error in validate(schemas, self.policy, self.sync)))
def test_upstream_plan_and_event_enums_cannot_be_extended_or_remapped(self):
path = 'openpilot/cereal/log.capnp'
self.assertIn(b'accelBoost @40 :Float32;', self.schemas[path])
self.assertNotIn(b'forceStopHolding', self.schemas[path])
self.assertNotIn(b'teslaCCEngaged', self.schemas[path])
mutations = (
(b'accelBoost @40 :Float32;', b'accelBoost @41 :Float32;'),
(b'accelBoost @40 :Float32;', b'accelBoost @40 :Float32;\n forceStopHolding @41 :Bool;'),
(b'enum EventName @0x91f1992a1f77fb03 {', b'enum EventName @0x91f1992a1f77fb03 {\n teslaCCEngaged @105;'),
)
for original, replacement in mutations:
with self.subTest(replacement=replacement):
schemas = self.schemas.copy()
self.assertIn(original, schemas[path])
schemas[path] = schemas[path].replace(original, replacement)
self.assertTrue(validate(schemas, self.policy, self.sync))
policy = copy.deepcopy(self.policy)
policy['reviewed_log_additions'] = [{'scope': 'struct LongitudinalPlan', 'fields': ['forceStopHolding @41 :Bool;']}]
self.assertTrue(validate(schemas, policy, self.sync))
def test_historical_slot_comments_do_not_count_as_fields(self):
path = 'openpilot/cereal/custom.capnp'
schemas = self.schemas.copy()
original = b'struct CustomReserved5 @0xa5cd762cd951a455 {'
self.assertIn(original, schemas[path])
schemas[path] = schemas[path].replace(
original, original + b'\n # Past wire use is reserved.\n')
self.assertEqual(validate(schemas, self.policy, self.sync), [])
def test_automatic_learning_keeps_reserved_identity_and_allows_append(self):
self.assertNotIn(1, self.policy['historical_empty_slots'])
slot = self.policy['reserved'][1]
self.assertEqual(slot, {'struct': 'CustomReserved1', 'type_id': '0xaedffd8f31e7b55d',
'field': 'customReserved1', 'ordinal': 108})
prefix = self.policy['historical_prefixes']['1']
self.assertEqual((prefix['name'], prefix['type_id']), ('StarPilotCarParams', slot['type_id']))
self.assertIn(b'struct StarPilotCarParams @0xaedffd8f31e7b55d {', self.schemas['openpilot/cereal/custom.capnp'])
self.assertIn(b'autoSteeringLearning @108 :Custom.StarPilotCarParams;', self.schemas['openpilot/cereal/log.capnp'])
self.assertIn(b'learning @9 :AutoSteeringLearning;', self.schemas['openpilot/cereal/custom.capnp'])
self.assertIn(b'pcmCruiseSpeed @7 :Bool = true;', self.schemas['openpilot/cereal/custom.capnp'])
schemas = self.schemas.copy()
original = b' controllerRevision @11 :Text;\n }\n }\n}'
self.assertIn(original, schemas['openpilot/cereal/custom.capnp'])
schemas['openpilot/cereal/custom.capnp'] = schemas['openpilot/cereal/custom.capnp'].replace(
original, b' controllerRevision @11 :Text;\n }\n }\n futureStatus @10 :UInt32;\n}')
self.assertEqual(validate(schemas, self.policy, self.sync), [])
def test_automatic_learning_payload_and_event_cannot_be_reinterpreted(self):
path = 'openpilot/cereal/custom.capnp'
for before, after in (
(b'struct StarPilotCarParams @0xaedffd8f31e7b55d {', b'struct UnrelatedFit @0xaedffd8f31e7b55d {'),
(b'0xaedffd8f31e7b55d', b'0xaedffd8f31e7b55e'),
(b'alternativeExperience @0 :Int16;', b'alternativeExperience @0 :UInt16;'),
(b'canUsePedal @1 :Bool;', b'canUsePedal @2 :Bool;'),
(b'flags @3 :UInt32;', b'flags @3 :Bool;'),
(b'pcmCruiseSpeed @7 :Bool = true;', b'pcmCruiseSpeed @7 :Bool = false;'),
(b'redneckCruiseAvailable @8 :Bool;', b''),
(b'safetyParam @0 :UInt16;', b'safetyParam @0 :UInt32;'),
(b'learning @9 :AutoSteeringLearning;', b'learning @10 :AutoSteeringLearning;'),
(b'version @0 :UInt16;', b'version @0 :UInt32;'),
(b'latAccelFactor @11 :Float64;', b'latAccelFactor @11 :Float32;'),
(b'latAccelFactor @11 :Float64;', b'unrelatedFactor @11 :Float64;'),
(b'heldoutCandidateLoss @18 :Float64;', b'heldoutCandidateLoss @19 :Float64;'),
(b'acceptancePolicyId @12 :Text;', b''),
(b'qualifiedFit @4;', b'qualifiedFit @5;'),
(b'linearFactor @1;', b'linearFactor @2;'),
(b'vehicle @0 :Text;', b'vehicle @0 :UInt64;'),
(b'controllerRevision @11 :Text;', b'controllerRevision @11 :Text = "changed";'),
):
with self.subTest(after=after):
schemas = self.schemas.copy()
head, marker, tail = schemas[path].partition(b'struct StarPilotCarParams @0xaedffd8f31e7b55d {')
body, following, remainder = tail.partition(b'\nstruct StarPilotCarState')
self.assertTrue(marker and following)
occupied = marker + body
self.assertIn(before, occupied)
schemas[path] = head + occupied.replace(before, after) + following + remainder
self.assertTrue(validate(schemas, self.policy, self.sync))
path = 'openpilot/cereal/log.capnp'
original = b'autoSteeringLearning @108 :Custom.StarPilotCarParams;'
for replacement in (b'autoSteeringLearning @109 :Custom.StarPilotCarParams;',
b'autoSteeringLearning @108 :Custom.CustomReserved5;'):
with self.subTest(replacement=replacement):
schemas = self.schemas.copy()
self.assertIn(original, schemas[path])
schemas[path] = schemas[path].replace(original, replacement)
self.assertTrue(any('event @108' in error for error in validate(schemas, self.policy, self.sync)))
def test_restored_lateral_field_keeps_original_wire_meaning(self):
self.assertNotIn(10, self.policy['historical_empty_slots'])
path = 'openpilot/cereal/custom.capnp'
original = b'desiredCurvature @0 :Float32;'
self.assertIn(original, self.schemas[path])
for changed in (b'desiredCurvature @0 :UInt32;', b'desiredCurvature @2 :Float32;'):
with self.subTest(changed=changed):
schemas = self.schemas.copy()
schemas[path] = schemas[path].replace(original, changed)
self.assertTrue(any('historical slot 10' in error for error in validate(schemas, self.policy, self.sync)))
def test_custom_id_reuse_event_retargeting_and_core_edits_are_rejected(self):
cases = (
('openpilot/cereal/custom.capnp', b'0xa4f1eb3323f5f582', b'0xc86a3d38d13eb3ef'),
('openpilot/cereal/log.capnp', b'@145 :Custom.MapdOut', b'@145 :Custom.MapdIn'),
('openpilot/cereal/log.capnp', b'@145 :Custom.MapdOut', b'@146 :Custom.MapdOut'),
('openpilot/cereal/log.capnp', b'struct Event {', b'struct Event {\n forkValue @200 :UInt32;'),
)
for path, before, after in cases:
with self.subTest(after=after):
source = copy.copy(self.schemas)
self.assertIn(before, source[path])
source[path] = source[path].replace(before, after)
self.assertTrue(validate(source, self.policy, self.sync))
def test_new_upstream_pin_requires_schema_contract_review(self):
self.sync['upstream']['commit'] = '0' * 40
self.assertTrue(validate(self.schemas, self.policy, self.sync))
def test_reviewed_native_enum_is_only_permitted_core_extension(self):
path = 'opendbc_repo/opendbc/car/car.capnp'
self.assertEqual(self.policy['reviewed_enum_additions'], {path: {'enum': 'SafetyModel', 'field': 'teslaPreap @39;'}})
for before, after in ((b'teslaPreap @39;', b'teslaPreap @40;'),
(b'teslaPreap @39;', b'teslaPreap @39;\n extraFork @40;'),
(b'teslaPreap @39;', b'teslaPreap @39;\n extraBool @62 :Bool;')):
changed = copy.copy(self.schemas)
self.assertIn(before, changed[path])
changed[path] = changed[path].replace(before, after)
self.assertTrue(validate(changed, self.policy, self.sync))
def test_navigation_keeps_full_historical_control_prefix(self):
path = 'openpilot/cereal/custom.capnp'
for before, after in ((b'hudControl @0 :HUDControl;', b'hudControl @0 :Text;'),
(b'cooperativeOffsetDeg @4 :Float32;', b'cooperativeOffsetDeg @4 :Bool;'),
(b'uwu @22;', b'uwu @23;')):
changed = copy.copy(self.schemas)
self.assertIn(before, changed[path])
changed[path] = changed[path].replace(before, after)
self.assertTrue(validate(changed, self.policy, self.sync))
def test_longitudinal_companion_keeps_wire_prefix_and_allows_append(self):
path = 'openpilot/cereal/custom.capnp'
original = b'forceStopHolding @4 :Bool;'
self.assertIn(original, self.schemas[path])
appended = self.schemas.copy()
appended[path] = appended[path].replace(original, original + b'\n futureStatus @5 :UInt32;')
self.assertEqual(validate(appended, self.policy, self.sync), [])
for replacement in (b'', b'forceStopHolding @5 :Bool;', b'forceStopHolding @4 :UInt32;',
b'changedHold @4 :Bool;'):
with self.subTest(replacement=replacement):
changed = self.schemas.copy()
changed[path] = changed[path].replace(original, replacement)
self.assertTrue(any('occupied prefix changed' in error for error in validate(changed, self.policy, self.sync)))
def test_replay_slc_prefix_preserves_fields_and_allows_append(self):
self.assertNotIn(6, self.policy['historical_empty_slots'])
path = 'openpilot/cereal/custom.capnp'
original = b'slcSpeedLimitSource @26 :Text;'
self.assertIn(original, self.schemas[path])
appended = self.schemas.copy()
appended[path] = appended[path].replace(original, original + b'\n futureStatus @27 :UInt32;')
self.assertEqual(validate(appended, self.policy, self.sync), [])
for before, after in ((b'accelerationJerk @0 :Float32;', b'accelerationJerk @0 :UInt32;'),
(b'starpilotEvents @10 :AnyPointer;', b'starpilotEvents @10 :Text;'),
(b'slcSpeedLimit @24 :Float32;', b'slcSpeedLimit @25 :Float32;'),
(original, b''),
(b'struct CustomReserved6 @0xf98d843bfd7004a3 {',
b'struct DifferentEvent @0xf98d843bfd7004a3 {')):
with self.subTest(after=after):
changed = self.schemas.copy()
self.assertIn(before, changed[path])
changed[path] = changed[path].replace(before, after)
self.assertTrue(any('occupied prefix changed' in error for error in validate(changed, self.policy, self.sync)))
def test_replay_slc_event_keeps_original_type_id_and_ordinal(self):
path = 'openpilot/cereal/log.capnp'
original = b'customReserved6 @113 :Custom.CustomReserved6;'
self.assertIn(original, self.schemas[path])
for after in (b'customReserved6 @113 :Custom.CustomReserved5;',
b'customReserved6 @114 :Custom.CustomReserved6;'):
with self.subTest(after=after):
changed = self.schemas.copy()
changed[path] = changed[path].replace(original, after)
self.assertTrue(validate(changed, self.policy, self.sync))