Files
banban/tools/test_voice_diag.py
T
chenjintao db6109172c feat: add device diagnostics for missing voice replies
Track microphone levels, short reads and PCM buffer drops. Report audio
queue admission separately from TLS writes, expose server replies and
ACKs before filtering, and capture per-round counters before timeouts.

Add diagnostic regression coverage and support POSIX hosts in WSS tests.
2026-10-26 18:35:24 +08:00

113 lines
5.3 KiB
Python

"""Compile actual microphone, queue and receive diagnostics with scripted inputs."""
from pathlib import Path
import subprocess
import tempfile
root = Path(__file__).resolve().parents[1]
app = root / 'ql-application/threadx/interface/custom_app'
voice = (app / 'ws_talkingq.c').read_text()
mic = (app / 'vad_det.c').read_text()
def function(source, signature):
start = source.index(signature)
opening = source.index('{', start)
depth, end = 1, opening + 1
while depth:
depth += (source[end] == '{') - (source[end] == '}')
end += 1
return source[start:end] + '\n'
harness = r'''
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define VAD_FRAME_BYTES 640
#define DEVICE_ID_SIZE 32
#define SESSION_ID_SIZE 33
#define RLOGE(...) ((void)0)
typedef int rws_socket;
static int g_mic_working;
static int g_state, ring_result = 640, fail_alloc, queue_result, dispatched;
static unsigned int conversation_round = 7;
static int active_session_valid = 1;
static char sessionId[33] = "session", deviceId[32] = "device";
static void *ring_sender;
static uint64_t now = 1000;
static int mutex_locked;
static uint64_t hal_os_up_time_ms(void) { return now; }
static void conversation_lock(void) { assert(!mutex_locked); mutex_locked = 1; }
static void conversation_unlock(void) { assert(mutex_locked); mutex_locked = 0; }
static int hal_os_rb_write(void *ring, const void *data, int len, unsigned int timeout) {
(void)ring; (void)data; (void)len; assert(timeout == 25); return ring_result;
}
static void *test_malloc(size_t size) { return fail_alloc ? NULL : malloc(size); }
static int talkingq_ws_send_bin(uint8_t *data, int len) {
uint32_t seq;
assert(len >= DEVICE_ID_SIZE + SESSION_ID_SIZE + 12);
memcpy(&seq, data + DEVICE_ID_SIZE + SESSION_ID_SIZE, 4);
assert(seq == 9);
return queue_result;
}
typedef struct { uint32_t sequenceNumber, packetType, dataSize; uint8_t *data; } AudioPacket;
static int conversation_accept_voice_message(const char *text) { return strstr(text, "old") == NULL; }
static void conversation_log_error_message(const char *text, unsigned int len) { (void)text; (void)len; }
static void onmessage_text_locked(rws_socket socket, const char *text, unsigned int len) {
(void)socket; (void)text; (void)len; assert(mutex_locked); dispatched++;
}
'''
start = voice.index('static struct {')
end = voice.index('} voice_diag;', start) + len('} voice_diag;')
harness += voice[start:end] + '\n'
start = mic.index('static struct {')
end = mic.index('extern int audio_packet_type;', start)
harness += mic[start:end]
harness += '#define malloc test_malloc\n'
harness += function(voice, 'int sendRecordingAudioPacket(AudioPacket *packet) {')
harness += '#undef malloc\n'
harness += function(voice, 'static void onmessage_text(rws_socket socket,')
harness += function(voice, 'static void onmessage_bin(rws_socket socket,')
harness += r'''
int main(void) {
uint8_t pcm[640] = {0};
AudioPacket packet = {9, 0, 4, pcm};
mic_diag.started_ms = now;
mic_diag_read(pcm, 640, 0);
assert(mic_diag.reads == 1 && mic_diag.zero_frames == 1 && mic_diag.peak == 0);
/* Full-scale negative input must avoid int16 absolute-value overflow. */
pcm[0] = 0; pcm[1] = 128;
mic_diag_read(pcm, 640, 0);
assert(mic_diag.peak == 32768 && mic_diag.abs_sum == 32768 && mic_diag.samples == 640);
mic_diag_read(pcm, 0, -1);
assert(mic_diag.short_reads == 1 && mic_diag.read_errors == 1 && mic_diag.zero_frames == 1);
mic_diag_read(pcm, 10000, 0); /* Diagnostic sampling stays inside the buffer. */
assert(mic_diag.samples == 960 && mic_diag.short_reads == 2);
ring_result = 640; assert(mic_diag_write(pcm, 640) == 640 && !mic_diag.ring_drops);
ring_result = -1; assert(mic_diag_write(pcm, 640) == -1);
ring_result = 320; assert(mic_diag_write(pcm, 640) == 320);
assert(mic_diag.ring_drops == 2 && mic_diag.ring_drop_bytes == 960);
mic_diag_log("test");
queue_result = 0; assert(sendRecordingAudioPacket(&packet) == 0);
assert(voice_diag.tx_packets == 1 && voice_diag.tx_bytes == 4 && !voice_diag.tx_failed);
queue_result = -1; assert(sendRecordingAudioPacket(&packet) == -1);
fail_alloc = 1; assert(sendRecordingAudioPacket(&packet) == -1);
assert(voice_diag.tx_packets == 1 && voice_diag.tx_bytes == 4 && voice_diag.tx_failed == 2);
onmessage_text(0, "START_ACK", 9);
onmessage_text(0, "END_ACK", 7);
onmessage_text(0, "unknown backend response", 24);
onmessage_text(0, "old TTS_START", 13);
assert(voice_diag.start_ack == 1 && voice_diag.end_ack == 1 && voice_diag.rx_text == 4);
assert(dispatched == 3 && !mutex_locked);
now = 2000; onmessage_bin(0, pcm, 640);
assert(voice_diag.rx_bin == 1 && voice_diag.last_rx_ms == now && !mutex_locked);
puts("PASS: microphone silence/level/short reads/errors, PCM drops, queue failure/allocation failure, ACK visibility, unknown/rejected replies, ignored binary replies");
}
'''
with tempfile.TemporaryDirectory(prefix='banban-voice-diag-') as directory:
cfile = Path(directory) / 'diag.c'
exe = Path(directory) / 'diag-test'
cfile.write_text(harness)
subprocess.run(['cc', '-std=c99', '-Wall', '-Wextra', '-Werror', '-fsanitize=address,undefined', str(cfile), '-o', str(exe)], check=True)
subprocess.run([str(exe)], check=True)