Files
story/firmware/echoear-platform-agent/tools/test_rtc_rx_host.c.in
T

338 lines
19 KiB
Plaintext
Raw Normal View History

/* Host-only RTOS/codec boundary stubs. Production functions are inserted below.
* Tests use synthetic byte markers, no hardware, network, or captured audio. */
#include <assert.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <unistd.h>
#define ESP_PLATFORM 1
#define pdTRUE 1
#define pdPASS 1
#define pdMS_TO_TICKS(n) (n)
#define portMAX_DELAY UINT32_MAX
#define tskNO_AFFINITY (-1)
#define MALLOC_CAP_SPIRAM 1
#define MALLOC_CAP_8BIT 2
#define ESP_AUDIO_ERR_OK 0
#define ESP_OPUS_DEC_FRAME_DURATION_120_MS 120
#define ESP_OPUS_DEC_CONFIG_DEFAULT() {0}
#define VOLC_DATA_TYPE_AUDIO 1
#define VOLC_AUDIO_DATA_TYPE_PCM 1
#define AUDIO_DATA_TYPE_OPUS 2
#define VOLC_DATA_TYPE_MESSAGE 3
#define VOLC_MSG_CONV_STATUS 4
#define VOLC_CONV_STATUS_INTERRUPTED 5
#define MAGIC_CONV "conv"
#define MAGIC_OFFSET 8
#define LOGI(...) ((void)0)
#define LOGE(...) ((void)0)
#define HAL_SAFE_FREE(p) do {free(p); (p)=NULL;} while(0)
typedef int audio_data_type_e;
typedef int BaseType_t;
static void __rtc_diagnostic_priority_apply(void) {} /* separately tested in voice-diagnostic-host.c */
typedef int volc_audio_data_type_e;
typedef void *byte_rtc_engine_t;
typedef void *volc_opt_t;
typedef struct {int unused;} volc_room_info_t;
typedef struct {int unused;} byte_rtc_event_handler_t;
typedef void (*volc_msg_cb)(void *, void *);
typedef struct {int code;struct {int conv_status;} data;} volc_msg_t;
typedef struct {int type; struct {struct {int data_type;} audio;struct {bool is_binary;} message;} info;} volc_data_info_t;
typedef void (*volc_data_cb)(void *, const void *, int, volc_data_info_t *);
typedef struct {char *p_channel_name,*p_uid,*p_token;} volc_rtc_option_t;
typedef struct {bool auto_publish_audio,auto_publish_video,auto_subscribe_audio,auto_subscribe_video;} byte_rtc_room_options_t;
static uint64_t esp_timer_get_time(void) {
struct timespec now; clock_gettime(CLOCK_MONOTONIC,&now);
return (uint64_t)now.tv_sec*1000000+now.tv_nsec/1000;
}
static uint64_t volc_osal_get_time_ms(void) {return esp_timer_get_time()/1000;}
static void vTaskDelay(unsigned ms) {
struct timespec t={.tv_sec=ms/1000,.tv_nsec=(long)(ms%1000)*1000000};
nanosleep(&t,NULL);
}
static struct timespec deadline(unsigned ms) {
struct timespec t; clock_gettime(CLOCK_REALTIME,&t);
t.tv_sec+=ms/1000; t.tv_nsec+=(long)(ms%1000)*1000000;
if(t.tv_nsec>=1000000000){t.tv_sec++;t.tv_nsec-=1000000000;}
return t;
}
static atomic_int fail_alloc_after=-1, live_allocations, live_tasks;
static void *host_alloc(size_t n) {
int value=atomic_load(&fail_alloc_after);
if(value>=0 && atomic_fetch_sub(&fail_alloc_after,1)==0) return NULL;
void *p=calloc(1,n); assert(p); atomic_fetch_add(&live_allocations,1); return p;
}
static void host_free(void *p) {if(p){free(p);atomic_fetch_sub(&live_allocations,1);}}
static void *heap_caps_calloc(size_t n,size_t size,int caps) {
assert(caps==(MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT));return host_alloc(n*size);
}
static void heap_caps_free(void *p) {host_free(p);}
typedef struct {
pthread_mutex_t mutex; pthread_cond_t readable;
unsigned *items, capacity, head, count;
} *QueueHandle_t;
static QueueHandle_t xQueueCreate(unsigned n,unsigned item_size) {
assert(item_size==sizeof(unsigned));
QueueHandle_t q=host_alloc(sizeof(*q)+n*sizeof(unsigned)); if(!q)return NULL;
q->items=(unsigned *)(q+1);q->capacity=n;
pthread_mutex_init(&q->mutex,NULL);pthread_cond_init(&q->readable,NULL);return q;
}
static int xQueueSend(QueueHandle_t q,const unsigned *item,unsigned wait) {
assert(wait==0);assert(q);pthread_mutex_lock(&q->mutex);
int ok=q->count<q->capacity;
if(ok){q->items[(q->head+q->count)%q->capacity]=*item;q->count++;pthread_cond_signal(&q->readable);}
pthread_mutex_unlock(&q->mutex);return ok;
}
static int xQueueReceive(QueueHandle_t q,unsigned *item,unsigned wait) {
assert(q);pthread_mutex_lock(&q->mutex);
struct timespec end=deadline(wait==portMAX_DELAY?5000:wait);
while(!q->count && wait){int result=pthread_cond_timedwait(&q->readable,&q->mutex,&end);if(result==ETIMEDOUT)break;assert(result==0);}
int ok=q->count!=0;
if(ok){*item=q->items[q->head];q->head=(q->head+1)%q->capacity;q->count--;}
pthread_mutex_unlock(&q->mutex);return ok;
}
static unsigned uxQueueMessagesWaiting(QueueHandle_t q) {
pthread_mutex_lock(&q->mutex);unsigned count=q->count;pthread_mutex_unlock(&q->mutex);return count;
}
static void vQueueDelete(QueueHandle_t q) {
pthread_mutex_destroy(&q->mutex);pthread_cond_destroy(&q->readable);host_free(q);
}
typedef struct {pthread_mutex_t mutex;pthread_cond_t ready;unsigned count;} *SemaphoreHandle_t;
static SemaphoreHandle_t make_sem(unsigned count) {
SemaphoreHandle_t s=host_alloc(sizeof(*s));if(!s)return NULL;
pthread_mutex_init(&s->mutex,NULL);pthread_cond_init(&s->ready,NULL);s->count=count;return s;
}
static SemaphoreHandle_t xSemaphoreCreateMutex(void){return make_sem(1);}
static SemaphoreHandle_t xSemaphoreCreateBinary(void){return make_sem(0);}
static int xSemaphoreTake(SemaphoreHandle_t s,unsigned wait) {
assert(s);pthread_mutex_lock(&s->mutex);struct timespec end=deadline(wait==portMAX_DELAY?5000:wait);
while(!s->count && wait){int rc=pthread_cond_timedwait(&s->ready,&s->mutex,&end);if(rc==ETIMEDOUT){assert(wait!=portMAX_DELAY);break;}assert(rc==0);}
int ok=s->count!=0;if(ok)s->count--;pthread_mutex_unlock(&s->mutex);return ok;
}
static int xSemaphoreGive(SemaphoreHandle_t s) {
pthread_mutex_lock(&s->mutex);assert(s->count==0);s->count=1;
pthread_cond_signal(&s->ready);pthread_mutex_unlock(&s->mutex);return pdTRUE;
}
static void vSemaphoreDelete(SemaphoreHandle_t s) {
pthread_mutex_destroy(&s->mutex);pthread_cond_destroy(&s->ready);host_free(s);
}
typedef struct {void (*fn)(void *);void *arg;pthread_t thread;} *TaskHandle_t;
static atomic_bool worker_allowed=true;
static void *task_entry(void *arg) {
TaskHandle_t t=arg;void (*fn)(void *)=t->fn;void *context=t->arg;
while(!atomic_load(&worker_allowed))vTaskDelay(1);
fn(context);assert(!"task should delete itself");return NULL;
}
static int xTaskCreatePinnedToCoreWithCaps(void (*fn)(void *),const char *name,unsigned stack,void *arg,int priority,TaskHandle_t *out,int core,int caps) {
(void)name;assert(stack==16384 && priority==5 && core==tskNO_AFFINITY && caps==(MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT));
TaskHandle_t t=host_alloc(sizeof(*t));if(!t)return 0;t->fn=fn;t->arg=arg;*out=t;
atomic_fetch_add(&live_tasks,1);assert(!pthread_create(&t->thread,NULL,task_entry,t));return pdPASS;
}
static void vTaskSuspend(TaskHandle_t task) {assert(!task);for(;;)vTaskDelay(1000);}
static void vTaskDeleteWithCaps(TaskHandle_t task) {
assert(task);pthread_cancel(task->thread);pthread_join(task->thread,NULL);host_free(task);atomic_fetch_sub(&live_tasks,1);
}
typedef struct {int sample_rate,channel,frame_duration;} esp_opus_dec_cfg_t;
typedef struct {uint8_t *buffer;size_t len,consumed;} esp_audio_dec_in_raw_t;
typedef struct {uint8_t *buffer;size_t len,decoded_size;} esp_audio_dec_out_frame_t;
typedef struct {int unused;} esp_audio_dec_info_t;
static atomic_int decode_calls, decoder_opens, decoder_mode, decoder_open_fail;
static atomic_bool decoder_blocked;
static pthread_t test_thread;
static int esp_opus_dec_open(const esp_opus_dec_cfg_t *cfg,size_t size,void **handle) {
(void)size;assert(cfg->sample_rate==16000 && cfg->channel==1 && cfg->frame_duration==120);
if(atomic_load(&decoder_open_fail))return -1;
*handle=host_alloc(1);atomic_fetch_add(&decoder_opens,1);return *handle?0:-1;
}
static void esp_opus_dec_close(void *handle){host_free(handle);}
static int esp_opus_dec_decode(void *handle,esp_audio_dec_in_raw_t *raw,esp_audio_dec_out_frame_t *frame,esp_audio_dec_info_t *info) {
(void)info;assert(handle && raw->len && frame->len>=2);assert(!pthread_equal(pthread_self(),test_thread));
atomic_fetch_add(&decode_calls,1);while(atomic_load(&decoder_blocked))vTaskDelay(1);
int mode=atomic_load(&decoder_mode);raw->consumed=1;frame->decoded_size=2;
frame->buffer[0]=raw->buffer[0];frame->buffer[1]=0;
if(mode==1)return -1;
if(mode==2)raw->consumed=0;
if(mode==3)raw->consumed=raw->len+1;
if(mode==4)frame->decoded_size=frame->len+1;
return 0;
}
static int join_result,leave_result;
static void *byte_rtc_get_user_data(byte_rtc_engine_t engine){return engine;}
static int byte_rtc_join_room(byte_rtc_engine_t engine,const char *room,const char *uid,const char *token,byte_rtc_room_options_t *options) {
(void)engine;(void)room;(void)uid;(void)token;(void)options;return join_result;
}
static int byte_rtc_leave_room(byte_rtc_engine_t engine,const char *room){(void)engine;(void)room;return leave_result;}
static bool _is_target_message(const uint8_t *message,const char *magic){return !memcmp(message,magic,4);}
static int _on_conversion_status_message_parsed(uint8_t *message,void *rtc){(void)rtc;return *message;}
/* @PRODUCTION@ */
static atomic_uint emitted;
static atomic_uint interruptions;
static atomic_bool emit_blocked,emit_entered;
static uint8_t output[256];
static void on_pcm(void *context,const void *data,int size,volc_data_info_t *info) {
(void)context;assert(size>0 && info->type==VOLC_DATA_TYPE_AUDIO);
atomic_store(&emit_entered,true);while(atomic_load(&emit_blocked))vTaskDelay(1);
unsigned count=atomic_load(&emitted);assert(count<sizeof(output));output[count]=*(const uint8_t *)data;
atomic_store(&emitted,count+1);
}
static void on_message(void *context,void *value) {
(void)context;volc_msg_t *msg=value;
assert(msg->code==VOLC_MSG_CONV_STATUS && msg->data.conv_status==VOLC_CONV_STATUS_INTERRUPTED);
/* This models the service clearing queued PCM, not a raw-audio capture. */
atomic_store(&emitted,0);atomic_fetch_add(&interruptions,1);
}
static void wait_value(atomic_int *value,int expected) {
uint64_t end=esp_timer_get_time()+3000000;
while(atomic_load(value)<expected && esp_timer_get_time()<end)vTaskDelay(1);
assert(atomic_load(value)>=expected);
}
static void wait_emitted(unsigned count) {
uint64_t end=esp_timer_get_time()+3000000;
while(atomic_load(&emitted)<count && esp_timer_get_time()<end)vTaskDelay(1);
assert(atomic_load(&emitted)==count);
}
static void wait_free(rtc_impl_t *rtc) {
uint64_t end=esp_timer_get_time()+3000000;
while(uxQueueMessagesWaiting(rtc->rx_free)<RTC_RX_PACKET_COUNT && esp_timer_get_time()<end)vTaskDelay(1);
assert(uxQueueMessagesWaiting(rtc->rx_free)==RTC_RX_PACKET_COUNT);
}
static void reset_stubs(void) {
atomic_store(&worker_allowed,true);atomic_store(&decoder_blocked,false);
atomic_store(&decode_calls,0);atomic_store(&decoder_opens,0);atomic_store(&emitted,0);
atomic_store(&decoder_mode,0);atomic_store(&decoder_open_fail,0);atomic_store(&fail_alloc_after,-1);
atomic_store(&interruptions,0);atomic_store(&emit_blocked,false);atomic_store(&emit_entered,false);
join_result=leave_result=0;test_thread=pthread_self();memset(output,0,sizeof(output));
}
static void fixture_init(rtc_impl_t *rtc,bool allow_worker) {
reset_stubs();atomic_store(&worker_allowed,allow_worker);memset(rtc,0,sizeof(*rtc));
rtc->rtc=rtc;rtc->opus_encoder=(void *)1;rtc->data_callback=on_pcm;rtc->message_callback=on_message;
assert(__rtc_rx_init(rtc)==0);
volc_rtc_option_t opt={.p_channel_name="room-A",.p_uid="device",.p_token="fixture"};
assert(__rtc_start(rtc,&opt)==0);
rtc->b_channel_joined=rtc->b_user_joined=true;
}
static void fixture_close(rtc_impl_t *rtc) {
atomic_store(&worker_allowed,true);atomic_store(&decoder_blocked,false);leave_result=0;
atomic_store(&emit_blocked,false);
__rtc_stop(rtc);__rtc_rx_destroy(rtc);
if(rtc->opus_decoder){esp_opus_dec_close(rtc->opus_decoder);rtc->opus_decoder=NULL;}
uint64_t end=esp_timer_get_time()+3000000;
while(atomic_load(&live_tasks) && esp_timer_get_time()<end)vTaskDelay(1);
assert(atomic_load(&live_tasks)==0);assert(atomic_load(&live_allocations)==0);
}
static void packet(rtc_impl_t *rtc,const char *room,const uint8_t *bytes,size_t size) {
_on_audio_data(rtc,room,"assistant",0,AUDIO_DATA_TYPE_OPUS,bytes,size,NULL,0);
}
static void interrupt_reply(rtc_impl_t *rtc,const char *room) {
uint8_t message[9]={'c','o','n','v',0,0,0,1,VOLC_CONV_STATUS_INTERRUPTED};
_on_message_received(rtc,room,"assistant",message,sizeof(message),true);
}
static void test_owned_multiframe(void) {
rtc_impl_t rtc;fixture_init(&rtc,false);uint8_t bytes[]={11,12,13};
packet(&rtc,"room-A",bytes,sizeof(bytes));memset(bytes,99,sizeof(bytes));
assert(!atomic_load(&decode_calls));atomic_store(&worker_allowed,true);
wait_emitted(3);assert(output[0]==11 && output[1]==12 && output[2]==13);fixture_close(&rtc);
}
static void test_slow_decode_bounded_queue(void) {
rtc_impl_t rtc;fixture_init(&rtc,true);atomic_store(&decoder_blocked,true);uint8_t byte=21;
packet(&rtc,"room-A",&byte,1);wait_value(&decode_calls,1);
for(unsigned i=1;i<RTC_RX_PACKET_COUNT;i++)packet(&rtc,"room-A",&byte,1);
assert(atomic_load(&decoder_blocked));assert(uxQueueMessagesWaiting(rtc.rx_pending)==RTC_RX_PACKET_COUNT-1);
packet(&rtc,"room-A",&byte,1);assert(atomic_load(&rtc.rx_overflow)==1);
packet(&rtc,"room-A",&byte,RTC_RX_PACKET_MAX+1);packet(&rtc,"room-A",NULL,1);packet(&rtc,"room-A",&byte,0);
assert(atomic_load(&rtc.rx_oversized)==3);assert(atomic_load(&rtc.rx_producers)==0);
atomic_store(&decoder_blocked,false);wait_emitted(RTC_RX_PACKET_COUNT);fixture_close(&rtc);
}
static void test_interrupt_inflight(void) {
rtc_impl_t rtc;fixture_init(&rtc,true);atomic_store(&decoder_blocked,true);uint8_t old[]={31,32},queued=33;
packet(&rtc,"room-A",old,2);wait_value(&decode_calls,1);packet(&rtc,"room-A",&queued,1);
interrupt_reply(&rtc,"room-A");atomic_store(&decoder_blocked,false);wait_free(&rtc);assert(!atomic_load(&emitted));
uint8_t fresh=34;packet(&rtc,"room-A",&fresh,1);wait_emitted(1);assert(output[0]==34);assert(atomic_load(&decoder_opens)==2);
fixture_close(&rtc);
}
static void test_interrupt_after_final_check(void) {
rtc_impl_t rtc;fixture_init(&rtc,true);atomic_store(&emit_blocked,true);uint8_t old=35;
packet(&rtc,"room-A",&old,1);
uint64_t end=esp_timer_get_time()+3000000;
while(!atomic_load(&emit_entered) && esp_timer_get_time()<end)vTaskDelay(1);
assert(atomic_load(&emit_entered));
/* The decoder's final epoch check has passed and its PCM delivery callback
* is paused immediately before the simulated player's enqueue operation. */
interrupt_reply(&rtc,"room-A");assert(atomic_load(&interruptions)==0);
uint8_t fresh=36;packet(&rtc,"room-A",&fresh,1);atomic_store(&emit_blocked,false);
wait_free(&rtc);assert(atomic_load(&interruptions)==1);
assert(atomic_load(&emitted)==1 && output[0]==36);fixture_close(&rtc);
}
typedef struct {rtc_impl_t *rtc;atomic_bool done;int result;} stop_arg;
static void *stop_call(void *arg) {stop_arg *a=arg;a->result=__rtc_stop(a->rtc);atomic_store(&a->done,true);return NULL;}
static void test_stop_race_and_room(void) {
rtc_impl_t rtc;fixture_init(&rtc,true);atomic_store(&decoder_blocked,true);uint8_t old=41;
packet(&rtc,"room-A",&old,1);wait_value(&decode_calls,1);packet(&rtc,"room-A",&old,1);
stop_arg arg={.rtc=&rtc};pthread_t stopper;assert(!pthread_create(&stopper,NULL,stop_call,&arg));
uint64_t end=esp_timer_get_time()+3000000;while(atomic_load(&rtc.rx_accepting) && esp_timer_get_time()<end)vTaskDelay(1);
assert(!atomic_load(&rtc.rx_accepting));assert(!atomic_load(&arg.done));
packet(&rtc,"room-A",&old,1);atomic_store(&decoder_blocked,false);pthread_join(stopper,NULL);
assert(arg.result==0 && atomic_load(&arg.done));wait_free(&rtc);assert(!atomic_load(&emitted));
volc_rtc_option_t opt={.p_channel_name="room-B",.p_uid="device",.p_token="fixture"};assert(__rtc_start(&rtc,&opt)==0);
packet(&rtc,"room-A",&old,1);uint8_t fresh=42;packet(&rtc,"room-B",&fresh,1);
wait_emitted(1);assert(output[0]==42);fixture_close(&rtc);
}
static void test_join_leave_failures(void) {
rtc_impl_t rtc;fixture_init(&rtc,true);assert(__rtc_stop(&rtc)==0);
volc_rtc_option_t opt={.p_channel_name="room-retry",.p_uid="device",.p_token="fixture"};join_result=-9;
assert(__rtc_start(&rtc,&opt)==-9);assert(!atomic_load(&rtc.rx_accepting));assert(!rtc.b_pipeline_started);
uint8_t byte=51;packet(&rtc,"room-retry",&byte,1);join_result=0;assert(__rtc_start(&rtc,&opt)==0);
packet(&rtc,"room-retry",&byte,1);wait_emitted(1);leave_result=-8;
assert(__rtc_stop(&rtc)==-8);assert(!atomic_load(&rtc.rx_accepting));assert(rtc.b_pipeline_started);
packet(&rtc,"room-retry",&byte,1);wait_free(&rtc);assert(atomic_load(&emitted)==1);fixture_close(&rtc);
}
static void test_decoder_rejects_invalid_progress(void) {
rtc_impl_t rtc;fixture_init(&rtc,true);uint8_t byte=61;
for(int mode=1;mode<=4;mode++){
atomic_store(&decoder_mode,mode);packet(&rtc,"room-A",&byte,1);wait_free(&rtc);assert(!atomic_load(&emitted));
}
atomic_store(&decoder_mode,0);packet(&rtc,"room-A",&byte,1);wait_emitted(1);fixture_close(&rtc);
}
static void test_decoder_open_failure_retry(void) {
rtc_impl_t rtc;fixture_init(&rtc,true);uint8_t byte=71;atomic_store(&decoder_open_fail,1);
packet(&rtc,"room-A",&byte,1);wait_free(&rtc);assert(!atomic_load(&emitted));
atomic_store(&decoder_open_fail,0);packet(&rtc,"room-A",&byte,1);wait_emitted(1);fixture_close(&rtc);
}
static void test_allocation_failures(void) {
for(int failure=0;failure<6;failure++){
reset_stubs();rtc_impl_t rtc={0};atomic_store(&fail_alloc_after,failure);
assert(__rtc_rx_init(&rtc)==-1);atomic_store(&fail_alloc_after,-1);__rtc_rx_destroy(&rtc);
assert(atomic_load(&live_allocations)==0 && atomic_load(&live_tasks)==0);
}
}
static void test_non_opus_passthrough(void) {
reset_stubs();rtc_impl_t rtc={.b_pipeline_started=true,.b_channel_joined=true,.b_user_joined=true,.data_callback=on_pcm};
uint8_t byte=81;_on_audio_data(&rtc,"room", "assistant",0,VOLC_AUDIO_DATA_TYPE_PCM,&byte,1,NULL,0);
assert(atomic_load(&emitted)==1 && output[0]==81 && atomic_load(&decode_calls)==0);
}
int main(void) {
test_owned_multiframe();puts("PASS owned packet + multi-frame input");
test_slow_decode_bounded_queue();puts("PASS callback returns during blocked decoder + bounds + full queue");
test_interrupt_inflight();puts("PASS interrupt invalidates in-flight and queued reply");
test_interrupt_after_final_check();puts("PASS interrupt serializes final-check/delivery race and fresh epoch");
test_stop_race_and_room();puts("PASS stop waits + old room cannot emit into new call");
test_join_leave_failures();puts("PASS join retry + failed leave still closes delivery");
test_decoder_rejects_invalid_progress();puts("PASS decoder error/progress/output bounds");
test_decoder_open_failure_retry();puts("PASS decoder open failure retries safely");
test_allocation_failures();puts("PASS all allocation failure cleanup");
test_non_opus_passthrough();puts("PASS non-Opus passthrough");
return 0;
}