Files
StarPilot/openpilot/starpilot/galaxy/tests/test_drive_stats.py
T
2026-10-27 10:05:44 -05:00

640 lines
26 KiB
Python

from unittest.mock import Mock
"""Durable local-drive statistics and background analysis contract."""
from datetime import UTC, datetime
from pathlib import Path
import copy
import json
import tempfile
import threading
import time
import unittest
from unittest import mock
from openpilot.starpilot.galaxy import drive_stats
from openpilot.starpilot.galaxy.drive_stats import DriveStatsOwner, MAX_CACHED_ROUTES
NOW = datetime(2026, 9, 27, 12, tzinfo=UTC).timestamp()
class History:
def __init__(self, root, routes):
self.root = root
self.routes = routes
self.scans = 0
self.incomplete = False
def snapshot(self):
self.scans += 1
return {'routes': list(self.routes), 'scanIncomplete': self.incomplete}
def route(identifier, number=0):
return {'routeId': identifier, 'segmentCount': 1,
'segments': [{'number': number, 'segmentName': f'{identifier}--{number}', 'files': {'rlog': True}}]}
def analyzed(root, selected, permitted):
return {'routeId': selected['routeId'], 'startTime': NOW - 60, 'endTime': NOW,
'distanceMeters': 1000.0, 'durationSeconds': 60.0, 'engagedSeconds': 30.0,
'engagedPercent': 50.0, 'model': 'Test Model', 'distractedMoments': 1,
'unresponsiveMoments': 0, 'complete': True, 'reason': None, 'segmentCount': 1}
class DriveStatsTest(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name) / 'logs'
self.root.mkdir()
self.store = Path(self.temp.name) / 'state' / 'drive-stats.json'
self.history = History(self.root, [route('abc')])
self.now = [NOW]
self.owners = []
def owner(self, analyzer=analyzed, permitted=lambda: True, monotonic=None, metric=None):
owner = DriveStatsOwner(root=self.root, store=self.store, history=Mock(wraps=self.history),
analyzer=analyzer, permitted=permitted, clock=lambda: self.now[0],
monotonic=monotonic if monotonic is not None else lambda: self.now[0], metric=metric)
self.owners.append(owner)
self.addCleanup(owner.close)
return owner
def completed(self, owner, scans=1):
deadline = time.monotonic() + 3
while time.monotonic() < deadline:
with owner._lock:
worker = owner._worker
if self.history.scans >= scans and worker and not worker.is_alive():
return owner.snapshot()
time.sleep(.01)
self.fail('background scan did not finish')
def test_recording_dates_reads_cache_without_starting_analysis(self):
owner = self.owner()
record = analyzed(self.root, route('abc'), lambda: True)
owner._records = {'abc': record}
self.assertEqual(owner.recording_dates(['abc', 'missing']), {'abc': NOW - 60})
self.assertIsNone(owner._worker)
self.assertEqual(self.history.scans, 0)
def test_aol_persists_and_exclusion_preserves_ordinary_engagement(self):
def with_aol(root, selected, permitted):
return {**analyzed(root, selected, permitted), 'aolSeconds': 15.0, 'aolPercent': 25.0}
owner = self.owner(analyzer=with_aol)
owner._start()
result = self.completed(owner)
self.assertEqual(result['totals']['aolSeconds'], 15)
self.assertEqual(result['totals']['engagedSeconds'], 30)
self.assertEqual(result['week']['aolPercent'], 25)
self.assertEqual(result['lastDrive']['aolSeconds'], 15)
loaded = self.owner().snapshot()
self.assertEqual(loaded['recentDrives'][0]['aolPercent'], 25)
ignored = owner.ignore('abc', True, lambda: True)
self.assertIsNone(ignored['totals']['aolSeconds'])
self.assertEqual(ignored['totals']['aolDrives'], 0)
self.assertEqual(ignored['recentDrives'][0]['aolSeconds'], 15)
def test_aol_aggregates_only_known_routes_and_weights_by_duration(self):
owner = self.owner(permitted=lambda: False)
base = {**analyzed(self.root, route('abc'), lambda: True), 'ignored': False}
owner._records = {
'abc': {**base, 'aolSeconds': 15, 'aolPercent': 25},
'long': {**base, 'routeId': 'long', 'durationSeconds': 180, 'aolSeconds': 135, 'aolPercent': 75},
'old': {**base, 'routeId': 'old'},
'gap': {**base, 'routeId': 'gap', 'aolSeconds': None, 'aolPercent': None},
'ignored': {**base, 'routeId': 'ignored', 'ignored': True, 'aolSeconds': 60, 'aolPercent': 100},
'incomplete': {**base, 'routeId': 'incomplete', 'complete': False, 'aolSeconds': 60, 'aolPercent': 100},
}
result = owner.snapshot()
for summary in (result['totals'], result['week']):
self.assertEqual(summary['aolSeconds'], 150)
self.assertEqual(summary['aolPercent'], 62.5)
self.assertEqual(summary['aolDurationSeconds'], 240)
self.assertEqual(summary['aolDrives'], 2)
self.assertEqual(summary['drives'], 4)
self.assertEqual(result['totals']['engagedSeconds'], 120)
def test_cache_rejects_invalid_aol_without_requiring_it_in_old_records(self):
owner = self.owner()
owner._start()
self.completed(owner)
record = json.loads(self.store.read_text())['routes']['abc']
self.assertTrue(drive_stats._valid_record(record))
for fields in ({'aolSeconds': -1, 'aolPercent': 0}, {'aolSeconds': True, 'aolPercent': 0},
{'aolSeconds': float('nan'), 'aolPercent': 0}, {'aolSeconds': 61, 'aolPercent': 100},
{'aolSeconds': 1, 'aolPercent': 101}, {'aolSeconds': 1, 'aolPercent': None}):
with self.subTest(fields=fields):
self.assertFalse(drive_stats._valid_record({**record, **fields}))
def test_complete_aggregation_week_models_and_exclusion_persist(self):
owner = self.owner()
owner._start()
initial = owner.snapshot()
self.assertEqual(initial['totals']['drives'], 0)
result = self.completed(owner)
self.assertEqual(result['totals']['distanceMeters'], 1000)
self.assertEqual(result['totals']['engagedSeconds'], 30)
self.assertEqual(result['week']['engagedPercent'], 50)
self.assertEqual(result['week']['days'][-1]['distanceMeters'], 1000)
self.assertEqual(result['models'][0]['name'], 'Test Model')
self.assertEqual(result['records'][0]['id'], 'longestDrive')
ignored = owner.ignore('abc', True, lambda: True)
self.assertEqual(ignored['totals']['drives'], 0)
self.assertTrue(ignored['recentDrives'][0]['ignored'])
self.assertIsNone(ignored['lastDrive'])
second = self.owner()
loaded = second.snapshot()
self.assertEqual(loaded['totals']['drives'], 0)
self.assertTrue(loaded['recentDrives'][0]['ignored'])
with self.assertRaises(PermissionError):
second.ignore('abc', False, lambda: False)
self.assertEqual(second.ignore('abc', False, lambda: True)['totals']['drives'], 1)
def test_week_uses_viewer_calendar_at_midnight_and_dst(self):
owner = self.owner(permitted=lambda: False)
self.now[0] = datetime(2026, 9, 28, 2, tzinfo=UTC).timestamp()
record = analyzed(self.root, route('abc'), lambda: True)
record.update(startTime=self.now[0] - 60, ignored=False)
owner._records = {'abc': record}
local = owner.snapshot('America/Chicago')
self.assertEqual(local['week']['days'][-1]['date'], '2026-09-27')
self.assertEqual(local['week']['days'][-1]['distanceMeters'], 1000)
self.assertEqual(owner.snapshot()['week']['days'][0]['date'], '2026-09-28')
for stamp in ('2026-11-01T06:30:00+00:00', '2026-11-01T07:30:00+00:00'):
self.now[0] = datetime.fromisoformat(stamp).timestamp()
record['startTime'] = self.now[0]
self.assertEqual(owner.snapshot('America/Chicago')['week']['days'][-1]['distanceMeters'], 1000)
for zone in ('../UTC', '/etc/passwd', 'Unknown/Zone', '', 'x' * 129):
with self.subTest(zone=zone), self.assertRaises(ValueError):
owner.snapshot(zone)
def test_metric_preference_changes_presentation_flag_only(self):
owner = self.owner(metric=lambda: False)
owner._start()
owner.snapshot()
output = self.completed(owner)
self.assertFalse(output['isMetric'])
self.assertEqual(output['totals']['distanceMeters'], 1000)
def test_personal_records_share_the_viewer_calendar_with_week_graph(self):
owner = self.owner(permitted=lambda: False)
self.now[0] = datetime(2026, 9, 28, 6, tzinfo=UTC).timestamp()
for identifier, stamp, distance, engaged in (
('sunday', '2026-09-28T04:30:00+00:00', 1000, 60),
('monday', '2026-09-28T05:30:00+00:00', 2000, 0),
):
record = analyzed(self.root, route(identifier), lambda: True)
start = datetime.fromisoformat(stamp).timestamp()
record.update(startTime=start, endTime=start + 60, ignored=False,
distanceMeters=distance, engagedSeconds=engaged, engagedPercent=100 * engaged / 60)
owner._records[identifier] = record
saved = copy.deepcopy(owner._records)
local = owner.snapshot('America/Chicago')
records = {record['id']: record['value'] for record in local['records']}
self.assertEqual(local['week']['distanceMeters'], 2000)
self.assertEqual(local['week']['days'][0]['distanceMeters'], 2000)
self.assertEqual(records['bestWeek'], 2000)
self.assertEqual(records['mostEngagedDay'], 100)
self.assertEqual(records['highestStreak'], 2)
self.assertEqual(local['coverage']['timezone'], local['week']['timezone'])
utc = owner.snapshot()
records = {record['id']: record['value'] for record in utc['records']}
self.assertEqual(utc['week']['distanceMeters'], 3000)
self.assertEqual(records['bestWeek'], 3000)
self.assertEqual(records['mostEngagedDay'], 50)
self.assertEqual(records['highestStreak'], 1)
self.assertEqual(utc['coverage']['timezone'], 'UTC')
self.assertEqual(local['totals'], utc['totals'])
self.assertEqual(owner._records, saved)
self.assertFalse(self.store.exists(), 'viewer timezone must not rewrite stored driving history')
def test_personal_record_streak_counts_calendar_days_across_dst(self):
owner = self.owner(permitted=lambda: False)
for stamps in (
('2026-03-07T18:00:00+00:00', '2026-03-08T17:00:00+00:00', '2026-03-09T17:00:00+00:00'),
('2026-10-31T17:00:00+00:00', '2026-11-01T18:00:00+00:00', '2026-11-02T18:00:00+00:00'),
):
with self.subTest(stamps=stamps):
owner._records.clear()
for index, stamp in enumerate(stamps):
record = analyzed(self.root, route(str(index)), lambda: True)
start = datetime.fromisoformat(stamp).timestamp()
record.update(startTime=start, endTime=start + 60, ignored=False)
owner._records[str(index)] = record
records = {record['id']: record['value'] for record in owner.snapshot('America/Chicago')['records']}
self.assertEqual(records['highestStreak'], 3)
def test_repeated_dst_hour_is_one_record_day(self):
owner = self.owner(permitted=lambda: False)
self.now[0] = datetime(2026, 11, 1, 8, tzinfo=UTC).timestamp()
for index, stamp in enumerate(('2026-11-01T06:30:00+00:00', '2026-11-01T07:30:00+00:00')):
record = analyzed(self.root, route(str(index)), lambda: True)
start = datetime.fromisoformat(stamp).timestamp()
record.update(startTime=start, endTime=start + 60, ignored=False, engagedSeconds=60 * index,
engagedPercent=100 * index)
owner._records[str(index)] = record
local = owner.snapshot('America/Chicago')
records = {record['id']: record['value'] for record in local['records']}
self.assertEqual(local['week']['days'][-1]['distanceMeters'], 2000)
self.assertEqual(records['highestStreak'], 1)
self.assertEqual(records['mostEngagedDay'], 50)
def test_default_store_inventory_uses_bounded_long_route_limit(self):
owner = DriveStatsOwner(root=self.root, store=self.store, analyzer=analyzed, permitted=lambda: False)
self.addCleanup(owner.close)
self.assertEqual(owner.history.max_segments_per_route, 512)
self.assertEqual(owner.history.max_segments, 2048)
def test_cleaned_logs_retain_complete_records_and_changed_logs_invalidate(self):
segment = self.root / 'abc--0'
segment.mkdir()
log = segment / 'rlog.zst'
log.write_bytes(b'first')
owner = self.owner()
owner._start()
owner.snapshot()
self.completed(owner)
self.history.routes = []
self.now[0] += 61
owner._start()
owner.snapshot()
self.assertEqual(self.completed(owner, scans=2)['totals']['drives'], 1)
self.history.routes = [route('abc')]
log.write_bytes(b'changed')
analyzing = threading.Event()
release = threading.Event()
def delayed(root, selected, permitted):
analyzing.set()
release.wait(2)
return analyzed(root, selected, permitted)
owner.analyzer = delayed
self.now[0] += 61
owner._start()
owner.snapshot()
self.assertTrue(analyzing.wait(2))
self.assertEqual(owner.snapshot()['totals']['drives'], 0)
release.set()
self.assertEqual(self.completed(owner, scans=3)['totals']['drives'], 1)
def test_incomplete_and_failed_routes_never_contribute(self):
def incomplete(root, selected, permitted):
result = analyzed(root, selected, permitted)
result.update(complete=False, reason='gap', distanceMeters=None, engagedSeconds=None)
return result
owner = self.owner(analyzer=incomplete)
owner._start()
owner.snapshot()
result = self.completed(owner)
self.assertEqual(result['totals']['drives'], 0)
self.assertEqual(result['coverage']['incompleteRoutes'], 1)
self.assertFalse(result['recentDrives'][0]['complete'])
self.assertEqual(result['records'], [])
self.assertTrue(result['coverage']['partialHistory'])
def test_log_cleanup_keeps_completed_drive_and_qlog_change_retries(self):
segment = self.root / 'abc--0'
segment.mkdir()
rlog = segment / 'rlog.zst'
qlog = segment / 'qlog.zst'
rlog.write_bytes(b'rlog')
qlog.write_bytes(b'qlog')
calls = []
def count(root, selected, permitted):
calls.append(1)
return analyzed(root, selected, permitted)
owner = self.owner(analyzer=count)
owner._start()
owner.snapshot()
self.completed(owner)
rlog.unlink()
self.now[0] += 61
owner._start()
owner.snapshot()
self.assertEqual(self.completed(owner, scans=2)['totals']['drives'], 1)
self.assertEqual(len(calls), 1)
qlog.write_bytes(b'changed qlog')
self.now[0] += 61
owner._start()
owner.snapshot()
self.assertEqual(self.completed(owner, scans=3)['totals']['drives'], 1)
self.assertEqual(len(calls), 2)
def test_attention_records_use_independent_observations(self):
def attention(root, selected, permitted):
result = analyzed(root, selected, permitted)
result.update(distractedMoments=2, unresponsiveMoments=0)
return result
owner = self.owner(analyzer=attention)
owner._start()
owner.snapshot()
records = {record['id']: record for record in self.completed(owner)['records']}
self.assertEqual(records['cleanDriveStreak']['value'], 1)
self.assertIsNone(records['longestUndistractedDrive']['value'])
def test_last_drive_can_be_incomplete_without_inflating_totals(self):
self.history.routes = [route('old'), route('new')]
def mixed(root, selected, permitted):
result = analyzed(root, selected, permitted)
if selected['routeId'] == 'old':
result.update(startTime=NOW - 3600, endTime=NOW - 3540)
else:
result.update(complete=False, reason='gap', distanceMeters=None, engagedSeconds=None)
return result
owner = self.owner(analyzer=mixed)
owner._start()
owner.snapshot()
output = self.completed(owner)
self.assertEqual(output['totals']['drives'], 1)
self.assertEqual(output['lastDrive']['routeId'], 'new')
self.assertFalse(output['lastDrive']['complete'])
def test_parked_check_is_throttled_and_close_is_immediate(self):
ticks = [100.0]
calls = []
def parked():
calls.append(1)
return True
owner = self.owner(permitted=parked, monotonic=lambda: ticks[0])
self.assertTrue(owner._allowed())
for _ in range(100):
self.assertTrue(owner._allowed())
self.assertEqual(len(calls), 1)
ticks[0] += .11
self.assertTrue(owner._allowed())
self.assertEqual(len(calls), 2)
owner.close()
self.assertFalse(owner._allowed())
self.assertEqual(len(calls), 2)
def test_backward_wall_clock_does_not_freeze_scan(self):
ticks = [100.0]
owner = self.owner(monotonic=lambda: ticks[0])
owner._start()
owner.snapshot()
self.completed(owner)
self.now[0] -= 86400
ticks[0] += 61
owner._start()
owner.snapshot()
self.completed(owner, scans=2)
def test_failed_cache_write_rolls_back_ignore(self):
owner = self.owner()
owner._start()
owner.snapshot()
self.completed(owner)
with mock.patch.object(owner, '_save', side_effect=OSError('full disk')):
with self.assertRaises(OSError):
owner.ignore('abc', True, lambda: True)
self.assertFalse(owner.snapshot()['recentDrives'][0]['ignored'])
def test_background_work_is_bounded_and_resumes_other_routes(self):
self.history.routes = [route(f'route-{index}') for index in range(5)]
owner = self.owner()
owner._start()
owner.snapshot()
first = self.completed(owner)
self.assertEqual(first['totals']['drives'], 2)
self.assertEqual(first['analysis']['pending'], 3)
self.now[0] += 61
owner._start()
owner.snapshot()
self.assertEqual(self.completed(owner, scans=2)['totals']['drives'], 4)
self.now[0] += 61
owner._start()
owner.snapshot()
self.assertEqual(self.completed(owner, scans=3)['totals']['drives'], 5)
def test_incomplete_source_waits_for_retry_or_revision(self):
calls = []
def incomplete(root, selected, permitted):
calls.append(1)
result = analyzed(root, selected, permitted)
result.update(complete=False, reason='gap')
return result
owner = self.owner(analyzer=incomplete)
owner._start()
owner.snapshot()
result = self.completed(owner)
self.assertEqual(result['analysis']['pending'], 0)
self.assertEqual(result['analysis']['state'], 'retrying')
self.assertFalse(result['analysis']['running'])
self.now[0] += 61
owner._start()
owner.snapshot()
self.completed(owner, scans=2)
self.assertEqual(len(calls), 1)
self.now[0] += 300
owner.analyzer = analyzed
owner._start()
owner.snapshot()
self.assertEqual(self.completed(owner, scans=3)['totals']['drives'], 1)
def test_exhausted_failure_recovers_once_after_analyzer_upgrade(self):
owner = self.owner(analyzer=lambda *args, **kwargs: {**analyzed(*args, **kwargs),
'complete': False, 'reason': 'End of route was not recorded'})
owner._start()
owner.snapshot()
self.completed(owner)
owner._failures['abc']['attempts'] = drive_stats.MAX_RETRY_ATTEMPTS
owner._save()
self.now[0] += 61
owner = self.owner(analyzer=mock.Mock(wraps=analyzed))
owner._start()
owner.snapshot()
paused = self.completed(owner)
owner.analyzer.assert_not_called()
self.assertEqual(paused['analysis']['state'], 'failed')
self.assertTrue(paused['analysis']['failures'][0]['retryStopped'])
self.assertEqual(paused['analysis']['failures'][0]['reason'], 'End of route was not recorded')
self.assertEqual(paused['totals']['drives'], 0)
self.now[0] += 61
with mock.patch.object(drive_stats, 'ANALYSIS_VERSION', drive_stats.ANALYSIS_VERSION + 1):
owner._start()
owner.snapshot()
recovered = self.completed(owner, scans=3)
owner.analyzer.assert_called_once()
self.assertEqual(recovered['analysis']['failed'], 0)
self.assertEqual(recovered['totals']['drives'], 1)
def test_legacy_failure_gets_fresh_bounded_attempts(self):
owner = self.owner(analyzer=lambda *args, **kwargs: {**analyzed(*args, **kwargs),
'complete': False, 'reason': 'gap'})
owner._start()
owner.snapshot()
self.completed(owner)
failure = owner._failures['abc']
failure.pop('analysisVersion')
failure['attempts'] = drive_stats.MAX_RETRY_ATTEMPTS
failure['firstAt'] = self.now[0] - drive_stats.MAX_RETRY_AGE_SECONDS
owner._save()
owner = self.owner(analyzer=owner.analyzer)
self.now[0] += 61
owner._start()
owner.snapshot()
result = self.completed(owner, scans=2)
self.assertEqual(result['analysis']['state'], 'retrying')
self.assertEqual(owner._failures['abc']['attempts'], 1)
self.assertEqual(owner._failures['abc']['analysisVersion'], drive_stats.ANALYSIS_VERSION)
self.now[0] += 61
owner._start()
owner.snapshot()
self.completed(owner, scans=3)
self.assertEqual(owner._failures['abc']['attempts'], 1)
def test_failed_analysis_leaves_queue_and_retries_after_cooldown(self):
owner = self.owner(analyzer=mock.Mock(side_effect=TimeoutError('bounded route timeout')))
owner._start()
owner.snapshot()
result = self.completed(owner)
self.assertEqual(result['analysis']['pending'], 0)
self.assertEqual(result['analysis']['failed'], 1)
self.assertEqual(result['analysis']['state'], 'retrying')
self.now[0] += 61
owner._start()
owner.snapshot()
self.completed(owner, scans=2)
self.assertEqual(owner.analyzer.call_count, 1)
owner.analyzer = analyzed
self.now[0] += 300
owner._start()
owner.snapshot()
result = self.completed(owner, scans=3)
self.assertEqual(result['analysis']['state'], 'ready')
self.assertEqual(result['analysis']['failed'], 0)
def test_analysis_upgrade_refreshes_retained_logs_but_preserves_deleted_history(self):
segment = self.root / 'abc--0'
segment.mkdir()
log = segment / 'rlog.zst'
log.write_bytes(b'original source')
owner = self.owner()
owner._start()
owner.snapshot()
self.completed(owner)
owner._records['abc'].update(analysisVersion=1, model=None)
owner._save()
self.now[0] += 61
owner._start()
owner.snapshot()
result = self.completed(owner, scans=2)
self.assertEqual(result['lastDrive']['model'], 'Test Model')
self.assertEqual(result['lastDrive']['analysisVersion'], drive_stats.ANALYSIS_VERSION)
owner._records['abc']['analysisVersion'] = 1
log.unlink()
self.now[0] += 61
owner.analyzer = mock.Mock(side_effect=AssertionError('deleted source must retain prior measurements'))
owner._start()
owner.snapshot()
result = self.completed(owner, scans=3)
self.assertEqual(result['totals']['distanceMeters'], 1000)
owner.analyzer.assert_not_called()
def test_metadata_upgrade_failure_preserves_previous_complete_totals(self):
owner = self.owner()
owner._start()
owner.snapshot()
self.completed(owner)
owner._records['abc']['analysisVersion'] = 1
owner.analyzer = lambda *args, **kwargs: {**analyzed(*args, **kwargs), 'complete': False, 'reason': 'timeout'}
self.now[0] += 61
owner._start()
owner.snapshot()
result = self.completed(owner, scans=2)
self.assertEqual(result['totals']['distanceMeters'], 1000)
self.assertEqual(result['analysis']['state'], 'retrying')
def test_offroad_wait_is_distinct_from_running_analysis(self):
result = self.owner(permitted=lambda: False).snapshot()
self.assertEqual(result['analysis']['state'], 'waitingParked')
self.assertFalse(result['analysis']['running'])
def test_byte_limit_evicts_oldest_with_partial_coverage(self):
owner = self.owner()
with owner._lock, mock.patch('openpilot.starpilot.galaxy.drive_stats.MAX_CACHE_BYTES', 2200):
for index in range(10):
record = analyzed(None, {'routeId': f'route-{index}'}, lambda: True)
record.update(ignored=False, sourceSignature='x', sourceManifest={}, model='x' * 120)
owner._records[record['routeId']] = record
owner._bound()
owner._save()
self.assertLess(len(owner._records), 10)
self.assertTrue(owner.snapshot()['coverage']['truncated'])
def test_corrupt_cache_not_replaced_until_valid_scan(self):
self.store.parent.mkdir(parents=True)
self.store.write_text('broken json')
permitted = [False]
owner = self.owner(permitted=lambda: permitted[0])
self.assertTrue(owner.snapshot()['coverage']['cacheCorrupt'])
self.assertEqual(self.store.read_text(), 'broken json')
permitted[0] = True
self.now[0] += .11
owner._start()
owner.snapshot()
result = self.completed(owner)
self.assertFalse(result['coverage']['cacheCorrupt'])
self.assertEqual(json.loads(self.store.read_text())['schemaVersion'], 1)
def test_oversized_number_in_cache_is_treated_as_corrupt(self):
self.store.parent.mkdir(parents=True)
record = analyzed(None, {'routeId': 'abc'}, lambda: True)
record.update(ignored=False, sourceSignature='x', sourceManifest={}, distanceMeters=10**400)
self.store.write_text(json.dumps({'schemaVersion': 1, 'routes': {'abc': record}}))
owner = self.owner(permitted=lambda: False)
result = owner.snapshot()
self.assertTrue(result['coverage']['cacheCorrupt'])
self.assertEqual(result['totals']['drives'], 0)
self.assertIn('abc', self.store.read_text())
def test_changed_source_during_analysis_cannot_commit_stale_result(self):
segment = self.root / 'abc--0'
segment.mkdir()
log = segment / 'qlog.zst'
log.write_bytes(b'initial')
def changes_log(root, selected, permitted):
log.write_bytes(b'replaced while decoding')
return analyzed(root, selected, permitted)
owner = self.owner(analyzer=changes_log)
owner._start()
owner.snapshot()
first = self.completed(owner)
self.assertEqual(first['totals']['drives'], 0)
self.assertEqual(first['analysis']['failed'], 1)
owner.analyzer = analyzed
self.now[0] += 61
owner._start()
owner.snapshot()
self.assertEqual(self.completed(owner, scans=2)['totals']['drives'], 1)
def test_cancel_prevents_late_commit_and_storage_is_bounded(self):
entered = threading.Event()
release = threading.Event()
def blocked(root, selected, permitted):
entered.set()
release.wait(2)
return analyzed(root, selected, permitted)
owner = self.owner(analyzer=blocked)
owner._start()
owner.snapshot()
self.assertTrue(entered.wait(2))
owner.close()
release.set()
self.assertFalse(self.store.exists())
bounded = self.owner()
with bounded._lock:
for index in range(MAX_CACHED_ROUTES + 1):
record = analyzed(None, {'routeId': f'route-{index}'}, lambda: True)
record.update(ignored=False, sourceSignature='x')
bounded._records[record['routeId']] = record
bounded._bound()
self.assertEqual(len(bounded._records), MAX_CACHED_ROUTES)
self.assertTrue(bounded._truncated)
if __name__ == '__main__':
unittest.main()