diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 161adc3965..e8a1f91e8b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -350,7 +350,7 @@ jobs: - name: Build native host dependencies run: scons - name: Gate every assigned lifecycle module serially - timeout-minutes: 10 + timeout-minutes: 15 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 }}" diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_camera_long.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_camera_long.py index 394c9181ac..d89078e3d9 100644 --- a/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_camera_long.py +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_camera_long.py @@ -1,3 +1,4 @@ +import os from tempfile import TemporaryDirectory from types import SimpleNamespace from unittest.mock import Mock, patch @@ -16,7 +17,8 @@ from opendbc.safety.tests.libsafety import libsafety_py from opendbc.safety.tests import test_hyundai_gv60_hda1_long_aol as native_fixture from openpilot.common.params import Params from openpilot.starpilot.car.hyundai.aol import policy_for, native_accepts_cp -from openpilot.selfdrive.ui.layouts.settings import toggles +with patch.dict(os.environ, {"SCALE": "1"}): + from openpilot.selfdrive.ui.layouts.settings import toggles def camera_params(offset=0, alpha=False): diff --git a/openpilot/selfdrive/car/tests/test_conditional_manual_event.py b/openpilot/selfdrive/car/tests/test_conditional_manual_event.py index 2b10f5fb80..864e67a9a7 100644 --- a/openpilot/selfdrive/car/tests/test_conditional_manual_event.py +++ b/openpilot/selfdrive/car/tests/test_conditional_manual_event.py @@ -84,6 +84,7 @@ class CardManualEventTests(unittest.TestCase): def test_traffic_media_uses_only_new_packet_durations_across_empty_card_polls(self): self.cp.carFingerprint = HYUNDAI_CAR.HYUNDAI_IONIQ_6 + self.cp.safetyConfigs = [{'safetyModel': 'hyundaiCanfd'}] self.cp.flags = int(HyundaiFlags.CANFD_LKA_STEER_MSG) self.params.put('ModeButtonControl', 6, block=True) self.params.put('LongModeButtonControl', 6, block=True) @@ -124,6 +125,7 @@ class CardManualEventTests(unittest.TestCase): def test_exact_ioniq_media_assignment_requires_source_and_host_authority(self): self.params.put('ModeButtonControl', 5, block=True) self.cp.carFingerprint = HYUNDAI_CAR.HYUNDAI_IONIQ_6 + self.cp.safetyConfigs = [{'safetyModel': 'hyundaiCanfd'}] self.assertIsNone(self.candidate(media=self.media((False, False, 700_000_000), (True, False, 900_000_000), (False, False, 1_000_000_000)))) # ECAN is 0 in this CP. @@ -157,6 +159,7 @@ class CardManualEventTests(unittest.TestCase): def test_ordinary_buttons_do_not_depend_on_optional_media_cache(self): self.cp.carFingerprint = HYUNDAI_CAR.HYUNDAI_IONIQ_6 + self.cp.safetyConfigs = [{'safetyModel': 'hyundaiCanfd'}] self.cp.flags = int(HyundaiFlags.CANFD_LKA_STEER_MSG) self.params.put('LKASButtonControl', 5, block=True) self.params.put('DistanceButtonControl', 5, block=True) @@ -177,6 +180,7 @@ class CardManualEventTests(unittest.TestCase): def test_ioniq_media_hold_cannot_cross_map_or_drive_change(self): self.cp.carFingerprint = HYUNDAI_CAR.HYUNDAI_IONIQ_6 + self.cp.safetyConfigs = [{'safetyModel': 'hyundaiCanfd'}] self.cp.flags = int(HyundaiFlags.CANFD_LKA_STEER_MSG) self.params.put('ModeButtonControl', 5, block=True) self.candidate(media=self.media((False, False, 1_000_000_000))) diff --git a/openpilot/selfdrive/test/process_replay/refs/starpilot/regenAFB9780D823_2025-04-08--23-00-34--0_card.zst b/openpilot/selfdrive/test/process_replay/refs/starpilot/regenAFB9780D823_2025-04-08--23-00-34--0_card.zst index e437b9cc05..e8844eb926 100644 Binary files a/openpilot/selfdrive/test/process_replay/refs/starpilot/regenAFB9780D823_2025-04-08--23-00-34--0_card.zst and b/openpilot/selfdrive/test/process_replay/refs/starpilot/regenAFB9780D823_2025-04-08--23-00-34--0_card.zst differ diff --git a/openpilot/starpilot/conditional_mode/tests/test_manual_saved_join.py b/openpilot/starpilot/conditional_mode/tests/test_manual_saved_join.py index b18d36a6de..f90cf09f7d 100644 --- a/openpilot/starpilot/conditional_mode/tests/test_manual_saved_join.py +++ b/openpilot/starpilot/conditional_mode/tests/test_manual_saved_join.py @@ -83,6 +83,7 @@ class ManualSavedJoinTests(unittest.TestCase): self.assertEqual(self.owner.manual_state.intent, ManualIntent.NONE) # Honda cannot consume Ioniq media. self.assertEqual(self.owner.card_last_sequence, 0) self.cp.carFingerprint = HYUNDAI_CAR.HYUNDAI_IONIQ_6 + self.cp.safetyConfigs = [{'safetyModel': 'hyundaiCanfd'}] self.cp.flags = int(HyundaiFlags.CANFD_LKA_STEER_MSG) self.sample(stamp + 50_000_000, self.receipt(stamp + 50_000_000, 2, button='mode')) self.assertEqual(self.owner.card_last_sequence, 2) diff --git a/openpilot/starpilot/controllers/tests/test_mode_actions.py b/openpilot/starpilot/controllers/tests/test_mode_actions.py index 58805f581f..05b80e99de 100644 --- a/openpilot/starpilot/controllers/tests/test_mode_actions.py +++ b/openpilot/starpilot/controllers/tests/test_mode_actions.py @@ -261,6 +261,7 @@ def test_actual_card_alpha_off_short_wheel_gesture_has_lateral_only_authority(): f.setUp() try: f.cp.carFingerprint = CAR.HYUNDAI_IONIQ_6 + f.cp.safetyConfigs = [{"safetyModel": "hyundaiCanfd"}] f.cp.flags = int(HyundaiFlags.CANFD_LKA_STEER_MSG) f.cp.openpilotLongitudinalControl = False f.sm['carControl'].enabled = f.sm['carControl'].longActive = False diff --git a/openpilot/starpilot/galaxy/setting_descriptions.py b/openpilot/starpilot/galaxy/setting_descriptions.py index 91d1f106ae..d1ef00b8f9 100644 --- a/openpilot/starpilot/galaxy/setting_descriptions.py +++ b/openpilot/starpilot/galaxy/setting_descriptions.py @@ -7,6 +7,7 @@ from openpilot.starpilot.ui.long_profile_feature import CATEGORY_HELP, VALUE_HEL from openpilot.starpilot.ui.feature_settings_state import SLC_OFFSET_KEYS DESCRIPTIONS = { + 'FordRearFogIndicator': 'Shows a movable amber telltale while the Ford rear fog light is on, using read-only vehicle status.', 'DisengageOnAccelerator': 'When enabled, pressing the accelerator pedal will disengage openpilot.', 'ReduceMotion': 'Reduces interface animations for a steadier display.', 'IsMetric': 'On displays speed and distance in kilometers; Off uses miles. Saved speed-limit offsets keep their physical values.', diff --git a/openpilot/starpilot/galaxy/tests/description-key-inventory.json b/openpilot/starpilot/galaxy/tests/description-key-inventory.json index 83b66a9259..705e038df5 100644 --- a/openpilot/starpilot/galaxy/tests/description-key-inventory.json +++ b/openpilot/starpilot/galaxy/tests/description-key-inventory.json @@ -34,6 +34,7 @@ "FordLowSpeedFactor_ang", "FordPrefLateralControl", "FordPrefSteerAngleCurvature", + "FordRearFogIndicator", "GMAutoHold", "GMLongitudinalTune", "GMPedalLongitudinal", diff --git a/openpilot/starpilot/galaxy/tests/test_settings.mjs b/openpilot/starpilot/galaxy/tests/test_settings.mjs index af973b3a6d..50aac13a87 100644 --- a/openpilot/starpilot/galaxy/tests/test_settings.mjs +++ b/openpilot/starpilot/galaxy/tests/test_settings.mjs @@ -171,7 +171,7 @@ assert.deepEqual(cardsFor(""), [["", [0]], ["StarPort", [2, 3]]], "a heading row assert.deepEqual(cardsFor("h.265"), [["", []], ["StarPort", [3]]], "search keeps a row in its card when the heading is filtered out") assert.deepEqual(cardsFor("voacc"), [["", [0]]], "an emptied card is not shown") -const experimentRows = [{ label: "Stay on Wi-Fi", value: "On", reason: "" }, { label: "Specific car experiments", value: "", reason: "", heading: true, collapsed: true }, +const experimentRows = [{ label: "Stay on Wi-Fi", value: "On", reason: "" }, { key: "heading:experiments", label: "Specific car experiments", value: "", reason: "", heading: true, collapsed: true }, { label: "Try experimental fixes", value: "Off", reason: "" }] const experimentCards = (state) => { const view = { state: { query: "", data: { rows: experimentRows }, ...state }, initialPage: "developer", activeSection: {} } @@ -179,12 +179,12 @@ const experimentCards = (state) => { return SettingsPage.computed.rowCards.call(view).map(({ title, open }) => [title, open]) } assert.deepEqual(experimentCards({ openCards: [] }), [["", true], ["Specific car experiments", false]], "a collapsible group starts closed") -assert.deepEqual(experimentCards({ openCards: ["Specific car experiments"] }), [["", true], ["Specific car experiments", true]]) +assert.deepEqual(experimentCards({ openCards: ["heading:experiments"] }), [["", true], ["Specific car experiments", true]]) assert.deepEqual(experimentCards({ openCards: [], query: "experimental" }).at(-1), ["Specific car experiments", true], "searching opens a matching group") const toggled = { state: { openCards: [] } } -SettingsPage.methods.toggleCard.call(toggled, "Specific car experiments") -assert.deepEqual(toggled.state.openCards, ["Specific car experiments"]) -SettingsPage.methods.toggleCard.call(toggled, "Specific car experiments") +SettingsPage.methods.toggleCard.call(toggled, "heading:experiments") +assert.deepEqual(toggled.state.openCards, ["heading:experiments"]) +SettingsPage.methods.toggleCard.call(toggled, "heading:experiments") assert.deepEqual(toggled.state.openCards, []) const conditional = fixture() diff --git a/openpilot/starpilot/galaxy/tests/test_settings.py b/openpilot/starpilot/galaxy/tests/test_settings.py index 042578a7aa..9c819631d8 100644 --- a/openpilot/starpilot/galaxy/tests/test_settings.py +++ b/openpilot/starpilot/galaxy/tests/test_settings.py @@ -840,7 +840,7 @@ class SettingsGatewayTest(unittest.TestCase): self.assertTrue(self.gateway.confirm(intent["intent"], self.session, self.generation)) self.assertEqual(read_lane_change(self.params).policy.close_gap_seconds, 0.8) self.assertEqual(self.page("lane_change")["rows"][gap]["value"], "0.8") - self.assertEqual(self.params.get("LaneChangePreferences")["version"], 5) + self.assertEqual(self.params.get("LaneChangePreferences")["version"], 6) saved = self.page("lane_change") revoke = self.gateway.preview(saved["view"], close, -1, self.session, self.generation) diff --git a/openpilot/starpilot/speed_limits/tests/test_vision_reader_saturation.py b/openpilot/starpilot/speed_limits/tests/test_vision_reader_saturation.py index 9c94def151..2dfc900176 100644 --- a/openpilot/starpilot/speed_limits/tests/test_vision_reader_saturation.py +++ b/openpilot/starpilot/speed_limits/tests/test_vision_reader_saturation.py @@ -1,4 +1,5 @@ import unittest +from unittest.mock import Mock import numpy as np @@ -24,9 +25,9 @@ class TestReaderSaturation(unittest.TestCase): core.temporal_tracking_enabled = True core.last_classifier_advisory_rejected = False core.last_classifier_reject_dominant = False - core._collect_detector_classifier_proposals = lambda frame, proposal=proposal: [(proposal, 0, (110, 10, 170, 70))] + core._collect_detector_classifier_proposals = Mock(side_effect=lambda frame, proposal=proposal: [(proposal, 0, (110, 10, 170, 70))]) regions = iter(regulatory) - core._is_regulatory_speed_sign = lambda crop, regions=regions: next(regions) + core._is_regulatory_speed_sign = Mock(side_effect=lambda crop, regions=regions: next(regions)) calls = [] def classify(crop, calls=calls, core=core, reads=reads): @@ -36,7 +37,7 @@ class TestReaderSaturation(unittest.TestCase): core.last_classifier_reject_dominant = core.last_classifier_advisory_rejected return reads[index] - core._classify_speed_limit_from_model = classify + core._classify_speed_limit_from_model = Mock(side_effect=classify) result = core._detect_sign_from_detector_classifier(np.zeros((100, 200, 3), dtype=np.uint8)) self.assertEqual(len(calls), expected_calls) if speed is None: diff --git a/openpilot/starpilot/speed_limits/vision/producer.py b/openpilot/starpilot/speed_limits/vision/producer.py index 3aa9ec167a..99acb1a9ad 100644 --- a/openpilot/starpilot/speed_limits/vision/producer.py +++ b/openpilot/starpilot/speed_limits/vision/producer.py @@ -2,6 +2,8 @@ from __future__ import annotations +from functools import partial + import math import os import time @@ -155,8 +157,8 @@ def _send(pm, *, session: str, status: str, stream: str, observed_ns: int = 0, pm.send('slcVisionObservation', msg) -def _publish_ready_stop(pm, sm, params, *, session, stream, is_metric, offset_ns, - source_observed_ns, eof_boot_ns, frame_id, width, height, stops, +def _publish_ready_stop(pm, sm, params, stops, *, session, stream, is_metric, offset_ns, + source_observed_ns, eof_boot_ns, frame_id, width, height, last_result, last_result_session, last_result_observed_ns, last_result_valid_until_ns): sm.update(0) pair = clock_pair_ns() @@ -389,12 +391,12 @@ def main() -> None: # The observation belongs to the original camera frame, not the end of # CPU inference. Completion never restamps an older camera observation. source_observed_ns = eof_boot_ns - offset_ns - core._stop_ready_callback = (lambda stops: _publish_ready_stop( + core._stop_ready_callback = partial(_publish_ready_stop, pm, sm, params, session=session, stream=stream, is_metric=is_metric, offset_ns=offset_ns, source_observed_ns=source_observed_ns, eof_boot_ns=eof_boot_ns, frame_id=frame_id, - width=frame.shape[1], height=frame.shape[0], stops=stops, last_result=last_result, + width=frame.shape[1], height=frame.shape[0], last_result=last_result, last_result_session=last_result_session, last_result_observed_ns=last_result_observed_ns, - last_result_valid_until_ns=last_result_valid_until_ns)) if tasks[1] else None + last_result_valid_until_ns=last_result_valid_until_ns) if tasks[1] else None try: result = core.observe(frame, now=source_observed_ns / 1e9) finally: diff --git a/openpilot/starpilot/speed_limits/vision/tests/test_advisory_rejection.py b/openpilot/starpilot/speed_limits/vision/tests/test_advisory_rejection.py index 1e375a27f3..3f93f1090b 100644 --- a/openpilot/starpilot/speed_limits/vision/tests/test_advisory_rejection.py +++ b/openpilot/starpilot/speed_limits/vision/tests/test_advisory_rejection.py @@ -56,6 +56,7 @@ class TestAdvisoryRejection(unittest.TestCase): core = VisionModelCore.__new__(VisionModelCore) core._collect_detector_classifier_proposals = Mock(return_value=proposals) core._remember_detector_proposal = Mock() + core.last_classifier_reject_dominant = False return core def test_confident_nonregulatory_rejection_stops_only_that_proposal(self): @@ -67,14 +68,15 @@ class TestAdvisoryRejection(unittest.TestCase): nonlocal calls calls += 1 core.last_classifier_advisory_rejected = calls == 2 + core.last_classifier_reject_dominant = core.last_classifier_advisory_rejected return None if calls == 2 else (35 if calls == 1 else 55, .99) with patch.object(core, '_classify_speed_limit_from_model', side_effect=classify), \ patch.object(core, '_is_regulatory_speed_sign', side_effect=lambda _crop: calls >= 2): - result = core._detect_sign_from_detector_classifier(np.zeros((200, 400, 3), np.uint8)) + result = core._detect_sign_from_detector_classifier(np.random.default_rng(0).integers(0, 256, (200, 400, 3), dtype=np.uint8)) assert result is not None self.assertEqual(result.speed_limit_mph, 55) - self.assertEqual(calls, 2 + len(model.DETECTOR_CLASSIFIER_EXPANSIONS)) + self.assertEqual(calls, 4) # Two advisory reads, then the existing two-read strong consensus. def test_white_regulatory_crop_keeps_its_expansion_fallback(self): core = self.detector([(.99, 0, (240, 30, 280, 90))]) @@ -84,11 +86,12 @@ class TestAdvisoryRejection(unittest.TestCase): nonlocal calls calls += 1 core.last_classifier_advisory_rejected = calls == 1 + core.last_classifier_reject_dominant = core.last_classifier_advisory_rejected return None if calls == 1 else (55, .99) with patch.object(core, '_classify_speed_limit_from_model', side_effect=classify), \ patch.object(core, '_is_regulatory_speed_sign', return_value=True): - result = core._detect_sign_from_detector_classifier(np.zeros((200, 400, 3), np.uint8)) + result = core._detect_sign_from_detector_classifier(np.random.default_rng(0).integers(0, 256, (200, 400, 3), dtype=np.uint8)) assert result is not None self.assertEqual(result.speed_limit_mph, 55) self.assertEqual(calls, len(model.DETECTOR_CLASSIFIER_EXPANSIONS)) diff --git a/openpilot/starpilot/speed_limits/vision/tests/test_classifier_reuse.py b/openpilot/starpilot/speed_limits/vision/tests/test_classifier_reuse.py index d6cc6d37f0..c2d4703860 100644 --- a/openpilot/starpilot/speed_limits/vision/tests/test_classifier_reuse.py +++ b/openpilot/starpilot/speed_limits/vision/tests/test_classifier_reuse.py @@ -5,6 +5,7 @@ import inspect import textwrap import types import unittest +from unittest.mock import Mock import numpy as np @@ -42,8 +43,8 @@ def fixture(frame, proposals, mode='counted_reject', uncached=False, after_read= core.speed_enabled = True core.last_classifier_advisory_rejected = False core.last_classifier_reject_dominant = False - core._collect_detector_classifier_proposals = lambda _: proposals - core._is_regulatory_speed_sign = lambda crop: mode == 'counted_reject' or bool(int(crop.sum()) % 3) + core._collect_detector_classifier_proposals = Mock(side_effect=lambda _: proposals) + core._is_regulatory_speed_sign = Mock(side_effect=lambda crop: mode == 'counted_reject' or bool(int(crop.sum()) % 3)) def reader(crop): import hashlib digest = hashlib.sha256(crop.tobytes()).hexdigest() @@ -65,9 +66,9 @@ def fixture(frame, proposals, mode='counted_reject', uncached=False, after_read= if value % 4 == 0: return None return [20, 25, 35, 45][value % 4], .6 + (value % 39) / 100 - core._classify_speed_limit_from_model = reader + core._classify_speed_limit_from_model = Mock(side_effect=reader) if uncached: - core._detect_sign_from_detector_classifier = types.MethodType(uncached_reference(), core) + core._detect_sign_from_detector_classifier = Mock(side_effect=types.MethodType(uncached_reference(), core)) return core @@ -120,7 +121,7 @@ class TestClassifierReuse(unittest.TestCase): 'CLASSIFIER_SHA256': '0' * 64} for key, value in changes.items(): with self.subTest(key=key): - def change(core, count): + def change(core, count, key=key, value=value): if count == 3: setattr(core, key, value) core = fixture(frame, proposals, after_read=change) diff --git a/openpilot/starpilot/speed_limits/vision/tests/test_early_stop_publication.py b/openpilot/starpilot/speed_limits/vision/tests/test_early_stop_publication.py index 154ec275bd..387e8f45ae 100644 --- a/openpilot/starpilot/speed_limits/vision/tests/test_early_stop_publication.py +++ b/openpilot/starpilot/speed_limits/vision/tests/test_early_stop_publication.py @@ -1,5 +1,6 @@ import ast import dataclasses +from functools import partial from pathlib import Path from types import SimpleNamespace import unittest @@ -7,17 +8,20 @@ import unittest P = Path(__file__).resolve().parent SOURCE = Path(__file__).resolve().parents[1] + def function(path, name, namespace): tree = ast.parse(path.read_text()) node = next(n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == name) exec(compile(ast.Module(body=[node], type_ignores=[]), str(path), 'exec'), namespace) return namespace[name] + @dataclasses.dataclass(frozen=True) class Box: confidence: float bbox: tuple + @dataclasses.dataclass(frozen=True) class Evidence: session: str @@ -29,74 +33,142 @@ class Evidence: height: int boxes: tuple + class Tests(unittest.TestCase): def setUp(self): self.sent = [] self.clock = [200, 1200] self.health = True - self.ns = dict(clock_pair_ns=lambda: tuple(self.clock), MAX_PAIR_SKEW_NS=10, - MAX_RESULT_AGE_NS=1000, STOP_MAX_AGE_NS=500, _driving_model_ready=lambda sm, now: self.health, - StopEvidence=Evidence, StopBox=Box, - _send=lambda pm, **kw: self.sent.append(kw)) - self.publish = function(SOURCE/'producer.py', '_publish_ready_stop', self.ns) - self.args = dict(session='s', stream='road', is_metric=False, offset_ns=1000, - source_observed_ns=100, eof_boot_ns=1100, frame_id=7, width=100, height=50, - stops=(Box(.8, (1,2,3,4)),), last_result=(40,.99,3,1), - last_result_session='s', last_result_observed_ns=80, last_result_valid_until_ns=800) + self.ns = { + 'clock_pair_ns': lambda: tuple(self.clock), + 'MAX_PAIR_SKEW_NS': 10, + 'MAX_RESULT_AGE_NS': 1000, + 'STOP_MAX_AGE_NS': 500, + '_driving_model_ready': lambda sm, now: self.health, + 'StopEvidence': Evidence, + 'StopBox': Box, + '_send': lambda pm, **kw: self.sent.append(kw), + } + self.publish = function(SOURCE / 'producer.py', '_publish_ready_stop', self.ns) + self.args = { + 'session': 's', + 'stream': 'road', + 'is_metric': False, + 'offset_ns': 1000, + 'source_observed_ns': 100, + 'eof_boot_ns': 1100, + 'frame_id': 7, + 'width': 100, + 'height': 50, + 'stops': (Box(0.8, (1, 2, 3, 4)),), + 'last_result': (40, 0.99, 3, 1), + 'last_result_session': 's', + 'last_result_observed_ns': 80, + 'last_result_valid_until_ns': 800, + } self.sm = SimpleNamespace(update=lambda timeout: None) self.params = SimpleNamespace(get_bool=lambda key: False) def test_callback_after_all_regions_before_reader_with_readonly_tuple(self): - events=[] + events = [] + class Base: def _collect_detector_classifier_proposals(self, frame): events.extend(['region1', 'region2']) - self.stop_detections=(Box(.8,(1,2,3,4)),) + self.stop_detections = (Box(0.8, (1, 2, 3, 4)),) return [('unchanged',)] - tree=ast.parse((SOURCE/'traffic_signs.py').read_text()) - cls=next(n for n in tree.body if isinstance(n,ast.ClassDef) and n.name=='TrafficSignModelCore') - method=next(n for n in cls.body if isinstance(n,ast.FunctionDef) and n.name=='_collect_detector_classifier_proposals') - trial=ast.ClassDef(name='Trial',bases=[ast.Name(id='Base',ctx=ast.Load())],keywords=[],body=[method],decorator_list=[]) - ns={'Base':Base};exec(compile(ast.fix_missing_locations(ast.Module(body=[trial],type_ignores=[])),'hook','exec'),ns) - core=ns['Trial']();core.net=object();received=[] - core._stop_ready_callback=lambda stops:(events.append('publish'),received.append(stops)) - result=core._collect_detector_classifier_proposals(None);events.append('reader') - self.assertEqual(events,['region1','region2','publish','reader']) - self.assertEqual(result,[('unchanged',)]) - self.assertIsInstance(received[0],tuple) - with self.assertRaises(dataclasses.FrozenInstanceError):received[0][0].confidence=1 + + tree = ast.parse((SOURCE / 'traffic_signs.py').read_text()) + cls = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == 'TrafficSignModelCore') + method = next(n for n in cls.body if isinstance(n, ast.FunctionDef) and n.name == '_collect_detector_classifier_proposals') + trial = ast.ClassDef(name='Trial', bases=[ast.Name(id='Base', ctx=ast.Load())], keywords=[], body=[method], decorator_list=[], type_params=[]) + ns = {'Base': Base} + exec(compile(ast.fix_missing_locations(ast.Module(body=[trial], type_ignores=[])), 'hook', 'exec'), ns) + core = ns['Trial']() + core.net = object() + received = [] + core._stop_ready_callback = lambda stops: (events.append('publish'), received.append(stops)) + result = core._collect_detector_classifier_proposals(None) + events.append('reader') + self.assertEqual(events, ['region1', 'region2', 'publish', 'reader']) + self.assertEqual(result, [('unchanged',)]) + self.assertIsInstance(received[0], tuple) + with self.assertRaises(dataclasses.FrozenInstanceError): + received[0][0].confidence = 1 def test_speed_support_unchanged_and_stop_source_not_restamped(self): - self.publish(None,self.sm,self.params,**self.args) - row=self.sent.pop();self.assertEqual(row['result'],self.args['last_result']) - self.assertEqual(row['observed_ns'],80);self.assertEqual(row['valid_until_ns'],800) - self.assertEqual(row['stop_sample'].observed_ns,100);self.assertEqual(row['stop_sample'].frame_id,7) - self.assertEqual(row['stop_sample'].boxes,self.args['stops']) - self.args['last_result_valid_until_ns']=150 - self.publish(None,self.sm,self.params,**self.args) - self.assertIsNone(self.sent[-1]['result']);self.assertEqual(self.sent[-1]['observed_ns'],0) + self.publish(None, self.sm, self.params, **self.args) + row = self.sent.pop() + self.assertEqual(row['result'], self.args['last_result']) + self.assertEqual(row['observed_ns'], 80) + self.assertEqual(row['valid_until_ns'], 800) + self.assertEqual(row['stop_sample'].observed_ns, 100) + self.assertEqual(row['stop_sample'].frame_id, 7) + self.assertEqual(row['stop_sample'].boxes, self.args['stops']) + self.args['last_result_valid_until_ns'] = 150 + self.publish(None, self.sm, self.params, **self.args) + self.assertIsNone(self.sent[-1]['result']) + self.assertEqual(self.sent[-1]['observed_ns'], 0) def test_late_clock_and_health_reject_without_send(self): - self.clock[:]=[200,1300];self.publish(None,self.sm,self.params,**self.args) - self.assertEqual(self.sent,[]) - self.clock[:]=[2000,3000];self.publish(None,self.sm,self.params,**self.args) - self.assertEqual(self.sent,[]) - for pair in ([600,1600], [700,1700]): - self.clock[:]=pair;self.publish(None,self.sm,self.params,**self.args) - self.assertEqual(self.sent,[]) - self.clock[:]=[200,1200];self.health=False;self.publish(None,self.sm,self.params,**self.args) - self.assertEqual(self.sent,[]) + self.clock[:] = [200, 1300] + self.publish(None, self.sm, self.params, **self.args) + self.assertEqual(self.sent, []) + self.clock[:] = [2000, 3000] + self.publish(None, self.sm, self.params, **self.args) + self.assertEqual(self.sent, []) + for pair in ([600, 1600], [700, 1700]): + self.clock[:] = pair + self.publish(None, self.sm, self.params, **self.args) + self.assertEqual(self.sent, []) + self.clock[:] = [200, 1200] + self.health = False + self.publish(None, self.sm, self.params, **self.args) + self.assertEqual(self.sent, []) + + def test_actual_callback_binds_frame_inputs_and_receives_stop_tuple(self): + tree = ast.parse((SOURCE / 'producer.py').read_text()) + node = next(n for n in ast.walk(tree) if isinstance(n, ast.Assign) and + any(isinstance(v, ast.Attribute) and v.attr == '_stop_ready_callback' for v in n.targets) and + isinstance(n.value, ast.IfExp)) + core = SimpleNamespace() + namespace = dict(self.args, core=core, partial=partial, _publish_ready_stop=self.publish, + pm=None, sm=self.sm, params=self.params, tasks=(True, True), frame=SimpleNamespace(shape=(50, 100))) + exec(compile(ast.Module(body=[node], type_ignores=[]), 'actual_callback', 'exec'), namespace) + namespace.update(session='later-frame', frame_id=99, frame=SimpleNamespace(shape=(1, 1)), last_result=None) + core._stop_ready_callback(self.args['stops']) + row = self.sent.pop() + self.assertEqual(row['stop_sample'].session, 's') + self.assertEqual(row['stop_sample'].frame_id, 7) + self.assertEqual((row['stop_sample'].width, row['stop_sample'].height), (100, 50)) + self.assertEqual(row['stop_sample'].boxes, self.args['stops']) + self.assertEqual(row['result'], self.args['last_result']) def test_reader_failure_clears_actual_producer_callback_finally(self): - class ReaderFailure(Exception):pass + class ReaderFailure(Exception): + pass + class Core: - _stop_ready_callback=lambda self,stops: None - def observe(self,*a,**kw):raise ReaderFailure() - tree=ast.parse((SOURCE/'producer.py').read_text()) - node=next(n for n in ast.walk(tree)if isinstance(n,ast.Try) and n.finalbody and - any(isinstance(x,ast.Assign) and any(isinstance(v,ast.Attribute) and v.attr=='_stop_ready_callback' for v in x.targets)for x in n.finalbody)) - core=Core();ns={'core':core,'frame':None,'source_observed_ns':100} - with self.assertRaises(ReaderFailure):exec(compile(ast.Module(body=[node],type_ignores=[]),'actual_finally','exec'),ns) + def _stop_ready_callback(self, stops): + pass + + def observe(self, *a, **kw): + raise ReaderFailure() + + tree = ast.parse((SOURCE / 'producer.py').read_text()) + node = next( + n + for n in ast.walk(tree) + if isinstance(n, ast.Try) + and n.finalbody + and any(isinstance(x, ast.Assign) and any(isinstance(v, ast.Attribute) and v.attr == '_stop_ready_callback' for v in x.targets) for x in n.finalbody) + ) + core = Core() + ns = {'core': core, 'frame': None, 'source_observed_ns': 100} + with self.assertRaises(ReaderFailure): + exec(compile(ast.Module(body=[node], type_ignores=[]), 'actual_finally', 'exec'), ns) self.assertIsNone(core._stop_ready_callback) -if __name__ == '__main__': unittest.main() + +if __name__ == '__main__': + unittest.main() diff --git a/openpilot/starpilot/speed_limits/vision/tests/test_producer.py b/openpilot/starpilot/speed_limits/vision/tests/test_producer.py index b27cd71795..98e2db2356 100644 --- a/openpilot/starpilot/speed_limits/vision/tests/test_producer.py +++ b/openpilot/starpilot/speed_limits/vision/tests/test_producer.py @@ -692,6 +692,7 @@ class TestVisionProducer(unittest.TestCase): messages.append(messaging.log_from_bytes(event.to_bytes()).slcVisionObservation.vision) clocks = [(1_000_000_000, 5_000_000_000), (1_000_000_000, 5_000_000_000), + (1_010_000_000, 5_010_000_000), (1_010_000_000, 5_010_000_000), (1_010_000_000, 5_010_000_000)] from openpilot.starpilot.speed_limits.vision.stop_signs import CapturedStopDetectorOutputs with patch.object(core, '_detector_for_mode', return_value=CapturedStopDetectorOutputs(projection_fixture.Net('stop'))), \ diff --git a/openpilot/starpilot/speed_limits/vision/tests/test_retry_consistency.py b/openpilot/starpilot/speed_limits/vision/tests/test_retry_consistency.py index 8448e5748e..528794f38d 100644 --- a/openpilot/starpilot/speed_limits/vision/tests/test_retry_consistency.py +++ b/openpilot/starpilot/speed_limits/vision/tests/test_retry_consistency.py @@ -11,7 +11,7 @@ from openpilot.starpilot.speed_limits.vision.tests import _identity_fixture as i from openpilot.starpilot.speed_limits.vision.traffic_signs import TrafficSignModelCore -def probabilities(value=None, confidence=.8): +def probabilities(value=None, confidence=0.8): scores = np.full(19, (1 - confidence) / 18, dtype=np.float32) scores[18 if value is None else model.US_CLASSIFIER_SPEED_VALUES.index(value)] = confidence return scores @@ -44,7 +44,7 @@ def core_fixture(reads, regulatory, proposals=None, cls=model.VisionModelCore): core.classifier_logits_layer = 'original_logits' core.classifier_input_size = 128 core.last_classifier_forward_count = 0 - core.last_classifier_forward_duration_s = 0. + core.last_classifier_forward_duration_s = 0.0 core.last_classifier_advisory_rejected = False core.last_classifier_reject_dominant = False core.temporal_tracking_enabled = False @@ -53,7 +53,7 @@ def core_fixture(reads, regulatory, proposals=None, cls=model.VisionModelCore): core.stop_detections = () core._stop_face_filter = speed_face.StopFaceVerifier(identity_fixture.Net("aux")) core._is_regulatory_speed_sign = lambda crop: regulatory[core.classifier_net.calls] - core._collect_detector_classifier_proposals = lambda frame: proposals or [(.9, 0, (40, 20, 100, 100))] + core._collect_detector_classifier_proposals = lambda frame: proposals or [(0.9, 0, (40, 20, 100, 100))] core._iter_school_zone_read_crops = lambda crop: [] return core @@ -65,14 +65,14 @@ class RetryConsistencyTests(unittest.TestCase): self.addCleanup(self.no_model_loads.stop) self.frame = np.zeros((200, 200, 3), dtype=np.uint8) self.reject = probabilities() - self.numeric = probabilities(25, .75) + self.numeric = probabilities(25, 0.75) def pair(self, scores): return scores, scores def test_conflicting_single_context_read_stops_at_original_short_circuit(self): reads = [self.pair(self.reject), self.pair(self.reject), self.pair(self.numeric)] - proposals = [(.9, 0, (40, 20, 100, 100)), (.8, 0, (105, 20, 165, 100))] + proposals = [(0.9, 0, (40, 20, 100, 100)), (0.8, 0, (105, 20, 165, 100))] core = core_fixture(reads, [False, True, True], proposals) self.assertIsNone(core.infer(self.frame)) self.assertEqual(core.classifier_net.calls, 3) @@ -98,18 +98,18 @@ class RetryConsistencyTests(unittest.TestCase): self.assertEqual(core.infer(self.frame).speed_limit_mph, 25) def test_school_fallback_is_unchanged(self): - core = core_fixture([self.pair(self.reject), self.pair(self.reject), self.pair(self.numeric)], - [False, True, True], [(.9, 2, (40, 20, 100, 100))]) + core = core_fixture([self.pair(self.reject), self.pair(self.reject), self.pair(self.numeric)], [False, True, True], [(0.9, 2, (40, 20, 100, 100))]) self.assertEqual(core.infer(self.frame).speed_limit_mph, 25) self.assertEqual(core.classifier_net.calls, 3) def test_new_winner_does_not_inherit_previous_proposal_conflict(self): - low = probabilities(25, .6001) - high = probabilities(25, .9) + low = probabilities(25, 0.6001) + high = probabilities(25, 0.9) reads = [self.pair(self.reject), self.pair(self.reject), self.pair(low)] + [self.pair(high)] * 3 - proposals = [(.9, 0, (40, 20, 100, 100)), (.8, 0, (105, 20, 165, 100))] + proposals = [(0.9, 0, (40, 20, 100, 100)), (0.8, 0, (105, 20, 165, 100))] core = core_fixture(reads, [False, True, True, True, True, True], proposals) - self.assertEqual(core.infer(self.frame).speed_limit_mph, 25) + frame = np.random.default_rng(0).integers(0, 256, self.frame.shape, dtype=np.uint8) + self.assertEqual(core.infer(frame).speed_limit_mph, 25) self.assertEqual(core.classifier_net.calls, 6) def test_missing_original_logits_does_not_invent_primary_reject_evidence(self): @@ -153,7 +153,11 @@ class RetryConsistencyTests(unittest.TestCase): self.assertFalse(core.last_classifier_reject_dominant) core.last_classifier_reject_dominant = True from openpilot.starpilot.speed_limits.vision.stop_signs import CapturedStopDetectorOutputs - with patch.object(core, '_detector_for_mode', return_value=CapturedStopDetectorOutputs(identity_fixture.Net('stop'))), patch.object(core, '_verified_model_path', return_value=Path('unused')): + + with ( + patch.object(core, '_detector_for_mode', return_value=CapturedStopDetectorOutputs(identity_fixture.Net('stop'))), + patch.object(core, '_verified_model_path', return_value=Path('unused')), + ): core.set_speed_enabled(False) self.assertFalse(core.last_classifier_reject_dominant) self.assertIsNone(core.classifier_net) @@ -161,10 +165,12 @@ class RetryConsistencyTests(unittest.TestCase): self.assertEqual(core.last_classifier_forward_count, 0) def test_stop_only_constructor_has_no_numeric_evidence_or_numeric_load(self): - with patch.object(model.VisionModelCore, '_load_net', return_value=identity_fixture.Net('stop')) as load, \ - patch.object(TrafficSignModelCore, '_verified_model_path', return_value=Path('unused')), \ - patch('openpilot.starpilot.speed_limits.vision.traffic_signs.SpeedFaceVerifier', side_effect=lambda _: identity_fixture.fake_identity()), \ - patch.object(TrafficSignModelCore, '_validate_model_abi'): + with ( + patch.object(model.VisionModelCore, '_load_net', return_value=identity_fixture.Net('stop')) as load, + patch.object(TrafficSignModelCore, '_verified_model_path', return_value=Path('unused')), + patch('openpilot.starpilot.speed_limits.vision.traffic_signs.SpeedFaceVerifier', side_effect=lambda _: identity_fixture.fake_identity()), + patch.object(TrafficSignModelCore, '_validate_model_abi'), + ): core = TrafficSignModelCore(is_metric=False, speed_enabled=False) self.assertFalse(core.last_classifier_reject_dominant) self.assertIsNone(core.classifier_net) diff --git a/openpilot/starpilot/speed_limits/vision/tests/test_speed_face_native_binding.py b/openpilot/starpilot/speed_limits/vision/tests/test_speed_face_native_binding.py index 3783996ed4..2047cd8e20 100644 --- a/openpilot/starpilot/speed_limits/vision/tests/test_speed_face_native_binding.py +++ b/openpilot/starpilot/speed_limits/vision/tests/test_speed_face_native_binding.py @@ -1,31 +1,94 @@ """Fresh typed synthetic envelopes: new full asset identity and independent STOP evidence.""" + from types import SimpleNamespace as NS import unittest import hashlib from openpilot.cereal import messaging -from openpilot.starpilot.speed_limits.vision import observation,producer,stop_observation +from openpilot.starpilot.speed_limits.vision import observation, producer, stop_observation from openpilot.starpilot.speed_limits.vision.traffic_signs import TrafficSignModelCore -from openpilot.starpilot.speed_limits.vision.speed_face import bundle_model_id,MODEL_SHA256 -BASE=100_000_000_000 -OFFSET=2_000_000_000 -OLD='52bb30f73bf0d166342f33d1b32b29b726bf036004e0359fef10ef7bf1f5556f:5d861067ab8f50986488f983cbc24f78527775d37af0b9740c5545d5f3173902' -class Input(dict): - def __init__(self,event,now): - super().__init__(slcVisionObservation=event.slcVisionObservation);self.seen={'slcVisionObservation':True};self.valid=self.seen.copy();self.alive=self.seen.copy();self.logMonoTime={'slcVisionObservation':int(event.logMonoTime)};self.recv_time={'slcVisionObservation':now/1e9};self.recv_frame={'slcVisionObservation':2} -def envelope(model_id=None): - now=BASE+400_000_000;sample=stop_observation.StopEvidence('fresh_synthetic_camera','road',BASE,BASE+OFFSET,7,1344,760,(stop_observation.StopBox(.875,(780,200,840,260)),));captured=[] - def send(service,event): - assert service=='slcVisionObservation';event.logMonoTime=now - if model_id is not None:event.slcVisionObservation.vision.modelId=model_id - captured.append(messaging.log_from_bytes(event.to_bytes())) - producer._send(NS(send=send),session=sample.session,status='unknown',stream=sample.stream,observed_ns=BASE,eof_boot_ns=BASE+OFFSET,frame_id=7,result=None,stop_sample=sample) - return captured[0],sample,now -class TestSpeedFaceNativeBinding(unittest.TestCase): - def test_full_three_asset_identity_wire_and_None_speed_valid_STOP(self): - assets=(TrafficSignModelCore.DETECTOR_SHA256,TrafficSignModelCore.CLASSIFIER_SHA256,MODEL_SHA256,TrafficSignModelCore.STOP_PROJECTION_SHA256);expected=hashlib.sha256(('vision_joint_stop_projection_identity_v1\n'+'\n'.join(assets)+'\n').encode()).hexdigest()+':'+TrafficSignModelCore.CLASSIFIER_SHA256;self.assertEqual(tuple(TrafficSignModelCore.ASSET_MANIFEST),assets);self.assertEqual(observation.MODEL_ID,TrafficSignModelCore.MODEL_ID);self.assertEqual(expected,observation.MODEL_ID);self.assertNotEqual(expected,OLD) - event,sample,now=envelope();self.assertEqual(str(event.slcVisionObservation.vision.modelId),expected);self.assertEqual(len(expected),129);self.assertEqual(event.slcVisionObservation.vision.supportCount,0);decoded=stop_observation.read_stop_evidence(Input(event,now),now_ns=now,now_boot_ns=now+OFFSET,drive_id=BASE-1_000_000_000);self.assertEqual(decoded,sample);speed=observation.vision_observation(event.slcVisionObservation.vision,receipt_mono_ns=now,now_mono_ns=now,now_boot_ns=now+OFFSET,car_fingerprint='synthetic',drive_session='synthetic');self.assertEqual(speed.kind,observation.acc.ObservationKind.UNKNOWN) - def test_old_two_asset_identity_rejected_by_actual_typed_consumers(self): - event,_,now=envelope(OLD);self.assertIsNone(stop_observation.read_stop_evidence(Input(event,now),now_ns=now,now_boot_ns=now+OFFSET,drive_id=BASE-1_000_000_000));speed=observation.vision_observation(event.slcVisionObservation.vision,receipt_mono_ns=now,now_mono_ns=now,now_boot_ns=now+OFFSET,car_fingerprint='synthetic',drive_session='synthetic');self.assertEqual(speed.kind,observation.acc.ObservationKind.UNKNOWN) +from openpilot.starpilot.speed_limits.vision.speed_face import bundle_model_id, MODEL_SHA256 - def test_previous_three_asset_identity_rejected_by_actual_typed_consumers(self): - previous=bundle_model_id(TrafficSignModelCore.DETECTOR_SHA256,TrafficSignModelCore.CLASSIFIER_SHA256);self.assertNotEqual(previous,observation.MODEL_ID);event,_,now=envelope(previous);self.assertIsNone(stop_observation.read_stop_evidence(Input(event,now),now_ns=now,now_boot_ns=now+OFFSET,drive_id=BASE-1_000_000_000));speed=observation.vision_observation(event.slcVisionObservation.vision,receipt_mono_ns=now,now_mono_ns=now,now_boot_ns=now+OFFSET,car_fingerprint='synthetic',drive_session='synthetic');self.assertEqual(speed.kind,observation.acc.ObservationKind.UNKNOWN) +BASE = 100_000_000_000 +OFFSET = 2_000_000_000 +OLD = '52bb30f73bf0d166342f33d1b32b29b726bf036004e0359fef10ef7bf1f5556f:5d861067ab8f50986488f983cbc24f78527775d37af0b9740c5545d5f3173902' + + +class Input(dict): + def __init__(self, event, now): + super().__init__(slcVisionObservation=event.slcVisionObservation) + self.seen = {'slcVisionObservation': True} + self.valid = self.seen.copy() + self.alive = self.seen.copy() + self.logMonoTime = {'slcVisionObservation': int(event.logMonoTime)} + self.recv_time = {'slcVisionObservation': now / 1e9} + self.recv_frame = {'slcVisionObservation': 2} + + +def envelope(model_id=None): + now = BASE + 400_000_000 + sample = stop_observation.StopEvidence( + 'fresh_synthetic_camera', 'road', BASE, BASE + OFFSET, 7, 1344, 760, (stop_observation.StopBox(0.875, (780, 200, 840, 260)),) + ) + captured = [] + + def send(service, event): + assert service == 'slcVisionObservation' + event.logMonoTime = now + if model_id is not None: + event.slcVisionObservation.vision.modelId = model_id + captured.append(messaging.log_from_bytes(event.to_bytes())) + + producer._send( + NS(send=send), + session=sample.session, + status='unknown', + stream=sample.stream, + observed_ns=BASE, + eof_boot_ns=BASE + OFFSET, + frame_id=7, + result=None, + stop_sample=sample, + ) + return captured[0], sample, now + + +class TestSpeedFaceNativeBinding(unittest.TestCase): + def test_full_three_asset_identity_wire_and_None_speed_valid_STOP(self): + assets = (TrafficSignModelCore.DETECTOR_SHA256, TrafficSignModelCore.CLASSIFIER_SHA256, MODEL_SHA256, TrafficSignModelCore.STOP_PROJECTION_SHA256) + expected = ( + hashlib.sha256(('vision_joint_stop_projection_identity_v1\n' + '\n'.join(assets) + '\n').encode()).hexdigest() + + ':' + + TrafficSignModelCore.CLASSIFIER_SHA256 + ) + self.assertEqual(tuple(TrafficSignModelCore.ASSET_MANIFEST), assets) + self.assertEqual(observation.MODEL_ID, TrafficSignModelCore.MODEL_ID) + self.assertEqual(expected, observation.MODEL_ID) + self.assertNotEqual(expected, OLD) + event, sample, now = envelope() + self.assertEqual(str(event.slcVisionObservation.vision.modelId), expected) + self.assertEqual(len(expected), 129) + self.assertEqual(event.slcVisionObservation.vision.supportCount, 0) + decoded = stop_observation.read_stop_evidence(Input(event, now), now_ns=now, now_boot_ns=now + OFFSET, drive_id=BASE - 1_000_000_000) + self.assertEqual(decoded, sample) + speed = observation.vision_observation( + event.slcVisionObservation.vision, receipt_mono_ns=now, now_mono_ns=now, now_boot_ns=now + OFFSET, car_fingerprint='synthetic', drive_session='synthetic' + ) + self.assertEqual(speed.kind, observation.acc.ObservationKind.UNKNOWN) + + def test_old_two_asset_identity_rejected_by_actual_typed_consumers(self): + event, _, now = envelope(OLD) + self.assertIsNone(stop_observation.read_stop_evidence(Input(event, now), now_ns=now, now_boot_ns=now + OFFSET, drive_id=BASE - 1_000_000_000)) + speed = observation.vision_observation( + event.slcVisionObservation.vision, receipt_mono_ns=now, now_mono_ns=now, now_boot_ns=now + OFFSET, car_fingerprint='synthetic', drive_session='synthetic' + ) + self.assertEqual(speed.kind, observation.acc.ObservationKind.UNKNOWN) + + def test_previous_three_asset_identity_rejected_by_actual_typed_consumers(self): + previous = bundle_model_id(TrafficSignModelCore.DETECTOR_SHA256, TrafficSignModelCore.CLASSIFIER_SHA256) + self.assertNotEqual(previous, observation.MODEL_ID) + event, _, now = envelope(previous) + self.assertIsNone(stop_observation.read_stop_evidence(Input(event, now), now_ns=now, now_boot_ns=now + OFFSET, drive_id=BASE - 1_000_000_000)) + speed = observation.vision_observation( + event.slcVisionObservation.vision, receipt_mono_ns=now, now_mono_ns=now, now_boot_ns=now + OFFSET, car_fingerprint='synthetic', drive_session='synthetic' + ) + self.assertEqual(speed.kind, observation.acc.ObservationKind.UNKNOWN) diff --git a/openpilot/starpilot/speed_limits/vision/tests/test_stop_projection.py b/openpilot/starpilot/speed_limits/vision/tests/test_stop_projection.py index 325daaf121..a2156bd477 100644 --- a/openpilot/starpilot/speed_limits/vision/tests/test_stop_projection.py +++ b/openpilot/starpilot/speed_limits/vision/tests/test_stop_projection.py @@ -27,10 +27,17 @@ class TestStopProjection(unittest.TestCase): owner = self class Identity: - def __init__(self, _): self.net = object() - def begin_frame(self): pass - def end_frame(self): pass - def close(self): self.net = None + def __init__(self, _): + self.net = object() + + def begin_frame(self): + pass + + def end_frame(self): + pass + + def close(self): + self.net = None def load(cls, name, expected): owner.loads.append(name) @@ -48,22 +55,24 @@ class TestStopProjection(unittest.TestCase): wrapped.setInput(np.zeros((1, 3, 256, 256), np.float32)) self.assertEqual(wrapped.forward_stops().shape, (1, 5, 4096)) self.assertEqual(raw.calls, [['stop/output0']]) - with self.assertRaises(ValueError): wrapped.forward() + with self.assertRaises(ValueError): + wrapped.forward() def test_stop_output_rejects_invalid_shape_dtype_or_nonfinite_values(self): - values = [np.zeros((1, 5, 1344), np.float32), np.zeros((1, 5, 4096), np.float64), - np.full((1, 5, 4096), np.nan, np.float32)] + values = [np.zeros((1, 5, 1344), np.float32), np.zeros((1, 5, 4096), np.float64), np.full((1, 5, 4096), np.nan, np.float32)] for value in values: with self.subTest(shape=value.shape, dtype=value.dtype): - wrapped = CapturedStopDetectorOutputs(SimpleNamespace(forward=lambda _: [value])) + wrapped = CapturedStopDetectorOutputs(SimpleNamespace(forward=lambda _, value=value: [value])) wrapped.stops = 'previous' - with self.assertRaises(ValueError): wrapped.forward_stops() + with self.assertRaises(ValueError): + wrapped.forward_stops() self.assertIsNone(wrapped.stops) def test_stop_output_requires_exactly_one_result(self): for outputs in ([], [None, None], None): wrapped = CapturedStopDetectorOutputs(SimpleNamespace(forward=lambda _, value=outputs: value)) - with self.assertRaises(ValueError): wrapped.forward_stops() + with self.assertRaises(ValueError): + wrapped.forward_stops() def test_stop_capture_is_owned_and_cleared_by_new_input(self): wrapped = CapturedStopDetectorOutputs(self.raw_net('traffic_signs_us_stop_detector.onnx')) @@ -108,7 +117,8 @@ class TestStopProjection(unittest.TestCase): original = core.net core.stop_detections = ('previous',) with patch.object(core, '_validate_candidate', side_effect=ValueError('Invalid candidate')): - with self.assertRaises(ValueError): core.set_speed_enabled(False) + with self.assertRaises(ValueError): + core.set_speed_enabled(False) self.assertIs(core.net, original) self.assertTrue(core.speed_enabled) self.assertEqual(core.stop_detections, ()) @@ -119,6 +129,7 @@ class TestStopProjection(unittest.TestCase): self.assertEqual(core.MODEL_ID, observation.MODEL_ID) with patch.object(core, '_verified_model_path') as verify, patch.object(core, '_detector_for_mode'): core._load_detector_net() - self.assertEqual([call.args[0] for call in verify.call_args_list], - ['traffic_signs_us_detector.onnx', 'traffic_signs_us_stop_detector.onnx', 'traffic_sign_identity.onnx']) + self.assertEqual( + [call.args[0] for call in verify.call_args_list], ['traffic_signs_us_detector.onnx', 'traffic_signs_us_stop_detector.onnx', 'traffic_sign_identity.onnx'] + ) self.assertEqual(verify.call_args_list[1].args[1], core.STOP_PROJECTION_SHA256) diff --git a/openpilot/starpilot/speed_limits/vision/tests/test_stop_signs.py b/openpilot/starpilot/speed_limits/vision/tests/test_stop_signs.py index 5f3a648001..92160da9a2 100644 --- a/openpilot/starpilot/speed_limits/vision/tests/test_stop_signs.py +++ b/openpilot/starpilot/speed_limits/vision/tests/test_stop_signs.py @@ -13,14 +13,25 @@ from openpilot.starpilot.speed_limits.vision.tests import _identity_fixture as i class TestStopSigns(unittest.TestCase): def decode(self, raw): - return decode_stop_signs(raw, ratio=1, pad_width=0, pad_height=0, region_width=256, region_height=256, - origin_x=100, origin_y=20, full_frame_width=600, full_frame_height=400, threshold=.5) + return decode_stop_signs( + raw, + ratio=1, + pad_width=0, + pad_height=0, + region_width=256, + region_height=256, + origin_x=100, + origin_y=20, + full_frame_width=600, + full_frame_height=400, + threshold=0.5, + ) def test_stop_output_is_independent_of_speed_and_uses_one_forward(self): speed = np.zeros((1, 5, 1344), np.float32) - speed[0, 4, 0] = .3 + speed[0, 4, 0] = 0.3 stops = np.zeros((1, 5, 4096), np.float32) - stops[0, :, 0] = [128, 128, 40, 40, .99] + stops[0, :, 0] = [128, 128, 40, 40, 0.99] calls = [] net = SignDetectorOutputs(SimpleNamespace(forward=lambda names: calls.append(names) or (speed, stops), setInput=lambda _: None)) self.assertIs(net.forward(), speed) @@ -41,7 +52,7 @@ class TestStopSigns(unittest.TestCase): def test_stop_work_is_bounded_before_nms(self): raw = np.zeros((1, 5, 4096), np.float32) raw[0, :4] = np.array([128, 128, 20, 20])[:, None] - raw[0, 4] = np.linspace(.6, .99, 4096) + raw[0, 4] = np.linspace(0.6, 0.99, 4096) with patch('openpilot.starpilot.speed_limits.vision.stop_signs.cv2.dnn.NMSBoxes', return_value=[]) as nms: self.decode(raw) self.assertEqual(len(nms.call_args.args[0]), 32) @@ -52,17 +63,17 @@ class TestStopSigns(unittest.TestCase): core.history = deque() core.net = SimpleNamespace(rgb256=None) core._stop_face_filter = StopFaceVerifier(identity_fixture.Net('aux')) - core.stop_detections = (StopDetection(.9, (10, 10, 30, 30)),) + core.stop_detections = (StopDetection(0.9, (10, 10, 30, 30)),) core.reset() self.assertEqual(core.stop_detections, ()) - core.stop_detections = (StopDetection(.9, (10, 10, 30, 30)),) + core.stop_detections = (StopDetection(0.9, (10, 10, 30, 30)),) with self.assertRaises(ValueError): core.infer(np.zeros((0, 0, 3), np.uint8)) self.assertEqual(core.stop_detections, ()) def test_sign_api_returns_both_outputs_without_an_extra_inference(self): core = VisionModelCore.__new__(VisionModelCore) - stop = StopDetection(.9, (100, 20, 140, 60)) + stop = StopDetection(0.9, (100, 20, 140, 60)) core.stop_detections = (stop,) with patch.object(core, 'infer', return_value=None) as infer: result = core.infer_signs(np.zeros((256, 256, 3), np.uint8)) @@ -72,6 +83,7 @@ class TestStopSigns(unittest.TestCase): def test_shipped_joint_model_runs_and_preserves_speed_output(self): from openpilot.starpilot.speed_limits.vision.model import VisionModelCore as SpeedCore + frame = np.zeros((760, 1344, 3), np.uint8) joint = VisionModelCore(is_metric=False) speed = SpeedCore(is_metric=False) @@ -83,6 +95,7 @@ class TestStopSigns(unittest.TestCase): def test_stop_only_does_not_load_or_run_the_numeric_classifier(self): from openpilot.starpilot.speed_limits.vision import model as vision_model + calls = [] class StopNet(identity_fixture.Net): @@ -97,10 +110,17 @@ class TestStopSigns(unittest.TestCase): self.assertEqual(expected, VisionModelCore.STOP_PROJECTION_SHA256) return StopNet('joint') - with patch.object(vision_model.VisionModelCore, '_load_net', classmethod(checked)), patch.object(VisionModelCore, '_verified_model_path'), patch('openpilot.starpilot.speed_limits.vision.traffic_signs.SpeedFaceVerifier', side_effect=lambda _: identity_fixture.fake_identity()): + with ( + patch.object(vision_model.VisionModelCore, '_load_net', classmethod(checked)), + patch.object(VisionModelCore, '_verified_model_path'), + patch('openpilot.starpilot.speed_limits.vision.traffic_signs.SpeedFaceVerifier', side_effect=lambda _: identity_fixture.fake_identity()), + ): core = VisionModelCore(is_metric=False, speed_enabled=False) frame = np.zeros((760, 1344, 3), np.uint8) - with patch.object(core, '_classify_speed_limit_from_model', side_effect=AssertionError('Unused speed classifier')), patch.object(core._stop_face_filter.net, 'forward', side_effect=AssertionError('Unexpected identity work')): + with ( + patch.object(core, '_classify_speed_limit_from_model', side_effect=AssertionError('Unused speed classifier')), + patch.object(core._stop_face_filter.net, 'forward', side_effect=AssertionError('Unexpected identity work')), + ): self.assertIsNone(core.observe(frame, now=1.0)) self.assertIsNone(core.classifier_net) self.assertIsNone(core._speed_face_filter) diff --git a/openpilot/starpilot/speed_limits/vision/traffic_signs.py b/openpilot/starpilot/speed_limits/vision/traffic_signs.py index d8c122fa98..ea7bdf32a0 100644 --- a/openpilot/starpilot/speed_limits/vision/traffic_signs.py +++ b/openpilot/starpilot/speed_limits/vision/traffic_signs.py @@ -21,7 +21,8 @@ class TrafficSignModelCore(model.VisionModelCore): DETECTOR_SHA256 = '52bb30f73bf0d166342f33d1b32b29b726bf036004e0359fef10ef7bf1f5556f' STOP_PROJECTION_SHA256 = "a744df1b585bfd80a2a983229509f52bf2f09714d9df011109ad1b8b875aaecc" ASSET_MANIFEST = (DETECTOR_SHA256, model.VisionModelCore.CLASSIFIER_SHA256, MODEL_SHA256, STOP_PROJECTION_SHA256) - MODEL_ID = hashlib.sha256(("vision_joint_stop_projection_identity_v1\n" + "\n".join(ASSET_MANIFEST) + "\n").encode()).hexdigest() + ":" + model.VisionModelCore.CLASSIFIER_SHA256 + MODEL_ID = (hashlib.sha256(("vision_joint_stop_projection_identity_v1\n" + "\n".join(ASSET_MANIFEST) + "\n").encode()).hexdigest() + + ":" + model.VisionModelCore.CLASSIFIER_SHA256) STOP_THRESHOLD = .6 _speed_face_filter: SpeedFaceVerifier | None = None _stop_face_filter: StopFaceVerifier | None = None diff --git a/openpilot/starpilot/ui/tests/test_widget_layers.py b/openpilot/starpilot/ui/tests/test_widget_layers.py index 07659c19fd..616d7e92e4 100644 --- a/openpilot/starpilot/ui/tests/test_widget_layers.py +++ b/openpilot/starpilot/ui/tests/test_widget_layers.py @@ -112,6 +112,7 @@ def test_renderer_dispatches_each_widget_in_saved_order(profile, viewport): method = next(node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef) and node.name == '_ordered_widgets') namespace: dict[str, Any] = {'CLOCK_WIDGET': CLOCK_WIDGET, 'MODE_WIDGET': MODE_WIDGET, 'placement': placement, 'widget_order': widget_order, 'RAIL_WIDGETS': RAIL_WIDGETS, 'STOPPED_TIMER_WIDGET': STOPPED_TIMER_WIDGET, + 'render_rear_fog': lambda *args: seen.append('rear_fog'), 'manual_slc_confirmation': lambda profile, state: False, 'AlertSize': NS(NONE='none', FULL='full'), 'CameraViewChoice': NS(NONE='none'), 'rl': NS(Rectangle=lambda x, y, width, height: NS(x=x, y=y, width=width, height=height))} exec(compile(ast.Module(body=[method], type_ignores=[]), str(source), 'exec'), namespace) diff --git a/prebuilt.json b/prebuilt.json index ced1774d7d..9b42998386 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "6d06f8093d88df04b4b2966c886c25ad3d978572ae815af3755706aa735463fd", "schema_version": 1} +{"protected_inventory_sha256": "e6d1299870cec364a7d5da259b7665b071fd7c089642009bdb44273397964f27", "schema_version": 1}