Repair hosted display selection and migrated test fixtures

Select the real headless backend for hosted AOL and RELEASE tests. Isolate SpacePilot scripted clocks from unrelated UI readers. Update camera and replay fixtures for existing native APIs, pin the reviewed schema addition, and supply declared CAN batch and throttle inputs.

Preserve production guards, acceptance assertions, selected cases, capability skips and receipt gating. Refresh the canonical protected inventory receipt.
This commit is contained in:
Dom
2026-11-04 00:50:22 -06:00
parent baf883200c
commit 9eab5400a4
10 changed files with 153 additions and 17 deletions
+4
View File
@@ -343,6 +343,8 @@ jobs:
run: scons
- name: Gate every assigned lifecycle module serially
timeout-minutes: 10
env:
RAYLIB_BACKEND: headless
run: python3 tools/ci/run_vehicle_tests.py --suite aol-lifecycle --shard-index "${{ matrix.shard }}" --shard-count 16 --output "ci-results/aol-lifecycle-${{ matrix.shard }}"
- name: Save every lifecycle outcome and native build
if: always()
@@ -453,6 +455,8 @@ jobs:
run: scons
- name: Run every assigned RELEASE target serially
timeout-minutes: 20
env:
RAYLIB_BACKEND: headless
run: python3 tools/ci/run_vehicle_tests.py --suite safety-release --shard-index "${{ matrix.shard }}" --shard-count 4 --output "ci-results/safety-release-${{ matrix.shard }}"
- name: Save every RELEASE outcome and native build
if: always()
@@ -244,14 +244,14 @@ class TestIoniq6PrepublicationHandoff(unittest.TestCase):
cp = CarInterface.get_params(model, fp, [], False, False, False)
ci = CarInterface(cp)
packer = CANPacker(DBC[model][Bus.pt])
ci.update((1_000_000_000, []))
ci.update([(1_000_000_000, [])])
sources = [(parser, state) for parser in ci.can_parsers.values()
for state in parser.message_states.values() if not state.ignore_alive and state.address != 0x2E0]
for frame in range(120):
packets = [packer.make_can_msg(state.name, parser.bus,
{"COUNTER": frame % 256} if any(sig.name == "COUNTER" for sig in state.signals) else {})
for parser, state in sources]
state = ci.update((1_000_000_000 + frame * 100_000_000, packets))
state = ci.update([(1_000_000_000 + frame * 100_000_000, packets)])
self.assertTrue(state.canValid)
self.assertFalse(cp.openpilotLongitudinalControl)
self.assertTrue(cp.pcmCruise)
@@ -261,7 +261,7 @@ class TestIoniq6PrepublicationHandoff(unittest.TestCase):
packets = [packer.make_can_msg(msg.name, parser.bus,
{"COUNTER": frame % 256} if any(sig.name == "COUNTER" for sig in msg.signals) else {})
for parser, msg in sources if msg.name != "MDPS"]
state = ci.update((1_000_000_000 + frame * 100_000_000, packets))
state = ci.update([(1_000_000_000 + frame * 100_000_000, packets)])
self.assertFalse(state.canValid)
def test_card_tags_aol_candidate_before_cp_publication_without_disabling_ecu(self):
@@ -98,6 +98,7 @@ class Plant:
# always start at current speed
model.modelV2.velocity.x = [float(self.speed)] + [float(self.speed + 0.5)] * (len(T_IDXS) - 1)
model.modelV2.acceleration.x = [0.0] * len(T_IDXS)
model.modelV2.meta.disengagePredictions.gasPressProbs = [float(prob_throttle)] * 6
control.controlsState.longControlState = LongCtrlState.pid if self.enabled else LongCtrlState.off
ss.selfdriveState.experimentalMode = self.e2e
@@ -1,6 +1,6 @@
{
"reviewed_schema_commit": "cd0f46b9d65a9c5e47002f57e5ad6625c9757c63",
"reviewed_schema_sha256": "5effa2eb1fbbada3d1bbaae0acd3d31d780d8ec0e6ee1e214fedbc38ae400182",
"reviewed_schema_commit": "63beafc6af294d0759b47dc3bc546f24082a2a5d",
"reviewed_schema_sha256": "ca01ef4f6d2b15c5fac59ce237ec6e49f1167e2408903c9088ee322ca490de31",
"incumbent_fixture_sha256": "a36ab54c00b5bbe61d520873d24aadfb8f33babd70c31a07046516d5545838d9",
"types": {
"0xf35cc4560bbf6ec2": {
@@ -18,7 +18,7 @@
},
"new_storage": {
"dataWordCount": 3,
"pointerCount": 6,
"pointerCount": 7,
"preferredListEncoding": "inlineComposite",
"isGroup": false,
"discriminantCount": 0,
@@ -59,11 +59,31 @@
"hadExplicitDefault": false
},
"name": "toyotaCruise",
"codeOrder": 38,
"codeOrder": 39,
"discriminantValue": 65535,
"ordinal": {
"explicit": 38
}
},
{
"slot": {
"offset": 6,
"type": {
"struct": {
"typeId": 15172299076723568810
}
},
"defaultValue": {
"schemaValueHex": "00000000040000000000000002000100100000000000000000000000000000000000000000000000"
},
"hadExplicitDefault": false
},
"name": "steeringLimitInfo",
"codeOrder": 38,
"discriminantValue": 65535,
"ordinal": {
"explicit": 39
}
}
]
},
@@ -656,7 +676,8 @@
},
"published_addition_commits": {
"slc": "ab8368f287b78b42f8590722878c7d5834609e19",
"flm": "b151ca11cb4d88f32d7adfdbcff50baf29f4fbbc"
"flm": "b151ca11cb4d88f32d7adfdbcff50baf29f4fbbc",
"tesla_cooperative_steering": "63beafc6af294d0759b47dc3bc546f24082a2a5d"
},
"review_basis": "Published SLC and FLM append-only changes. Every incumbent compiled field descriptor and enum prefix was independently checked unchanged; only the exact seven typed deltas below are permitted."
"review_basis": "Published SLC, FLM, and Tesla cooperative-steering additions. Every incumbent wire field descriptor and enum prefix was independently checked unchanged; only the exact typed deltas listed here are permitted. Tesla adds steeringLimitInfo @39 at pointer slot 6 using the existing SteeringLimitInfo type. ToyotaCruise @38 retains pointer slot 5, type, and default; only its declaration codeOrder changes from 38 to 39."
}
@@ -115,6 +115,47 @@ def test_additive_feedback_survives_incumbent_reader_relay(historical):
assert recovered.feedbackPublication.to_dict() == expected_feedback
def test_additive_steering_limits_preserve_defaults_and_survive_incumbent_relay(historical):
original = historical.StarPilotCarState.new_message(
pedalMaxRegen=True, pedalLongActive=True, sourceCarStateMonoTime=123456,
gps={'latitude': 41.25, 'longitude': -87.5, 'hasFix': True})
incumbent = original.to_dict()
with custom.StarPilotCarState.from_bytes(original.to_bytes()) as decoded:
assert not decoded.steeringLimitInfo.valid
assert decoded.steeringLimitInfo.monoTime == 0
current = decoded.as_builder()
current.feedbackPublication = {'version': 1, 'publicationSequence': 27, 'reportedTorque': 0.25}
current.toyotaCruise = {'version': 1, 'status': 'active', 'targetKph': 72., 'sessionId': 'incumbent'}
current.steeringLimitInfo = {
'valid': True, 'modelLimitErrorDeg': 1.25, 'resumeLimitErrorDeg': 2.5,
'cooperativeLimitErrorDeg': 3.75, 'cooperativeOffsetDeg': -0.5,
'monoTime': 123456789, 'combinedLimitErrorDeg': 4.0}
expected = current.to_dict()
with historical.StarPilotCarState.from_bytes(current.to_bytes()) as old:
assert old.to_dict() == incumbent
relayed = old.as_builder().to_bytes()
with custom.StarPilotCarState.from_bytes(relayed) as recovered:
assert recovered.to_dict() == expected
@pytest.mark.parametrize(('before', 'after'), [
('dashboardSpeedLimit @3 :Float32;', 'dashboardSpeedLimit @3 :Float64;'),
('toyotaCruise @38 :ToyotaCruise;', 'toyotaCruise @38 :StarPilotCarControl.SteeringLimitInfo;'),
('steeringLimitInfo @39 :StarPilotCarControl.SteeringLimitInfo;', 'steeringLimitInfo @39 :StarPilotCarState.Gps;'),
('steeringLimitInfo @39 :StarPilotCarControl.SteeringLimitInfo;',
'steeringLimitInfo @39 :StarPilotCarControl.SteeringLimitInfo; unreviewed @40 :Bool;'),
])
def test_reviewed_steering_addition_does_not_allow_other_schema_changes(historical, tmp_path, before, after):
source = Path(CEREAL_PATH, 'custom.capnp').read_text()
assert source.count(before) == 1
path = tmp_path / 'unreviewed.capnp'
path.write_text(source.replace(before, after))
parser = capnp.SchemaParser()
changed = parser.load(str(path), imports=[CEREAL_PATH, str(files('opendbc') / 'car')])
with pytest.raises(AssertionError):
compare_node(historical.StarPilotCarState, changed.StarPilotCarState)
@pytest.mark.parametrize(('fields', 'compatible'), [
('value @0 :UInt32; label @1 :Text;', True),
('value @0 :UInt32; label @1 :Text; extra @2 :Bool;', False),
@@ -574,11 +574,11 @@ def test_published_readiness_survives_buffering_but_rejects_seeks_and_old_epochs
@pytest.mark.parametrize('matching_frame', (False, True))
def test_epoch_reset_preserves_native_camera_stream_and_eligible_paused_frame(monkeypatch, matching_frame):
@pytest.mark.parametrize('streams,expected_streams', (({0, 2}, [0, 2]), ([2, 0], [2, 0])))
def test_epoch_reset_preserves_native_camera_stream_and_eligible_paused_frame(monkeypatch, matching_frame, streams, expected_streams):
monkeypatch.setattr('openpilot.starpilot.ui.runtime_snapshot.RuntimeSnapshotAdapter', Mock())
monkeypatch.setattr('openpilot.starpilot.ui.pip_render.PiPRenderer', Mock())
core: Any = object.__new__(Compositor)
streams = {0, 2}
client = Mock(frame_id=5 if matching_frame else 4, timestamp_sof=9_000_000_000, timestamp_eof=9_010_000_000)
client.is_connected.return_value = True
frame = SimpleNamespace(frame_id=5)
@@ -610,7 +610,7 @@ def test_epoch_reset_preserves_native_camera_stream_and_eligible_paused_frame(mo
callback()
assert core.render_receipts == {}
assert core.camera.client is client
assert core.camera.available_streams == streams
assert core.camera.available_streams == expected_streams
assert core.camera.available_streams is not streams
core.camera._initialize_textures.assert_called_once()
assert core.camera.frame is (frame if matching_frame else None)
@@ -9,6 +9,7 @@ import pytest
from openpilot.selfdrive.ui.mici.layouts import main
from openpilot.selfdrive.ui.mici.onroad import cameraview
from openpilot.selfdrive.ui.onroad import cameraview as base_cameraview
@pytest.fixture
@@ -152,7 +153,8 @@ def camera(monkeypatch):
monkeypatch.setattr(cameraview, 'VisionIpcClient', new_client)
monkeypatch.setattr(cameraview, 'COMMA_HARDWARE', True)
monkeypatch.setattr(cameraview, 'destroy_egl_image', lambda image: events.append(('egl', image)))
monkeypatch.setattr(base_cameraview, 'COMMA_HARDWARE', True)
monkeypatch.setattr(base_cameraview, 'destroy_egl_image', lambda image: events.append(('egl', image)))
monkeypatch.setattr(cameraview.rl, 'unload_texture', lambda texture: events.append(('texture', texture.id)))
monkeypatch.setattr(cameraview.rl, 'unload_shader', lambda shader: events.append(('shader', shader.id)))
monkeypatch.setattr(cameraview.rl, 'get_time', lambda: 1.0)
@@ -1,11 +1,12 @@
"""Cold native entry and preempted clock reads from the recorded d5 exit paths."""
import time
from types import SimpleNamespace as NS
from unittest.mock import Mock
import pytest
from openpilot.starpilot.ui.spacepilot import ignition, native, session
from openpilot.starpilot.ui.spacepilot import guard, ignition, native, session
from openpilot.starpilot.ui.tests.test_spacepilot_session import Clock, Vehicle, game_ui, offroad, setup as setup
from openpilot.starpilot.ui.tests.test_spacepilot_state import mature_gate, read
from openpilot.system.ui.lib.application import gui_app
@@ -20,11 +21,25 @@ def clock_reads(monkeypatch, *, delayed=1, backwards=False, offset=2_000_000_000
boots.append(now + offset)
now += max(delta, 0)
mono = Mock(side_effect=monos)
monkeypatch.setattr(session.time, 'monotonic_ns', mono)
monkeypatch.setattr(session.time, 'clock_gettime_ns', Mock(side_effect=boots))
# Keep background UI readers from consuming the scripted clock pairs.
monkeypatch.setattr(guard, 'time', NS(monotonic_ns=mono, clock_gettime_ns=Mock(side_effect=boots), CLOCK_BOOTTIME=time.CLOCK_BOOTTIME))
return mono
def test_clock_reads_leave_unrelated_process_clocks_untouched(monkeypatch):
process_mono, process_boot = time.monotonic_ns, time.clock_gettime_ns
mono = clock_reads(monkeypatch, delayed=3)
assert time.monotonic_ns is process_mono
assert time.clock_gettime_ns is process_boot
time.monotonic_ns()
time.clock_gettime_ns(time.CLOCK_BOOTTIME)
assert mono.call_count == guard.time.clock_gettime_ns.call_count == 0
with pytest.raises(ValueError, match='stable device clock'):
guard.sample_clocks()
assert mono.call_count == 6
assert guard.time.clock_gettime_ns.call_count == 3
def witness_ui(monkeypatch):
ui = game_ui()
Clock().refresh(ui, 0)
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "b2b3de9483ae6ab96b75644afda03011e55dba114cb05ec6646ff9d85bee14bb", "schema_version": 1}
{"protected_inventory_sha256": "e0d27b06417496a48ff1a04a246384c056d93f34d2f33f31dc026b9010612af2", "schema_version": 1}
@@ -0,0 +1,52 @@
"""Hosted collection uses the real display-free Raylib backend, like unit tests."""
import os
from pathlib import Path
import re
import unittest
from unittest.mock import patch
from tools.ci.vehicle_pytest import serial_environment
ROOT = Path(__file__).resolve().parents[3]
RUNNERS = (
('unit_tests', 'Run unit tests'),
('custom_pytest', 'Test every assigned custom pytest node serially'),
('aol_lifecycles', 'Gate every assigned lifecycle module serially'),
('safety_release_contracts', 'Run every assigned RELEASE target serially'),
)
class TestHeadlessCiEnvironment(unittest.TestCase):
def runner_step(self, workflow, job_name, step_name):
jobs = dict(re.findall(r'^ ([a-z_]+):\n(.*?)(?=^ [a-z_]+:|\Z)', workflow, re.M | re.S))
steps = dict(re.findall(r'^ - name: ([^\n]+)\n(.*?)(?=^ - |\Z)', jobs[job_name], re.M | re.S))
return steps[step_name]
def assert_headless_step(self, step):
self.assertRegex(step, r'(?m)^ env:\n(?: .*\n)*? RAYLIB_BACKEND: headless\n')
def test_hosted_collection_selects_native_headless_backend(self):
workflow = (ROOT / '.github/workflows/tests.yaml').read_text()
for job_name, step_name in RUNNERS:
with self.subTest(job=job_name):
self.assert_headless_step(self.runner_step(workflow, job_name, step_name))
def test_missing_or_desktop_backend_is_rejected(self):
workflow = (ROOT / '.github/workflows/tests.yaml').read_text()
for job_name, step_name in RUNNERS:
step = self.runner_step(workflow, job_name, step_name)
for replacement in ('', ' RAYLIB_BACKEND: desktop\n'):
with self.subTest(job=job_name, replacement=replacement), self.assertRaises(AssertionError):
self.assert_headless_step(step.replace(' RAYLIB_BACKEND: headless\n', replacement))
def test_serial_vehicle_pytest_preserves_backend(self):
with patch.dict(os.environ, {'RAYLIB_BACKEND': 'headless'}):
with serial_environment():
self.assertEqual(os.environ['RAYLIB_BACKEND'], 'headless')
self.assertEqual(os.environ['RAYLIB_BACKEND'], 'headless')
if __name__ == '__main__':
unittest.main()