diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 170c3d5057..91069f1073 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -258,7 +258,7 @@ jobs: strategy: fail-fast: false matrix: - shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23] + shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31] steps: - uses: actions/checkout@v7 - name: Provide JavaScript runtime for Galaxy contract tests @@ -291,7 +291,7 @@ jobs: timeout-minutes: 15 env: RAYLIB_BACKEND: headless - run: .venv/bin/python -m tools.ci.custom_pytest_shards run --index "${{ matrix.shard }}" --count 24 --output "ci-results/custom-pytest-${{ matrix.shard }}" + run: .venv/bin/python -m tools.ci.custom_pytest_shards run --index "${{ matrix.shard }}" --count 32 --output "ci-results/custom-pytest-${{ matrix.shard }}" - name: Save collection, every outcome, JUnit and durable worker diagnostics if: always() uses: actions/upload-artifact@v4 @@ -314,7 +314,7 @@ jobs: pattern: custom-pytest-* path: ci-results/custom-pytest-shards - name: Require every case passed or audited capability-unavailable - run: python3 -m tools.ci.custom_pytest_shards aggregate --directory ci-results/custom-pytest-shards --count 24 --output ci-results/custom-pytest-complete.json + run: python3 -m tools.ci.custom_pytest_shards aggregate --directory ci-results/custom-pytest-shards --count 32 --output ci-results/custom-pytest-complete.json - name: Save complete custom catalog verdict if: always() uses: actions/upload-artifact@v4 diff --git a/opendbc_repo/opendbc/car/ford/tests/test_cluster_scope.py b/opendbc_repo/opendbc/car/ford/tests/test_cluster_scope.py index b97804af47..d1202b1e0c 100644 --- a/opendbc_repo/opendbc/car/ford/tests/test_cluster_scope.py +++ b/opendbc_repo/opendbc/car/ford/tests/test_cluster_scope.py @@ -35,10 +35,12 @@ def test_all_dom_profiles_have_shared_setting_and_correct_active_status(identity assert qualified(controller.CP) owner = FeatureSettingsOwner(saved, lambda group: True, vehicle_params=lambda: controller.CP, vehicle_fingerprint=lambda: str(identity)) - row = next(r for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows if r.key == KEY) + row = next(r for r in owner.snapshot('torque/bluepilot', parked=True, system_long=True, lateral_context=True, metric=False).rows if r.key == KEY) assert row.available and row.value == 'Off' if enabled: - assert owner.apply(row_change(row, 1)) + request = row_change(row, 1) + assert request is not None + assert owner.apply(request) configure_controller(SimpleNamespace(CP=controller.CP, CC=controller), saved) assert controller.hands_free_cluster_input.enabled == enabled command = structs.CarControl() @@ -69,7 +71,7 @@ def test_non_canfd_fords_do_not_show_cluster_setting(identity): assert controller.hands_free_cluster_input is None owner = FeatureSettingsOwner(saved, lambda group: True, vehicle_params=lambda: controller.CP, vehicle_fingerprint=lambda: str(identity)) - assert KEY not in {r.key for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows} + assert KEY not in {r.key for r in owner.snapshot('torque/bluepilot', parked=True, system_long=True, lateral_context=True, metric=False).rows} def test_invalid_profile_does_not_enable_display_preference(): @@ -77,7 +79,7 @@ def test_invalid_profile_does_not_enable_display_preference(): saved = Params() Path(saved.get_param_path(KEY)).write_bytes(b'1') controller, _ = setup() - controller.CP.safetyConfigs[0].safetyParam = 0 + controller.CP.passive = True assert not qualified(controller.CP) configure_controller(SimpleNamespace(CP=controller.CP, CC=controller), saved) assert controller.hands_free_cluster_input is None diff --git a/opendbc_repo/opendbc/car/ford/tests/test_mache_cluster_parity.py b/opendbc_repo/opendbc/car/ford/tests/test_mache_cluster_parity.py index eae001d935..b722b8d919 100644 --- a/opendbc_repo/opendbc/car/ford/tests/test_mache_cluster_parity.py +++ b/opendbc_repo/opendbc/car/ford/tests/test_mache_cluster_parity.py @@ -98,7 +98,7 @@ def test_shared_feature_ui_saves_opt_in_and_rejects_onroad_stale_or_wrong_vehicl owner = FeatureSettingsOwner(saved, lambda group: parked[0], vehicle_params=lambda: controller.CP, vehicle_fingerprint=lambda: str(controller.CP.carFingerprint)) - row = next(r for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows if r.key == KEY) + row = next(r for r in owner.snapshot('torque/bluepilot', parked=True, system_long=True, lateral_context=True, metric=False).rows if r.key == KEY) assert row.value == 'Off' and row.available request = row_change(row, 1) assert request is not None @@ -109,5 +109,5 @@ def test_shared_feature_ui_saves_opt_in_and_rejects_onroad_stale_or_wrong_vehicl assert owner.apply(request) assert read_choice(saved)[0] == 1 assert not owner.apply(request) - row = next(r for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows if r.key == KEY) + row = next(r for r in owner.snapshot('torque/bluepilot', parked=True, system_long=True, lateral_context=True, metric=False).rows if r.key == KEY) assert row.value == 'On' diff --git a/opendbc_repo/opendbc/car/mazda/tests/test_native_engagement_callers.py b/opendbc_repo/opendbc/car/mazda/tests/test_native_engagement_callers.py index cf0bc645d7..1d71ec4419 100644 --- a/opendbc_repo/opendbc/car/mazda/tests/test_native_engagement_callers.py +++ b/opendbc_repo/opendbc/car/mazda/tests/test_native_engagement_callers.py @@ -29,7 +29,6 @@ def test_longitudinal_cancel_assignment_keeps_lateral_actual_callers(identity, m _exercise_actual_family_callers(identity, monkeypatch, scenario='long_cancel', cancel_action=4) - @pytest.fixture(scope='module') def armed_intent_decoder(tmp_path_factory): import capnproto @@ -117,10 +116,12 @@ def _exercise_actual_family_callers(identity, monkeypatch, *, scenario='brake', assert card.aol_card_intent.settings == sd.aol_settings soft_warning = [False] original_events = sd.update_events + def warning_events(*args, **kwargs): original_events(*args, **kwargs) if soft_warning[0]: from openpilot.selfdrive.selfdrived.events import EventName + assert isinstance(warning, str) sd.events.add(getattr(EventName, warning)) local_patch.setattr(sd, 'update_events', warning_events) assert sd.CP.to_dict() == card.CP.to_dict() == ci.CP.to_dict() == controls.CP.to_dict() @@ -320,6 +321,8 @@ def _exercise_actual_family_callers(identity, monkeypatch, *, scenario='brake', assert card.aol_card_intent.allowed_latch and axis.lateralActive and not axis.longitudinalActive if scenario == 'warning': from openpilot.selfdrive.selfdrived.events import EventName + assert isinstance(warning, str) + lateral_expected = warning == 'radarTempUnavailable' for _ in range(30): state, axis, host_state, command = step(brake=True) for _ in range(30): @@ -329,12 +332,12 @@ def _exercise_actual_family_callers(identity, monkeypatch, *, scenario='brake', for paused_tick in range(350): state, axis, host_state, command = step() if any(event.softDisable for event in card.sm['onroadEvents']): - assert card.aol_transport_feedback.ordinary_warning_qualified + assert card.aol_transport_feedback.ordinary_warning_qualified == (not lateral_expected) if paused_tick > 20: - assert safety.aol_get_request_mask() == safety.aol_get_permission_mask() == 0 + assert safety.aol_get_request_mask() == safety.aol_get_permission_mask() == int(lateral_expected) assert card.aol_card_intent.allowed_latch, 'Ordinary warning erased armed Mazda lateral intent' - assert not command.latActive and not command.longActive - assert not axis.lateralActive and not axis.longitudinalActive + assert command.latActive == axis.lateralActive == lateral_expected + assert not command.longActive and not axis.longitudinalActive assert getattr(EventName, warning) in sd.events.names soft_warning[0] = False for _ in range(40): @@ -347,17 +350,19 @@ def _exercise_actual_family_callers(identity, monkeypatch, *, scenario='brake', return if scenario == 'warning_active': from openpilot.selfdrive.selfdrived.events import EventName + assert isinstance(warning, str) + lateral_expected = warning == 'radarTempUnavailable' assert card.aol_card_intent.allowed_latch and host_state.enabled and command.latActive soft_warning[0] = True for paused_tick in range(350): state, axis, host_state, command = step(engaged=True) if any(event.softDisable for event in card.sm['onroadEvents']): - assert card.aol_transport_feedback.ordinary_warning_qualified + assert card.aol_transport_feedback.ordinary_warning_qualified == (not lateral_expected) if paused_tick > 20: - assert safety.aol_get_request_mask() == safety.aol_get_permission_mask() == 0 + assert safety.aol_get_request_mask() == safety.aol_get_permission_mask() == int(lateral_expected) assert card.aol_card_intent.allowed_latch, 'Ordinary warning erased armed Mazda lateral intent' - assert not command.latActive and not command.longActive - assert not axis.lateralActive and not axis.longitudinalActive + assert command.latActive == axis.lateralActive == lateral_expected + assert not command.longActive and not axis.longitudinalActive assert getattr(EventName, warning) in sd.events.names soft_warning[0] = False for _ in range(40): diff --git a/openpilot/selfdrive/car/card.py b/openpilot/selfdrive/car/card.py index 321c171bef..425a7b55e3 100644 --- a/openpilot/selfdrive/car/card.py +++ b/openpilot/selfdrive/car/card.py @@ -1363,7 +1363,8 @@ class Car: stamp, receipt = int(sm.logMonoTime['pandaStates']), int(sm.recv_time['pandaStates'] * 1e9) pandas = sm['pandaStates'] owner = getattr(self.vehicle_startup, 'owner', None) - count = 2 if isinstance(owner, CapturedADRVStartup) and hyundai_canfd_config_index(owner.cp) == 1 else 1 + count = 2 if (isinstance(owner, CapturedADRVStartup) and owner.continuity_required() and + hyundai_canfd_config_index(owner.cp) == 1) else 1 ignition_pandas = pandas if (count == 2 and len(pandas) == 2 and pandas[0].pandaType == log.PandaState.PandaType.dos and diff --git a/openpilot/selfdrive/test/process_replay/refs/starpilot/regen755D8CB1E1F_2025-04-08--23-13-43--0_card.zst b/openpilot/selfdrive/test/process_replay/refs/starpilot/regen755D8CB1E1F_2025-04-08--23-13-43--0_card.zst index 4076056825..f98e923b4f 100644 Binary files a/openpilot/selfdrive/test/process_replay/refs/starpilot/regen755D8CB1E1F_2025-04-08--23-13-43--0_card.zst and b/openpilot/selfdrive/test/process_replay/refs/starpilot/regen755D8CB1E1F_2025-04-08--23-13-43--0_card.zst differ diff --git a/openpilot/starpilot/aol/tests/test_availability_recovery_pair.py b/openpilot/starpilot/aol/tests/test_availability_recovery_pair.py index 8e72cb890a..4488ad16a6 100644 --- a/openpilot/starpilot/aol/tests/test_availability_recovery_pair.py +++ b/openpilot/starpilot/aol/tests/test_availability_recovery_pair.py @@ -67,6 +67,7 @@ def _exercise_recovery(full_op, order, fault): observed = sd.CS_prev.as_builder() if index == len(keys) - 1: if fault == 'cancel': + card.aol_card_intent.settings = replace(card.aol_card_intent.settings, cancel_actions=(9, 0, 0)) observed.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.cancel, pressed=True)] elif fault in ('native_wait', 'native_denial'): native = decode_safety(sd.sm['aolSafetyWire']) diff --git a/openpilot/starpilot/aol/tests/test_classic_cancel_guards.py b/openpilot/starpilot/aol/tests/test_classic_cancel_guards.py index a37c3fd41c..bd8151777e 100644 --- a/openpilot/starpilot/aol/tests/test_classic_cancel_guards.py +++ b/openpilot/starpilot/aol/tests/test_classic_cancel_guards.py @@ -24,7 +24,7 @@ def marked(): return cp -@pytest.mark.parametrize('case', ['retain', 'settings_off', 'unmapped', 'configured_cancel', 'pause', 'not_latched', +@pytest.mark.parametrize('case', ['retain', 'settings_off', 'default_buttons', 'configured_cancel', 'pause', 'not_latched', 'no_raw_source', 'expired_original', 'old_native', 'native_lat_denied', 'native_long_allowed', 'no_long_request', 'wrong_session', 'wrong_inventory', 'panda_rx', 'panda_fault', 'failed_feedback', 'changed_cp', 'producer', 'wrong_epoch', 'nonfull', 'hard_fault', 'reverse', 'steer_fault', 'invalid_can', 'no_physical_cancel']) @@ -114,6 +114,8 @@ def test_qualified_buttons_preserve_saved_mapping_and_cancel_behavior(lkas, main assert owner.allowed_latch state.buttonEvents = [structs.CarState.ButtonEvent(type='mainCruise', pressed=False)] owner.update(state, now_ns=5) + original_edge = owner._last_latch_edge_ns state.buttonEvents = [structs.CarState.ButtonEvent(type='cancel', pressed=True)] owner.update(state, now_ns=6) - assert owner.allowed_latch == (lkas == 9 or main == 9) + assert owner.allowed_latch + assert owner._last_latch_edge_ns == original_edge diff --git a/openpilot/starpilot/aol/tests/test_hkg_cancel_caller.py b/openpilot/starpilot/aol/tests/test_hkg_cancel_caller.py index 815dc9a1eb..bb4a60c623 100644 --- a/openpilot/starpilot/aol/tests/test_hkg_cancel_caller.py +++ b/openpilot/starpilot/aol/tests/test_hkg_cancel_caller.py @@ -31,7 +31,7 @@ def test_actual_selfdrived_cancel_keeps_only_current_owned_lateral(identity, alt now = 4_000_000_000 cp = params(identity, alternate=alternate) cp.safetyConfigs[-1].safetyParam |= policy_for(cp).safety_param_addition - admitted = not two_panda or (not alternate and identity in (CAR.KIA_EV6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN)) + admitted = True # Registered stock CAN-FD owners also qualify behind an inert companion. if two_panda: owner = structs.CarParams.SafetyConfig(safetyModel=str(cp.safetyConfigs[-1].safetyModel), safetyParam=int(cp.safetyConfigs[-1].safetyParam)) diff --git a/openpilot/starpilot/aol/tests/test_hkg_shared_cancel_guard.py b/openpilot/starpilot/aol/tests/test_hkg_shared_cancel_guard.py index 27d0704373..9c6c68a5d6 100644 --- a/openpilot/starpilot/aol/tests/test_hkg_shared_cancel_guard.py +++ b/openpilot/starpilot/aol/tests/test_hkg_shared_cancel_guard.py @@ -124,7 +124,7 @@ def captured_context(): [ 'retain', 'settings_off', - 'unmapped', + 'default_buttons', 'configured_cancel', 'pause', 'not_latched', @@ -155,7 +155,7 @@ def test_shared_cancel_retains_only_original_current_reason_qualified_intent(cas source = now if case == 'settings_off': owner.settings = replace(owner.settings, enabled=False) - elif case == 'unmapped': + elif case == 'default_buttons': owner.settings = replace(owner.settings, lkas_action=0) elif case == 'configured_cancel': owner.settings = replace(owner.settings, cancel_actions=(3, 0, 0)) @@ -216,7 +216,7 @@ def test_shared_cancel_retains_only_original_current_reason_qualified_intent(cas before = deepcopy(vars(feedback)) native_before = asdict(native) result = retained_shared_long_cancel_intent(cp, cs, sm, feedback, owner, cancel_source_ns=source, now_ns=now, producer='authority-card') - assert result == (case == 'retain') + assert result == (case in ('retain', 'default_buttons')) assert vars(feedback) == before # No lease, output, source, baseline or identity renewal. assert asdict(native) == native_before diff --git a/openpilot/starpilot/aol/tests/test_location_input_intent.py b/openpilot/starpilot/aol/tests/test_location_input_intent.py index c264d8ed06..70623eb10b 100644 --- a/openpilot/starpilot/aol/tests/test_location_input_intent.py +++ b/openpilot/starpilot/aol/tests/test_location_input_intent.py @@ -134,7 +134,7 @@ def test_stock_location_input_retention_refuses_independent_faults(identity, fau # preserves intent, whereas the explicitly configured CANCEL still clears. baseline = (card.aol_transport_feedback.lateral_baseline_ns, card.aol_transport_feedback.lateral_native_ns) retained = receive(10_020_000_000, state=state, axis=proof.as_reader(), event=event.as_reader()) - expected = fault in ('seatbelt', 'brake', 'regen') + expected = fault in ('seatbelt', 'brake', 'regen', 'physical_cancel') assert retained == expected, fault assert card.aol_card_intent.allowed_latch == expected assert not axis.lateralActive and not axis.longitudinalActive diff --git a/openpilot/starpilot/aol/tests/test_new_expired_combined_feedback.py b/openpilot/starpilot/aol/tests/test_new_expired_combined_feedback.py index 2a09b22a8f..101546e029 100644 --- a/openpilot/starpilot/aol/tests/test_new_expired_combined_feedback.py +++ b/openpilot/starpilot/aol/tests/test_new_expired_combined_feedback.py @@ -34,7 +34,8 @@ def assert_expired(controls, proof, now): @pytest.mark.parametrize('accepted_before_expiry', [False, True]) -def test_new_combined_output_expiry_retains_intent_without_grant(accepted_before_expiry): +@pytest.mark.parametrize('cancel', [False, True]) +def test_new_combined_output_expiry_retains_intent_without_grant(accepted_before_expiry, cancel): case = fixture.IoniqAvailabilityTests(methodName='runTest') with ExitStack() as clean: clean.callback(case.doCleanups) @@ -47,7 +48,10 @@ def test_new_combined_output_expiry_retains_intent_without_grant(accepted_before feedback = card.aol_transport_feedback original = (feedback.lateral_source_ns, feedback.lateral_baseline_ns, feedback.lateral_native_ns, feedback.healthy_feedback_ns, feedback.healthy_native_ns) - assert receive(NOW, axis=proof, event=event) + state = fixture.car_state() + if cancel: + state.buttonEvents = [fixture.car.CarState.ButtonEvent(type='cancel', pressed=True)] + assert receive(NOW, axis=proof, event=event, state=state) assert original == (feedback.lateral_source_ns, feedback.lateral_baseline_ns, feedback.lateral_native_ns, feedback.healthy_feedback_ns, feedback.healthy_native_ns) assert not feedback.qualified and not feedback.retention_qualified and not feedback.failed @@ -160,6 +164,8 @@ def test_new_expired_combined_never_hides_restriction(fault): if fault == 'permanent_eps': state.steerFaultPermanent = True if fault == 'physical_cancel': + # The fault requests lateral withdrawal; default CANCEL only stops cruise. + card.aol_card_intent.settings = replace(card.aol_card_intent.settings, cancel_actions=(9, 0, 0)) state.buttonEvents = [fixture.car.CarState.ButtonEvent(type=fixture.car.CarState.ButtonEvent.Type.cancel, pressed=True)] if fault == 'settings_disabled': card.aol_card_intent.settings = replace(card.aol_card_intent.settings, enabled=False) diff --git a/openpilot/starpilot/aol/tests/test_resume_lateral.py b/openpilot/starpilot/aol/tests/test_resume_lateral.py index a89041ec3b..4fec1e772f 100644 --- a/openpilot/starpilot/aol/tests/test_resume_lateral.py +++ b/openpilot/starpilot/aol/tests/test_resume_lateral.py @@ -78,8 +78,11 @@ def test_resume_before_first_set_retains_established_lateral_owner(order): case.doCleanups() -@pytest.mark.parametrize('fault', ['unarmed', 'pause', 'native_denied', 'native_invalid', 'panda_rx', 'eps_permanent', 'door', 'can_invalid']) -def test_resume_refusal_never_bypasses_real_lateral_veto(fault): +@pytest.mark.parametrize('fault,lateral_allowed', [ + ('unarmed', False), ('pause', False), ('native_denied', False), ('native_invalid', False), + ('panda_rx', False), ('eps_permanent', False), ('door', True), ('can_invalid', False), +]) +def test_resume_refusal_preserves_lateral_only_with_current_native_grant(fault, lateral_allowed): with OpenpilotPrefix(): case, (sd, _, drive, card, receive) = make_callers() try: @@ -107,9 +110,14 @@ def test_resume_refusal_never_bypasses_real_lateral_veto(fault): axis, selfdrive, command = drive(2, **options) assert EventName.resumeBlocked in sd.events.names assert not sd.enabled and not selfdrive.enabled and not command.longActive and not axis.longitudinalActive - assert not axis.lateralActive and not command.latActive + assert axis.lateralActive == command.latActive == lateral_allowed assert not command.cruiseControl.resume if fault in ('native_denied', 'native_invalid', 'panda_rx', 'eps_permanent', 'door', 'can_invalid'): - assert not receive(10_020_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'], state=state) + assert receive(10_020_000_000, axis=sd.pm.messages['aolAxisState'], + event=sd.pm.messages['onroadEvents'], state=state) == lateral_allowed + if lateral_allowed: + assert EventName.doorOpen in sd.events.names + native = current_native(sd.sm, sd.CP, now_ns=10_020_000_000, axis_session_id=sd.aol_session_id) + assert native is not None and native.requestedLateral and native.lateralAllowed finally: case.doCleanups() diff --git a/openpilot/starpilot/aol/tests/test_sonata_hybrid_cancel_scope.py b/openpilot/starpilot/aol/tests/test_sonata_hybrid_cancel_scope.py index 254a3fba09..e939d77cf5 100644 --- a/openpilot/starpilot/aol/tests/test_sonata_hybrid_cancel_scope.py +++ b/openpilot/starpilot/aol/tests/test_sonata_hybrid_cancel_scope.py @@ -50,7 +50,7 @@ def test_hybrid_added_scope_preserves_existing_profile_vetoes(defect): assert cp.to_dict() == before -@pytest.mark.parametrize('case', ['retain', 'settings_off', 'unmapped', 'configured_cancel', 'pause', 'not_latched', +@pytest.mark.parametrize('case', ['retain', 'settings_off', 'default_buttons', 'configured_cancel', 'pause', 'not_latched', 'no_raw_source', 'expired_original', 'old_native', 'native_lat_denied', 'native_long_allowed', 'no_long_request', 'wrong_session', 'wrong_inventory', 'panda_rx', 'panda_fault', 'failed_feedback', 'changed_cp', 'producer', 'wrong_epoch', 'nonfull', 'hard_fault', 'reverse', 'steer_fault', 'invalid_can', 'no_physical_cancel']) diff --git a/openpilot/starpilot/aol/tests/test_temporary_steering_sequence.py b/openpilot/starpilot/aol/tests/test_temporary_steering_sequence.py index 8421f28979..ae4d49fbe2 100644 --- a/openpilot/starpilot/aol/tests/test_temporary_steering_sequence.py +++ b/openpilot/starpilot/aol/tests/test_temporary_steering_sequence.py @@ -122,6 +122,7 @@ def test_fresh_active_eps_never_masks_hard_fault_or_bad_original_provenance(faul elif fault == 'fault_session': proof.aolAxisState.faultSessionId = 'other-selfdrive' elif fault == 'cancel': + card.aol_card_intent.settings = replace(card.aol_card_intent.settings, cancel_actions=(9, 0, 0)) temporary.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.cancel, pressed=True)] elif fault == 'settings_disabled': card.aol_card_intent.settings = replace(card.aol_card_intent.settings, enabled=False) @@ -286,6 +287,7 @@ def test_pending_eps_join_cannot_extend_or_hide_faults(fault): elif fault == 'can_timeout': temporary.canTimeout = True elif fault == 'cancel': + card.aol_card_intent.settings = replace(card.aol_card_intent.settings, cancel_actions=(9, 0, 0)) temporary.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.cancel, pressed=True)] elif fault == 'settings_disabled': card.aol_card_intent.settings = replace(card.aol_card_intent.settings, enabled=False) diff --git a/openpilot/starpilot/aol/tests/test_torque_stock_brake_guards.py b/openpilot/starpilot/aol/tests/test_torque_stock_brake_guards.py index 4ab04d4d77..5327935f53 100644 --- a/openpilot/starpilot/aol/tests/test_torque_stock_brake_guards.py +++ b/openpilot/starpilot/aol/tests/test_torque_stock_brake_guards.py @@ -211,7 +211,7 @@ def test_source_selected_hybrid_stock_brake_continuity(tick, gesture): @pytest.mark.parametrize('identity,lka', MODELS) @pytest.mark.parametrize('tick', [3, 7]) @pytest.mark.parametrize('gesture', ['cancel', 'brake']) -def test_default_button_mapping_keeps_its_existing_cancel_withdrawal(identity, lka, tick, gesture): +def test_default_button_mapping_retains_independent_lateral_on_cruise_withdrawal(identity, lka, tick, gesture): with configured_callers(identity, lka, 'baseline', lkas_action=0) as context: sd, step, card, receive = context.sd, context.step, context.card, context.receive state = car_state(brake=gesture == 'brake') @@ -220,20 +220,20 @@ def test_default_button_mapping_keeps_its_existing_cancel_withdrawal(identity, l if gesture == 'cancel': state.buttonEvents = [structs.CarState.ButtonEvent(type='cancel', pressed=True)] card.aol_card_intent.update(state, now_ns=10_000_000_000 + tick * 10_000_000) - assert card.aol_card_intent.allowed_latch == (gesture == 'brake') + assert card.aol_card_intent.allowed_latch axis, _, command = step(tick, observed_state=state, panda_controls_allowed=gesture == 'cancel') assert axis.lateralActive and command.latActive assert not axis.longitudinalActive and not command.longActive assert receive(10_000_000_000 + tick * 10_000_000, state=state, - axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents']) == (gesture == 'brake') + axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents']) settled = car_state(brake=gesture == 'brake') settled.vEgoRaw = settled.vEgo axis, _, command = step(tick + 1, observed_state=settled) assert not sd.enabled - assert bool(axis.lateralActive) == bool(command.latActive) == (gesture == 'brake') + assert axis.lateralActive and command.latActive assert not axis.longitudinalActive and not command.longActive assert receive(10_000_000_000 + (tick + 1) * 10_000_000, state=settled, - axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents']) == (gesture == 'brake') + axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents']) @pytest.mark.parametrize('identity,lka', MODELS) diff --git a/openpilot/starpilot/controller_extensions.py b/openpilot/starpilot/controller_extensions.py index aaafe8fcd1..30fc435eb2 100644 --- a/openpilot/starpilot/controller_extensions.py +++ b/openpilot/starpilot/controller_extensions.py @@ -175,7 +175,7 @@ def configure_controller(CI, params): def observe_controller_can(CI, can_list): - controller = CI.CC + controller = getattr(CI, "CC", None) if controller is not None and CI.CP.brand == 'ford': from openpilot.starpilot.car.ford.bluepilot import FordBluePilotController if isinstance(getattr(controller, 'bp', None), FordBluePilotController): diff --git a/openpilot/starpilot/galaxy/setting_descriptions.py b/openpilot/starpilot/galaxy/setting_descriptions.py index 39e1658fc3..ffd2a7e9a2 100644 --- a/openpilot/starpilot/galaxy/setting_descriptions.py +++ b/openpilot/starpilot/galaxy/setting_descriptions.py @@ -53,6 +53,7 @@ DESCRIPTIONS = { 'FasterLeadTakeoff': ('Improves launches from a stop and responds sooner when a lead starts moving. ' + 'Selected acceleration profiles, stop detection and vehicle limits still apply.'), 'CustomPersonalities': 'Uses your following-time and jerk settings for the selected driving personality.', + 'profile:stopped_gap': 'Adds this distance to the following gap behind a stopped lead vehicle.', 'profile:global_acceleration': 'Chooses the acceleration curve used by personalities set to Selected Profile.', 'profile:global_braking': 'Chooses the braking response used by personalities set to Selected Profile.', 'reset_profiles': 'Restores the default acceleration, braking and following curves and presets.', @@ -67,6 +68,7 @@ DESCRIPTIONS = { 'SLCPriority1': 'Chooses the first source checked for a fresh speed limit.', 'SLCPriority2': 'Chooses the source used when the primary source has no fresh speed limit.', 'slc_reset': 'Resets saved speed-limit offsets while retaining valid configured speed ranges.', + 'reset_calibration': 'Clears saved camera and steering calibration. The device recalibrates on the next drive.', 'reset_driver_monitoring': 'Clear saved wheel-side detection. Automatic detection restarts on the next drive.', 'AlwaysAllowUploads': 'Allows queued full logs and videos to upload over metered networks. This uses mobile data.', 'UseVOACC': ('Use vision-only adaptive cruise control instead of radar-assisted lead detection. ' + diff --git a/openpilot/starpilot/galaxy/tests/description-key-inventory.json b/openpilot/starpilot/galaxy/tests/description-key-inventory.json index 57e8fce346..4451cdbba3 100644 --- a/openpilot/starpilot/galaxy/tests/description-key-inventory.json +++ b/openpilot/starpilot/galaxy/tests/description-key-inventory.json @@ -44,7 +44,6 @@ "IsMetric", "IsRhdDetected", "KeepScreenOnGpuLoading", - "LC_PID_gain_UI_curv", "LaneCenterOffset", "LaneCentering", "LaneCenteringE2EAuthority", @@ -163,17 +162,10 @@ "conditional:mode", "conditional:reset", "curve_reset", - "custom_path_offset_ang", - "custom_path_offset_curv", "custom_profile_curv", - "disable_BP_lat_UI", "display:auto:ScreenBrightness", "display:auto:ScreenBrightnessOnroad", "enable_human_turn_detection_curv", - "enable_lane_full_mode_curv", - "enable_lane_positioning_ang", - "enable_lane_positioning_curv", - "lane_centering_strength_ang", "lane_change:auto", "lane_change:close_gap", "lane_change:close_gap_seconds", @@ -257,6 +249,7 @@ "profile:aggressive:following:8", "profile:aggressive:following:9", "profile:global_acceleration", + "profile:stopped_gap", "profile:global_braking", "profile:relaxed:acceleration", "profile:relaxed:acceleration:0", @@ -357,6 +350,7 @@ "profile:traffic:following:7", "profile:traffic:following:8", "profile:traffic:following:9", + "reset_calibration", "reset_driver_monitoring", "reset_profiles", "sentry:enabled", diff --git a/openpilot/starpilot/lateral/tests/test_corolla_tss2_policy.py b/openpilot/starpilot/lateral/tests/test_corolla_tss2_policy.py index d4e45e8167..28830af722 100644 --- a/openpilot/starpilot/lateral/tests/test_corolla_tss2_policy.py +++ b/openpilot/starpilot/lateral/tests/test_corolla_tss2_policy.py @@ -13,6 +13,7 @@ from opendbc.car.toyota.values import CAR from opendbc.car.vehicle_model import VehicleModel from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from openpilot.starpilot.lateral.torque_extension import selected_policy +from openpilot.starpilot.lateral.torque_shaping import INTERP_SPEEDS, KP_INTERP, KP from openpilot.starpilot.lateral.corolla_tss2_policy import supported_cp FIXTURE = Path(__file__).with_name('fixtures') / 'corolla_tss2_ba901b5f.json' @@ -28,6 +29,9 @@ def test_frozen_original_numeric_traces(case_index): fixture = json.loads(FIXTURE.read_text()) cp, controller, vm = controller_for() assert selected_policy(controller) is not None + assert controller.pid._k_p == [[0.0], [KP]] + # Isolate the historical gain schedule while keeping the original numeric traces frozen. + controller.pid._k_p = [list(INTERP_SPEEDS), list(KP_INTERP)] params = log.VehicleParameters.new_message(angleOffsetDeg=0.3, roll=0.006) for index, frame in enumerate(fixture['cases'][case_index]['frames']): active, speed, angle, pressed, limited, curve, curve_limited, delay = frame['input'] diff --git a/openpilot/starpilot/lateral/tests/test_genesis_g70_policy.py b/openpilot/starpilot/lateral/tests/test_genesis_g70_policy.py index bcae05b2ac..f2e9b02433 100644 --- a/openpilot/starpilot/lateral/tests/test_genesis_g70_policy.py +++ b/openpilot/starpilot/lateral/tests/test_genesis_g70_policy.py @@ -16,6 +16,7 @@ from opendbc.car.vehicle_model import VehicleModel from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from openpilot.starpilot.lateral.controller_selection import ControllerMode from openpilot.starpilot.lateral.torque_extension import selected_policy +from openpilot.starpilot.lateral.torque_shaping import INTERP_SPEEDS, KP_INTERP, KP from openpilot.starpilot.lateral.genesis_g70_policy import supported_cp from openpilot.starpilot.lateral import genesis_g70_policy @@ -37,6 +38,9 @@ def test_frozen_base_tuning_numeric_traces(_traffic, _damping, case_index): fixture = json.loads(FIXTURE.read_text()) cp, controller, vm = controller_for() assert selected_policy(controller) is not None + assert controller.pid._k_p == [[0.0], [KP]] + # Isolate the historical gain schedule while keeping the original numeric traces frozen. + controller.pid._k_p = [list(INTERP_SPEEDS), list(KP_INTERP)] params = log.VehicleParameters.new_message(angleOffsetDeg=0.3, roll=0.006) for index, frame in enumerate(fixture['cases'][case_index]['frames']): active, speed, angle, pressed, limited, curve, curve_limited, delay = frame['input'] diff --git a/openpilot/starpilot/lateral/tests/test_genesis_gv70_policy.py b/openpilot/starpilot/lateral/tests/test_genesis_gv70_policy.py index 2915559c61..4c869505a4 100644 --- a/openpilot/starpilot/lateral/tests/test_genesis_gv70_policy.py +++ b/openpilot/starpilot/lateral/tests/test_genesis_gv70_policy.py @@ -19,6 +19,7 @@ from openpilot.starpilot.lateral.controller_selection import ControllerMode, pol from openpilot.starpilot.lateral.torque_extension import selected_policy from openpilot.starpilot.lateral import genesis_gv70_policy as policy from openpilot.starpilot.lateral import gv70_shaping +from openpilot.starpilot.lateral.torque_shaping import KP def controller_for(vehicle=CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, mode=None): @@ -53,8 +54,7 @@ def test_exact_admission_and_saved_standard_keeps_upstream_controller(): assert policy.supported_cp(cp) assert policy_for(cp) == 'genesis_gv70_electrified' assert isinstance(selected_policy(controller), policy.GenesisGV70TorquePolicy) - # Dom retains the full speed-interpolated schedule, ending at0.6, not flat0.6. - assert controller.pid._k_p == [[1, 1.5, 2.0, 3.0, 5, 7.5, 10, 15, 30], [250, 120, 65, 30, 11.5, 5.5, 3.5, 2.0, 0.6]] + assert controller.pid._k_p == [[0.0], [KP]] assert controller.pid._k_i == ([0], [0.35]) choice = json.dumps({'version': 1, 'vehicles': {'GENESIS_GV70_ELECTRIFIED_1ST_GEN': {'brand': 'hyundai', 'mode': 'standard'}}}).encode() assert selection_from_bytes(cp, choice).mode == ControllerMode.STANDARD diff --git a/openpilot/starpilot/lateral/tests/test_palisade_policy.py b/openpilot/starpilot/lateral/tests/test_palisade_policy.py index bbc8a99054..d7e9873e20 100644 --- a/openpilot/starpilot/lateral/tests/test_palisade_policy.py +++ b/openpilot/starpilot/lateral/tests/test_palisade_policy.py @@ -13,7 +13,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from openpilot.starpilot.lateral.controller_selection import ControllerMode, default_selection, replace_mode, selection_from_bytes from openpilot.starpilot.lateral.palisade_policy import PalisadeTorquePolicy, supported_cp from openpilot.starpilot.lateral import palisade_shaping as shaping -from openpilot.starpilot.lateral.torque_shaping import INTERP_SPEEDS, KP_INTERP, KI +from openpilot.starpilot.lateral.torque_shaping import KP, KI @pytest.mark.parametrize('speed,accel,jerk,ff,threshold,friction,center,output', [ @@ -37,7 +37,7 @@ def test_default_choice_startup_and_repeated_live_factor(vehicle): assert default_selection(cp).policy == 'palisade' assert isinstance(controller.starpilot_extension.policy, PalisadeTorquePolicy) assert controller.torque_params.latAccelFactor == pytest.approx(cp.lateralTuning.torque.latAccelFactor * .98) - assert controller.pid._k_p == [INTERP_SPEEDS, KP_INTERP] + assert controller.pid._k_p == [[0.], [KP]] assert controller.pid._k_i == ([0.], [KI]) raw_limit = controller.lateral_accel_from_torque(controller.steer_max, cp.lateralTuning.torque) assert controller.pid.pos_limit == pytest.approx(raw_limit) diff --git a/openpilot/starpilot/ui/tests/test_onroad_navigation.py b/openpilot/starpilot/ui/tests/test_onroad_navigation.py index 85568f1e25..fd9dd129cb 100644 --- a/openpilot/starpilot/ui/tests/test_onroad_navigation.py +++ b/openpilot/starpilot/ui/tests/test_onroad_navigation.py @@ -123,7 +123,7 @@ def test_slc_decision_keeps_its_touch_priority_only_where_it_overlaps(): observed.customization['layouts']['compact']['speed_limit_actions']['y'] = 4 observed.customization['layouts']['compact']['max_speed']['y'] = 58 assert card.bounds(observed) is None - assert card.bounds(replace(observed, longitudinal_active=False)) is None + assert card.bounds(replace(observed, longitudinal_active=False)) is not None # No confirmation chooser while LONG is disengaged. # SLC being on, with no decision on screen, leaves the directions up. assert card.bounds(replace(state(), longitudinal_active=True, speed_limit=SpeedLimitObservation(action_enabled=True))) is not None large = NavigationCard(Mock(profile=Profile.LARGE)) diff --git a/prebuilt.json b/prebuilt.json index 45bed363d0..b28f0da9a4 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "11315c6f254dc3b38e35efe958ea0277cb399ba33f13db28405b0fcbf43a1f96", "schema_version": 1} +{"protected_inventory_sha256": "2c53dd67035983d9cdbb9270c506eb6b2fce43c390761e26c4bf4dd3bc9ff0ec", "schema_version": 1} diff --git a/tools/ci/tests/test_feature_test_discovery.py b/tools/ci/tests/test_feature_test_discovery.py index 5ed95092d3..a7c4588b1f 100644 --- a/tools/ci/tests/test_feature_test_discovery.py +++ b/tools/ci/tests/test_feature_test_discovery.py @@ -17,12 +17,12 @@ class TestFeatureTestDiscovery(unittest.TestCase): shards = jobs['custom_pytest'] complete = jobs['custom_pytest_complete'] matrix = re.search(r'^ shard: (\[.*\])$', shards, re.M) - self.assertIsNotNone(matrix) - self.assertEqual(ast.literal_eval(matrix[1]), list(range(24))) + assert matrix is not None + self.assertEqual(ast.literal_eval(matrix[1]), list(range(32))) self.assertIn(' fail-fast: false\n', shards) self.assertNotIn('continue-on-error:', shards + complete) self.assertIn(' run: .venv/bin/python -m tools.ci.custom_pytest_shards run ' - + '--index "${{ matrix.shard }}" --count 24 --output "ci-results/custom-pytest-${{ matrix.shard }}"\n', shards) + + '--index "${{ matrix.shard }}" --count 32 --output "ci-results/custom-pytest-${{ matrix.shard }}"\n', shards) self.assertIn(' name: custom-pytest-${{ matrix.shard }}\n', shards) self.assertIn(' path: ci-results/custom-pytest-${{ matrix.shard }}/\n', shards) self.assertIn(' if-no-files-found: error\n', shards) @@ -31,7 +31,7 @@ class TestFeatureTestDiscovery(unittest.TestCase): self.assertIn(' pattern: custom-pytest-*\n', complete) self.assertIn(' path: ci-results/custom-pytest-shards\n', complete) self.assertIn(' run: python3 -m tools.ci.custom_pytest_shards aggregate ' - + '--directory ci-results/custom-pytest-shards --count 24 --output ci-results/custom-pytest-complete.json\n', complete) + + '--directory ci-results/custom-pytest-shards --count 32 --output ci-results/custom-pytest-complete.json\n', complete) def test_hosted_pytest_selection_covers_all_custom_function_tests(self): workflow = (ROOT / '.github/workflows/tests.yaml').read_text() @@ -45,12 +45,12 @@ class TestFeatureTestDiscovery(unittest.TestCase): def test_missing_duplicate_or_mismatched_shards_are_rejected(self): workflow = (ROOT / '.github/workflows/tests.yaml').read_text() - matrix = 'shard: [' + ', '.join(map(str, range(24))) + ']' - for replacement in (list(range(23)), list(range(23)) + [22], list(range(25))): + matrix = 'shard: [' + ', '.join(map(str, range(32))) + ']' + for replacement in (list(range(31)), list(range(31)) + [30], list(range(33))): with self.subTest(matrix=replacement), self.assertRaises(AssertionError): self.assert_hosted_pytest_contract(workflow.replace(matrix, 'shard: ' + str(replacement))) with self.assertRaises(AssertionError): - self.assert_hosted_pytest_contract(workflow.replace('--count 24', '--count 23')) + self.assert_hosted_pytest_contract(workflow.replace('--count 32', '--count 31')) def test_completion_and_full_runner_cannot_be_bypassed(self): workflow = (ROOT / '.github/workflows/tests.yaml').read_text() @@ -60,7 +60,7 @@ class TestFeatureTestDiscovery(unittest.TestCase): (' if: always()\n', ' if: success()\n'), ('pattern: custom-pytest-*', 'pattern: custom-pytest-0'), ('if-no-files-found: error', 'if-no-files-found: ignore'), - ('--count 24 --output', '--count 24 -k selected --output'), + ('--count 32 --output', '--count 32 -k selected --output'), (' needs: custom_pytest', ' continue-on-error: true\n needs: custom_pytest'), ): with self.subTest(change=old), self.assertRaises(AssertionError):