ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
338 lines
19 KiB
Plaintext
338 lines
19 KiB
Plaintext
/* 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;
|
|
}
|