mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
147 lines
6.3 KiB
Python
147 lines
6.3 KiB
Python
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
from openpilot.starpilot.audio.native_playback import NativePlayback
|
|
|
|
|
|
def playback(*, frozen=1, count=21, words=None, underflows=1, clock_errors=0):
|
|
obj = NativePlayback.__new__(NativePlayback)
|
|
obj.owner = SimpleNamespace(output_underflow_count=underflows)
|
|
obj.timing_reported = False
|
|
obj.handle, obj.rate, obj.buffer = 1, 48000, 4096
|
|
calls = []
|
|
def read(handle, values, capacity):
|
|
calls.append((handle, capacity))
|
|
header = (1, 22, 32, 25, 21, frozen, 5, count, 0, clock_errors)
|
|
for index, value in enumerate(header):
|
|
values[index] = value
|
|
for row in range(min(count, 32)):
|
|
values[10 + row * 22] = 5 + row
|
|
return 10 + 22 * count if words is None else words
|
|
obj.timing_getter = read
|
|
return obj, calls
|
|
|
|
|
|
def test_healthy_or_old_native_library_does_not_poll_or_log():
|
|
obj, calls = playback(underflows=0)
|
|
with patch('openpilot.common.swaglog.cloudlog.error') as error:
|
|
obj.report_underflow_timing()
|
|
obj.owner.output_underflow_count = 1
|
|
obj.timing_getter = None
|
|
obj.report_underflow_timing()
|
|
assert not calls
|
|
error.assert_not_called()
|
|
|
|
|
|
def test_waits_for_recovery_then_logs_one_bounded_trace():
|
|
obj, calls = playback(frozen=0)
|
|
with patch('openpilot.common.swaglog.cloudlog.error') as error:
|
|
obj.report_underflow_timing()
|
|
error.assert_not_called()
|
|
assert not obj.timing_reported
|
|
obj.timing_getter = playback()[0].timing_getter
|
|
obj.report_underflow_timing()
|
|
obj.report_underflow_timing()
|
|
assert len(calls) == 1
|
|
error.assert_called_once()
|
|
trace = error.call_args.args[0]
|
|
assert trace['event'] == 'soundd_underflow_timing' and trace['schema'] == 1
|
|
assert trace['sample_rate'] == 48000 and trace['block_frames'] == 4096
|
|
assert len(trace['columns']) == 22 and trace['columns'][1:5] == ('entry_mono_ns', 'entry_cpu_ns', 'exit_mono_ns', 'exit_cpu_ns')
|
|
assert len(trace['header_columns']) == 10 and trace['header_columns'][8] == 'callback_aborts'
|
|
assert trace['header'] == [1, 22, 32, 25, 21, 1, 5, 21, 0, 0]
|
|
assert len(trace['rows']) == 21 and all(len(row) == 22 for row in trace['rows'])
|
|
assert [row[0] for row in trace['rows']] == list(range(5, 26))
|
|
|
|
|
|
def test_unavailable_snapshot_retries_and_clock_errors_remain_explicit():
|
|
obj, calls = playback(words=0)
|
|
with patch('openpilot.common.swaglog.cloudlog.error') as error:
|
|
obj.report_underflow_timing()
|
|
assert not obj.timing_reported
|
|
obj.timing_getter = playback(clock_errors=3)[0].timing_getter
|
|
obj.report_underflow_timing()
|
|
assert len(calls) == 1
|
|
assert error.call_args.args[0]['header'][9] == 3
|
|
|
|
|
|
def test_invalid_native_metadata_is_an_error_without_unbounded_rows():
|
|
for count, words in ((33, 736), (21, 999)):
|
|
obj, _ = playback(count=count, words=words)
|
|
with patch('openpilot.common.swaglog.cloudlog.error') as error:
|
|
obj.report_underflow_timing()
|
|
obj.report_underflow_timing()
|
|
error.assert_called_once_with({'event': 'soundd_underflow_timing', 'error': 'invalid_native_diagnostic'})
|
|
|
|
|
|
def test_closing_stream_preserves_partial_underflow_trace():
|
|
obj, _ = playback(frozen=0)
|
|
with patch('openpilot.common.swaglog.cloudlog.error') as error:
|
|
obj.report_underflow_timing(closing=True)
|
|
error.assert_called_once()
|
|
assert error.call_args.args[0]['header'][5] == 0
|
|
assert error.call_args.args[0]['rows']
|
|
|
|
|
|
def test_closing_stream_reports_native_abort_without_claiming_an_underflow():
|
|
obj, _ = playback(frozen=0, underflows=0)
|
|
read = obj.timing_getter
|
|
def aborted(handle, values, capacity):
|
|
words = read(handle, values, capacity)
|
|
values[4], values[8] = 0, 1
|
|
return words
|
|
obj.timing_getter = aborted
|
|
with patch('openpilot.common.swaglog.cloudlog.error') as error:
|
|
obj.report_underflow_timing(closing=True)
|
|
header = error.call_args.args[0]['header']
|
|
assert header[4] == 0 and header[8] == 1
|
|
|
|
|
|
def test_compiled_native_trace_reaches_service_logger_with_exact_pcm(tmp_path):
|
|
import ctypes
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
source = Path(__file__).parents[1] / 'native/audio_mixer.c'
|
|
library = tmp_path / 'mixer.so'
|
|
subprocess.run(['cc', '-std=c11', '-O2', '-shared', '-fPIC', str(source), '-o', str(library)],
|
|
check=True, capture_output=True, timeout=15)
|
|
lib = ctypes.CDLL(str(library))
|
|
lib.audio_create.restype = ctypes.c_void_p
|
|
lib.audio_destroy.argtypes = [ctypes.c_void_p]
|
|
lib.audio_callback_address.restype = ctypes.c_size_t
|
|
lib.audio_publish_bank.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.POINTER(ctypes.c_float)),
|
|
ctypes.POINTER(ctypes.c_uint), ctypes.c_uint, ctypes.c_uint64]
|
|
lib.audio_command.argtypes = [ctypes.c_void_p, ctypes.c_uint, ctypes.c_uint, ctypes.c_int, ctypes.c_int,
|
|
ctypes.c_float, ctypes.c_uint64, ctypes.c_uint64, ctypes.c_uint64]
|
|
lib.audio_prepare.argtypes = [ctypes.c_void_p]
|
|
lib.audio_diagnostic_snapshot.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint64), ctypes.c_uint]
|
|
lib.audio_diagnostic_snapshot.restype = ctypes.c_uint
|
|
obj, _ = playback()
|
|
obj.handle = lib.audio_create()
|
|
assert obj.handle
|
|
obj.timing_getter = lib.audio_diagnostic_snapshot
|
|
samples = (ctypes.c_float * 4)(.25, -.5, .75, -1.)
|
|
bank = (ctypes.POINTER(ctypes.c_float) * 1)(samples)
|
|
lengths = (ctypes.c_uint * 1)(4)
|
|
output = (ctypes.c_float * 4)()
|
|
callback = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_ulong,
|
|
ctypes.c_void_p, ctypes.c_ulong, ctypes.c_void_p)(lib.audio_callback_address())
|
|
try:
|
|
assert lib.audio_publish_bank(obj.handle, bank, lengths, 1, 1)
|
|
assert lib.audio_command(obj.handle, 1, 0, -1, 0, .5, 1, 1, 0)
|
|
assert lib.audio_prepare(obj.handle)
|
|
with patch('openpilot.common.swaglog.cloudlog.error') as error:
|
|
for index in range(25):
|
|
assert callback(None, output, 4, None, 4 if index == 20 else 0, obj.handle) == 0
|
|
assert list(output) == [.125, -.25, .375, -.5]
|
|
obj.report_underflow_timing()
|
|
obj.report_underflow_timing()
|
|
error.assert_called_once()
|
|
trace = error.call_args.args[0]
|
|
assert trace['header'][:8] == [1, 22, 32, 25, 21, 1, 5, 21]
|
|
assert trace['rows'][16][9] == 4 and trace['rows'][16][16] == 1
|
|
assert all(row[19] == 0 for row in trace['rows']) # Missing host clock metadata remains explicit.
|
|
finally:
|
|
lib.audio_destroy(obj.handle)
|