Files
banban/tools/test_conversation_timeout.py
T

106 lines
5.7 KiB
Python

"""Run firmware timeout/round functions on host with scripted RTOS and transport."""
from pathlib import Path
import subprocess
import tempfile
root = Path(__file__).resolve().parents[1]
source = (root / 'ql-application/threadx/interface/custom_app/ws_talkingq.c').read_text()
def function(name):
start = source.index(name + '(')
start = source.rfind('\n', 0, start) + 1
opening = source.index('{', start)
depth = 1
end = opening + 1
while depth:
depth += (source[end] == '{') - (source[end] == '}')
end += 1
return source[start:end] + '\n'
state_start = source.index('static ql_mutex_t conversation_mutex')
state_end = source.index('static volatile int player_mq_couter')
harness = r'''
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stddef.h>
typedef void *ql_mutex_t;
static int g_mic_working, recording, shine_init, fota_start;
static int tts_playing, tts_play_status, audio_packet_type, no_speech_timeout;
static int queued, disconnected, flags_reset, mutex_locked;
static uint64_t now;
static unsigned int gSequenceNumber;
static int end_result;
typedef struct { unsigned int sequenceNumber; int packetType, dataSize; } AudioPacket;
static int ql_rtos_mutex_lock(ql_mutex_t m, unsigned int t) {
(void)m; (void)t; assert(!mutex_locked); mutex_locked = 1; return 0;
}
static void ql_rtos_mutex_unlock(ql_mutex_t m) { (void)m; assert(mutex_locked); mutex_locked = 0; }
static int vad_audio_is_recording(void) { return recording; }
static uint64_t hal_os_up_time_ms(void) { return now; }
static void vad_set_speech_timeout_flag(int value) { no_speech_timeout = value; }
static void vad_reset_conversation_flags(void) { no_speech_timeout = 0; flags_reset++; }
static int build_random_tts_error_url(char *p, size_t n) { (void)p; (void)n; return 1; }
static void add_file_to_play_player_queue(char *p, int type) { (void)p; assert(type == 3); queued++; }
static void app_play_tips_file(int n) { (void)n; assert(0); }
static int conversation_try_begin(void);
static void talkingq_ws_disconnect(void) {
assert(!mutex_locked); disconnected++;
/* A key racing a committed timeout cannot start inside disconnect. */
assert(conversation_try_begin() == -1);
}
static int sendRecordingAudioPacket(AudioPacket *p) { assert(p->packetType == 2); return end_result; }
'''
harness += source[state_start:state_end]
for name in ['conversation_try_begin', 'sendEndPacket', 'conversation_check_timeout']:
harness += function(name)
harness += r'''
static void reset(void) {
g_mic_working = recording = shine_init = fota_start = 0;
conversation_round = wait_round = sender_round = 0;
conversation_wait = WAIT_NONE; timeout_action_busy = round_response_received = 0;
queued = disconnected = flags_reset = no_speech_timeout = mutex_locked = 0;
now = 1000; end_result = 0;
}
int main(void) {
/* No confirmed speech: prompt remains, without disconnect or reply wait. */
reset(); assert(conversation_try_begin() == 0); g_mic_working = 0;
conversation_no_speech(); conversation_check_timeout();
assert(queued == 1 && disconnected == 0 && no_speech_timeout == 1);
conversation_check_timeout(); assert(queued == 1);
/* New key cancels pending old no-speech prompt. */
reset(); conversation_no_speech(); assert(conversation_try_begin() == 0);
g_mic_working = 0; now += 20000; conversation_check_timeout();
assert(!queued && !disconnected && !no_speech_timeout);
/* Repeated keys during either waiting-for-speech or active speech are ignored. */
reset(); recording = 1; conversation_wait = WAIT_REPLY; wait_round = 0;
assert(conversation_try_begin() == -1);
assert(conversation_round == 0 && conversation_wait == WAIT_REPLY && !flags_reset);
/* END starts reply deadline; stale round and early URL cannot arm it. */
reset(); sendEndPacket(); assert(conversation_wait == WAIT_REPLY);
now += 5000; conversation_check_timeout(); assert(!queued);
now++; conversation_check_timeout(); assert(queued == 1 && disconnected == 1);
reset(); sendEndPacket(); assert(conversation_try_begin() == 0);
g_mic_working = 0; now += 20000; conversation_check_timeout(); assert(!queued && !disconnected);
reset(); conversation_round = 1; sendEndPacket(); assert(conversation_wait == WAIT_NONE);
reset(); round_response_received = 1; sendEndPacket(); assert(conversation_wait == WAIT_NONE);
/* END queue failure still produces an error instead of waiting forever. */
reset(); end_result = -1; sendEndPacket(); assert(conversation_wait == WAIT_SEND_FAILURE);
conversation_check_timeout(); assert(queued == 1 && disconnected == 1);
/* Prompt completion preserves the key-reserved round; automatic listening advances. */
reset(); assert(conversation_try_begin() == 0); conversation_record_begin(); assert(conversation_round == 1);
g_mic_working = 0; conversation_record_begin(); assert(conversation_round == 2);
/* Force-stop cancels waits; stale timeout owner cannot affect a new round. */
reset(); conversation_no_speech(); conversation_cancel_wait(); conversation_check_timeout(); assert(!queued);
reset(); conversation_wait = WAIT_REPLY; wait_round = 9; now += 20000;
conversation_check_timeout(); assert(!queued && !disconnected);
puts("PASS: no-speech prompt, new-round cancellation, ignored recording keys, END deadline, stale rounds, early reply, send failure, timeout/key race, auto-listening");
}
'''
with tempfile.TemporaryDirectory(prefix='banban-timeout-') as directory:
cfile = Path(directory) / 'timeout.c'
exe = Path(directory) / 'timeout-test'
cfile.write_text(harness)
subprocess.run(['cc', '-std=c99', '-Wall', '-Wextra', '-Werror', str(cfile), '-o', str(exe)], check=True)
subprocess.run([str(exe)], check=True)