"""The actual paired sampler bounds midpoint error without renewing proposals.""" import unittest from unittest.mock import patch from openpilot.starpilot.conditional_mode import projection from openpilot.starpilot.conditional_mode.consumer import ModeConsumer from openpilot.starpilot.conditional_mode.policy import ManualIntent from openpilot.starpilot.conditional_mode.preferences import manual_for_drive from openpilot.starpilot.conditional_mode.status import LIFETIME_NS from openpilot.starpilot.conditional_mode.tests import test_status_consumer as transport from openpilot.starpilot.conditional_mode.tests.test_projection import MONO, BOOT, CP, FakeSubMaster, serialized_scene class TestPairedClock(unittest.TestCase): def setUp(self): self.fixture = transport.TestModeTransport(methodName='runTest') self.fixture.setUp() self.addCleanup(self.fixture.doCleanups) self.fixture.consumer = ModeConsumer() self.sample(None, MONO) def pair(self, now, *, offset=-500, skew=2000): before = now - skew // 2 after = before + skew with patch.object(projection.time, 'CLOCK_BOOTTIME', getattr(projection.time, 'CLOCK_BOOTTIME', 7), create=True), \ patch.object(projection.time, 'monotonic_ns', side_effect=(before, after)), \ patch.object(projection.time, 'clock_gettime_ns', return_value=now + offset): result = projection.paired_clocks_ns() self.assertIsNotNone(result) assert result is not None self.assertEqual(result, (now, now + offset, skew)) return result def sample(self, event, now, *, offset=-500, skew=2000, **changes): mono, boot, error = self.pair(now, offset=offset, skew=skew) return self.fixture.consume(event, mono, now_boot_ns=boot, sample_skew_ns=error, **changes) def test_asymmetric_actual_clock_pair_accepts_fresh_proposal(self): now = MONO + 50_000_000 event, _, _ = self.fixture.produce(now) result = self.sample(event, now) self.assertTrue(result.accepted) self.assertTrue(result.experimental) self.assertIsNone(self.fixture.consumer.rejection) def test_exact_midpoint_half_skew_bound_accepts_even_and_odd_pairs(self): for skew in (1999, 2000): with self.subTest(skew=skew): now = MONO + 50_000_000 + skew event, _, _ = self.fixture.produce(now) self.assertTrue(self.sample(event, now, offset=-(skew // 2), skew=skew).accepted) self.assertIsNone(self.fixture.consumer.rejection) def test_negative_offset_beyond_midpoint_bound_rejected(self): for skew in (1999, 2000): with self.subTest(skew=skew): now = MONO + 50_000_000 event, _, _ = self.fixture.produce(now) self.assertFalse(self.sample(event, now, offset=-(skew // 2)-1, skew=skew).accepted) self.assertEqual(self.fixture.consumer.rejection, 'clock_inputs') self.assertIsNone(self.fixture.consumer.last) def test_valid_zero_and_positive_offsets_accept_fresh_proposals(self): for offset in (0, 100_000): with self.subTest(offset=offset): now = MONO + 50_000_000 + offset event, _, _ = self.fixture.produce(now) self.assertTrue(self.sample(event, now, offset=offset).accepted) self.assertIsNone(self.fixture.consumer.rejection) def test_true_clock_discontinuity_remains_rejected(self): now = MONO + 50_000_000 event, _, _ = self.fixture.produce(now) self.assertFalse(self.sample(event, now, offset=2_000_000).accepted) self.assertEqual(self.fixture.consumer.rejection, 'discontinuity') self.assertEqual(self.fixture.consumer.barrier_ns, now) def test_sampling_bound_cannot_renew_original_proposal_expiry(self): now = MONO + 50_000_000 # Same-time typed model/car sources remove an unrelated earlier source-age # limit, so this test reaches the proposal's exact original expiry. sm = FakeSubMaster(serialized_scene(stamp=BOOT + now - MONO), now) proposal = self.fixture.host.sample( sm, CP, now_mono_ns=now, now_boot_ns=BOOT + now - MONO, sample_skew_ns=1000, drive_id=self.fixture.drive_id, settings_owner=self.fixture.owner, manual=manual_for_drive(ManualIntent.FORCE_EXPERIMENTAL, self.fixture.drive_id, self.fixture.drive_id + 1), selected_t_follow_s=1.45, selected_t_follow_observed_mono_ns=now) event = self.fixture.publisher.attach(None, proposal, self.fixture.owner.current, now_ns=now, drive_id=self.fixture.drive_id, model_ns=now, car_state_ns=now) original_envelope = event.to_bytes() self.assertEqual(event.logMonoTime, now) self.assertTrue(self.sample(event, now, message_ns=now, receipt_ns=now, model_ns=now, car_state_ns=now).accepted) original = self.fixture.consumer.last assert original is not None self.assertEqual(original.expires_ns, now + LIFETIME_NS) expiry = original.expires_ns self.assertTrue(self.sample(event, expiry, message_ns=now, receipt_ns=now, model_ns=expiry, car_state_ns=expiry).accepted) self.assertEqual(self.fixture.consumer.last, original) self.assertEqual(event.as_reader().as_builder().to_bytes(), original_envelope) self.assertFalse(self.sample(event, expiry + 1, message_ns=now, receipt_ns=now, model_ns=expiry + 1, car_state_ns=expiry + 1).accepted) self.assertEqual(self.fixture.consumer.rejection, 'proposal_invalid') self.assertEqual(self.fixture.consumer.last, original) self.assertEqual(self.fixture.consumer.last.expires_ns, expiry) self.assertEqual(event.as_reader().as_builder().to_bytes(), original_envelope) def test_monotonic_rollback_and_drive_change_still_require_new_sources(self): now = MONO + 50_000_000 event, _, _ = self.fixture.produce(now) self.assertTrue(self.sample(event, now).accepted) self.assertFalse(self.sample(event, now - 1).accepted) self.assertEqual(self.fixture.consumer.rejection, 'discontinuity') self.assertIsNone(self.fixture.consumer.last) self.assertFalse(self.sample(event, now + 1, drive_id=self.fixture.drive_id + 1).accepted) self.assertEqual(self.fixture.consumer.rejection, 'discontinuity') self.assertEqual(self.fixture.consumer.barrier_ns, now + 1) def test_preempted_sample_retries_once_using_only_the_new_measurement(self): with patch.object(projection.time, 'CLOCK_BOOTTIME', 7, create=True), \ patch.object(projection.time, 'monotonic_ns', side_effect=(MONO, MONO + 2_000_000, MONO + 2_100_000, MONO + 2_102_000)), \ patch.object(projection.time, 'clock_gettime_ns', side_effect=(BOOT, BOOT + 2_101_000)): self.assertEqual(projection.paired_clocks_ns(), (MONO + 2_101_000, BOOT + 2_101_000, 2000)) def test_two_preemptions_or_rollback_still_fail_without_reusing_a_pair(self): for stamps in ((MONO, MONO + 2_000_000, MONO + 3_000_000, MONO + 5_000_000), (MONO, MONO - 1)): with self.subTest(stamps=stamps), \ patch.object(projection.time, 'CLOCK_BOOTTIME', 7, create=True), \ patch.object(projection.time, 'monotonic_ns', side_effect=stamps), \ patch.object(projection.time, 'clock_gettime_ns', return_value=BOOT): self.assertIsNone(projection.paired_clocks_ns())