"""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 #include #include #include #include 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, player_mq_couter, vad_starts; 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 vad_start_func(void) { vad_starts++; recording = 1; g_mic_working = 0; } 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', 'conversation_accept_voice_message', 'conversation_finish_startup_prompt', 'conversation_find_session_id']: 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; player_mq_couter = vad_starts = 0; active_session_valid = 0; startup_start_ms = 0; tts_playing = 0; } int main(void) { char id[33]; const char *tagged = "PROMPT_SOUND_URL:http://host/26101309423304_EFGHIJKLMNOPQRSTUV.mp3"; const char *plain = "PROMPT_SOUND_URL:http://host/tts_error.mp3"; assert(conversation_find_session_id(tagged, (unsigned int)strlen(tagged), id)); assert(strcmp(id, "26101309423304EFGHIJKLMNOPQRSTUV") == 0); assert(!conversation_find_session_id(plain, (unsigned int)strlen(plain), id)); assert(!conversation_find_session_id(tagged, 20, id)); /* 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); /* Reproduce ai_active followed by a late untagged backend error. */ reset(); active_session_valid = 1; active_session_round = 0; assert(conversation_try_begin() == 0); assert(!conversation_accept_voice_message("PROMPT_SOUND_URL:http://host/tts_error.mp3")); assert(!conversation_accept_voice_message("TTS_START")); assert(conversation_accept_voice_message("NFC_SOUND_URL:http://host/card.mp3")); /* A following queue entry cannot consume the startup action. */ player_mq_couter = 1; conversation_finish_startup_prompt(conversation_round, 0); assert(vad_starts == 1 && recording && !g_mic_working); assert(!conversation_accept_voice_message("PROMPT_SOUND_URL:http://host/tts_error.mp3")); active_session_valid = 1; active_session_round = conversation_round; assert(conversation_accept_voice_message("PROMPT_SOUND_URL:http://host/tts_error.mp3")); /* Stale prompt completion must not start another round. */ reset(); assert(conversation_try_begin() == 0); conversation_finish_startup_prompt(conversation_round - 1, 0); assert(!vad_starts); conversation_finish_startup_prompt(conversation_round, -1); assert(vad_starts == 1); /* Lost startup releases reservation; active work is never recovered. */ reset(); assert(conversation_try_begin() == 0); now += 30001; conversation_check_timeout(); assert(!g_mic_working); assert(conversation_try_begin() == 0); now += 30001; player_mq_couter = 1; conversation_check_timeout(); assert(g_mic_working); player_mq_couter = 0; recording = 1; conversation_check_timeout(); assert(g_mic_working); puts("PASS: late backend error admission, prompt completion with queued audio, stale prompt, failed playback, stalled-startup recovery; 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)