219 lines
13 KiB
Python
219 lines
13 KiB
Python
"""Run timeout, round and role-notice scheduling 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>
|
|
#include <string.h>
|
|
typedef void *ql_mutex_t;
|
|
static int g_mic_working, recording, shine_init, fota_start;
|
|
enum { STATE_IDLE, STATE_RECORDING_WAIT_SOUND, STATE_RECORDING_ACTIVE, STATE_WAITING_COMMAND };
|
|
static int tts_playing, tts_play_status, audio_packet_type, no_speech_timeout;
|
|
static int power_down_status, sleeping, vad_state;
|
|
static int queue_failure, last_queue_type;
|
|
static char last_queue_url[256];
|
|
static int get_sleep_mode(void) { return sleeping; }
|
|
static int vad_audio_get_state(void) { return vad_state; }
|
|
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 int add_file_to_play_player_queue(char *p, int type) {
|
|
assert(type == 3 || type == 5);
|
|
if (queue_failure) return -1;
|
|
queued++; player_mq_couter++; last_queue_type = type;
|
|
snprintf(last_queue_url, sizeof(last_queue_url), "%s", p);
|
|
return 0;
|
|
}
|
|
static void vad_start_func(void) { vad_starts++; recording = 1; vad_state = STATE_RECORDING_WAIT_SOUND; 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', 'role_notice_is_url', 'role_notice_start_locked', 'role_notice_poll_locked', 'app_role_notice_handle_url', 'app_role_notice_before_finish', 'role_notice_finished']:
|
|
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;
|
|
power_down_status = sleeping = queue_failure = 0; vad_state = STATE_IDLE;
|
|
role_notice_pending = role_notice_playing = role_notice_consumed = 0;
|
|
role_notice_round = 0; last_queue_type = 0; last_queue_url[0] = 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);
|
|
/* The exact resource alone is routed; unrelated notices retain original path. */
|
|
reset();
|
|
assert(!app_role_notice_handle_url("http://host/assets/audio/card.mp3"));
|
|
assert(!app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3.extra"));
|
|
assert(!role_notice_is_url("http://host/other/switch_role.mp3"));
|
|
assert(!queued);
|
|
/* No speech: retain one notice until tts_error + optional local fallback ends. */
|
|
reset(); recording = 1; vad_state = STATE_RECORDING_WAIT_SOUND;
|
|
assert(app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3"));
|
|
assert(!queued && role_notice_pending);
|
|
recording = 0; vad_state = STATE_WAITING_COMMAND; conversation_wait = WAIT_NO_SPEECH;
|
|
assert(app_role_notice_before_finish() && !queued);
|
|
conversation_wait = WAIT_NONE; player_mq_couter = 1; no_speech_timeout = 1;
|
|
assert(app_role_notice_before_finish() && !queued);
|
|
player_mq_couter = 0;
|
|
assert(app_role_notice_before_finish() && queued == 1 && last_queue_type == 5);
|
|
assert(no_speech_timeout == 1); /* Error flag never becomes auto-listening. */
|
|
assert(app_role_notice_before_finish() && queued == 1);
|
|
player_mq_couter = 0; role_notice_finished(conversation_round, 0);
|
|
assert(!app_role_notice_before_finish() && no_speech_timeout == 1);
|
|
/* Speech: zero insertion while uploading/waiting or between reply segments. */
|
|
reset(); recording = 1; vad_state = STATE_RECORDING_ACTIVE;
|
|
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
|
|
recording = 0; vad_state = STATE_WAITING_COMMAND; conversation_wait = WAIT_REPLY;
|
|
assert(app_role_notice_before_finish() && !queued);
|
|
conversation_wait = WAIT_NONE; tts_playing = 1;
|
|
assert(app_role_notice_before_finish() && !queued);
|
|
tts_playing = 0; player_mq_couter = 2;
|
|
assert(app_role_notice_before_finish() && !queued);
|
|
player_mq_couter = 0;
|
|
assert(app_role_notice_before_finish() && queued == 1);
|
|
role_notice_finished(conversation_round, 0); player_mq_couter = 0;
|
|
assert(!app_role_notice_before_finish() && no_speech_timeout == 0);
|
|
/* Repeated switches produce one latest URL at the end, never six entries. */
|
|
reset(); vad_state = STATE_WAITING_COMMAND; tts_playing = 1;
|
|
for (int n = 0; n < 6; n++) {
|
|
char url[128]; snprintf(url, sizeof(url), "http://host/assets/audio/switch_role.mp3?v=%d", n);
|
|
assert(app_role_notice_handle_url(url)); assert(!queued);
|
|
}
|
|
tts_playing = 0;
|
|
assert(app_role_notice_before_finish() && queued == 1);
|
|
assert(strstr(last_queue_url, "v=5"));
|
|
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
|
|
assert(queued == 1 && !role_notice_pending);
|
|
role_notice_finished(conversation_round, 0); player_mq_couter = 0;
|
|
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
|
|
assert(queued == 1 && !app_role_notice_before_finish());
|
|
/* Idle plays promptly; playback/queue failure releases completion too. */
|
|
reset(); app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
|
|
assert(queued == 1); player_mq_couter = 0;
|
|
role_notice_finished(conversation_round, -1); assert(!app_role_notice_before_finish());
|
|
reset(); queue_failure = 1;
|
|
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
|
|
assert(!queued && !app_role_notice_before_finish());
|
|
/* Accepted restart/force-stop cancels notice; no replay into next round. */
|
|
reset(); vad_state = STATE_WAITING_COMMAND; tts_playing = 1;
|
|
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
|
|
assert(conversation_try_begin() == 0 && !role_notice_pending);
|
|
reset(); recording = 1; app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
|
|
conversation_cancel_wait(); assert(!role_notice_pending && !role_notice_playing);
|
|
for (int mode = 0; mode < 3; mode++) {
|
|
reset(); vad_state = STATE_WAITING_COMMAND;
|
|
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
|
|
fota_start = mode == 0; power_down_status = mode == 1; sleeping = mode == 2;
|
|
assert(!app_role_notice_before_finish() && !role_notice_pending && !queued);
|
|
}
|
|
puts("PASS: role notices: silent/speech end, latest-only coalescing, idle, failure, restart/OTA/shutdown/sleep isolation; 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)
|