Files
StarPilot/openpilot/starpilot/audio/tests/test_default_enrollment.py
T
firestar5683 fd4ca85d61 Audio
Provide alert playback and volume control with a native audio callback that does not depend on the Python GIL.

Co-authored-by: Daniel Koepping <elkoled@gmail.com>
2026-10-14 20:02:39 -05:00

189 lines
8.0 KiB
Python

from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import ast
import tempfile
import unittest
from unittest.mock import patch
from openpilot.starpilot.audio import default_enrollment as enrollment
class FilesystemParams:
def __init__(self, namespace):
self.namespace = namespace
def get_param_path(self):
return str(self.namespace)
class SoundDefaultEnrollmentTests(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name)
self.target = self.root / "params" / ".namespace"
self.target.mkdir(parents=True)
self.namespace = self.target.parent / "d"
self.namespace.symlink_to(self.target, target_is_directory=True)
self.params = FilesystemParams(self.namespace)
self.storage = self.root / "storage"
def selection(self, value):
if value is None:
(self.namespace / "SoundPack").unlink(missing_ok=True)
else:
(self.namespace / "SoundPack").write_bytes(value)
def enroll(self):
return enrollment.enroll_default_sounds(self.params, self.storage)
def marker(self):
return next((self.storage / "sound-default-once").iterdir())
def test_first_enrollment_replaces_all_selections_and_preserves_volumes(self):
for raw in (None, b"", b"default", b"stock", b"custom", b"starpilot", b"frog", b"\xff"):
with self.subTest(raw=raw), tempfile.TemporaryDirectory() as storage:
self.selection(raw)
volume = self.namespace / "EngageVolume"
volume.write_bytes(b"37")
self.assertTrue(enrollment.enroll_default_sounds(self.params, Path(storage)))
self.assertEqual((self.namespace / "SoundPack").read_bytes(), b"starpilot")
self.assertEqual(volume.read_bytes(), b"37")
def test_completed_enrollment_preserves_menu_optout_across_restarts(self):
self.enroll()
marker_bytes = self.marker().read_bytes()
for choice in (b"stock", b"custom", b"another-custom"):
self.selection(choice)
with patch.object(enrollment, "_write_selection", side_effect=AssertionError("repeat enrollment")):
for _ in range(3):
self.assertFalse(self.enroll())
self.assertEqual((self.namespace / "SoundPack").read_bytes(), choice)
self.assertEqual(self.marker().read_bytes(), marker_bytes)
def test_new_resolved_namespace_at_same_path_enrolls(self):
self.enroll()
self.selection(b"stock")
old_target = self.target
self.target = old_target.parent / ".fresh"
self.target.mkdir()
self.namespace.unlink()
self.namespace.symlink_to(self.target, target_is_directory=True)
self.selection(b"custom")
self.assertTrue(self.enroll())
self.assertEqual((self.namespace / "SoundPack").read_bytes(), b"starpilot")
self.assertEqual((old_target / "SoundPack").read_bytes(), b"stock")
def test_failed_pending_marker_does_not_change_selection_and_retries(self):
self.selection(b"stock")
with patch.object(enrollment, "_atomic_write", side_effect=OSError("disk full")):
with self.assertRaises(OSError):
self.enroll()
self.assertEqual((self.namespace / "SoundPack").read_bytes(), b"stock")
self.assertTrue(self.enroll())
def test_failed_selection_retries_without_marking_complete(self):
self.selection(b"custom")
with patch.object(enrollment, "_write_selection", side_effect=OSError("disk full")):
with self.assertRaises(OSError):
self.enroll()
self.assertIn(b'"pending"', self.marker().read_bytes())
self.assertEqual((self.namespace / "SoundPack").read_bytes(), b"custom")
self.assertTrue(self.enroll())
def test_failed_completion_retries_without_rewriting_soundpack(self):
self.selection(b"stock")
writer = enrollment._atomic_write
def fail_complete(path, raw):
if b'"complete"' in raw:
raise OSError("disk full")
writer(path, raw)
with patch.object(enrollment, "_atomic_write", side_effect=fail_complete):
with self.assertRaises(OSError):
self.enroll()
self.assertEqual((self.namespace / "SoundPack").read_bytes(), b"starpilot")
with patch.object(enrollment, "_write_selection", side_effect=AssertionError("already selected")):
self.assertTrue(self.enroll())
self.selection(b"stock")
self.assertFalse(self.enroll())
self.assertEqual((self.namespace / "SoundPack").read_bytes(), b"stock")
def test_changed_choice_during_pending_retry_is_preserved(self):
self.selection(b"stock")
with patch.object(enrollment, "_write_selection", side_effect=OSError("disk full")):
with self.assertRaises(OSError):
self.enroll()
self.selection(b"custom")
self.assertFalse(self.enroll())
self.assertEqual((self.namespace / "SoundPack").read_bytes(), b"custom")
self.assertFalse(self.enroll())
def test_concurrent_enrollments_only_select_once(self):
self.selection(b"custom")
with ThreadPoolExecutor(max_workers=8) as pool:
results = list(pool.map(lambda _: self.enroll(), range(16)))
self.assertEqual(results.count(True), 1)
self.assertEqual((self.namespace / "SoundPack").read_bytes(), b"starpilot")
def test_selection_stages_outside_namespace_and_cleans_failed_write(self):
self.selection(b"stock")
replace = enrollment.os.replace
observed = []
def fail_selection(source, destination):
if Path(destination) == self.namespace / "SoundPack":
observed.append(Path(source))
self.assertEqual(Path(source).parent, self.namespace.parent)
self.assertEqual({path.name for path in self.target.iterdir()}, {"SoundPack"})
raise OSError("replace failure")
replace(source, destination)
with patch.object(enrollment.os, "replace", side_effect=fail_selection):
with self.assertRaises(OSError):
self.enroll()
self.assertEqual(len(observed), 1)
self.assertFalse(observed[0].exists())
self.assertEqual({path.name for path in self.target.iterdir()}, {"SoundPack"})
self.assertTrue(self.enroll())
def test_corrupt_marker_preserves_optout(self):
self.enroll()
self.selection(b"stock")
self.marker().write_bytes(b"invalid")
with self.assertRaises(ValueError):
self.enroll()
self.assertEqual((self.namespace / "SoundPack").read_bytes(), b"stock")
def test_symlink_selection_is_not_followed_or_replaced(self):
external = self.root / "external"
external.write_bytes(b"custom")
(self.namespace / "SoundPack").symlink_to(external)
with self.assertRaises(OSError):
self.enroll()
self.assertEqual(external.read_bytes(), b"custom")
self.assertTrue((self.namespace / "SoundPack").is_symlink())
def test_manager_runs_after_admission_before_cleanup_and_warns_on_failure(self):
# Inspect the real startup call without importing its hardware/process graph.
manager = Path(enrollment.__file__).parents[2] / "system/manager/manager.py"
tree = ast.parse(manager.read_text())
init = next(node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == "manager_init")
calls = [node for node in ast.walk(init) if isinstance(node, ast.Call)]
def line(name):
return next(node.lineno for node in calls if isinstance(node.func, ast.Name) and node.func.id == name)
self.assertLess(line("prepare_manager_start"), line("enroll_default_sounds"))
cleanup = next(node.lineno for node in calls if isinstance(node.func, ast.Attribute) and node.func.attr == "clear_all")
self.assertLess(line("enroll_default_sounds"), cleanup)
guard = next(node for node in init.body if isinstance(node, ast.Try) and any(
isinstance(call, ast.Call) and isinstance(call.func, ast.Name) and call.func.id == "enroll_default_sounds" for call in ast.walk(node)))
assert guard.handlers[0].type is not None
self.assertEqual(ast.unparse(guard.handlers[0].type), "(OSError, ValueError)")
self.assertTrue(any(isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "warning"
for node in ast.walk(guard.handlers[0])))
if __name__ == "__main__":
unittest.main()