Files
story/firmware/echoear-platform-agent/tests/device-ota-host.c
T

603 lines
45 KiB
C
Raw Normal View History

#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <sys/time.h>
#include <setjmp.h>
#include <openssl/sha.h>
typedef int esp_err_t;
typedef int nvs_handle_t;
#define ESP_OK 0
#define ESP_FAIL -1
#define ESP_ERR_NO_MEM 1
#define ESP_ERR_INVALID_RESPONSE 2
#define ESP_ERR_NOT_FOUND 3
#define ESP_ERR_INVALID_ARG 4
#define ESP_ERR_INVALID_SIZE 5
#define ESP_ERR_TIMEOUT 6
#define ESP_ERR_INVALID_CRC 7
#define ESP_ERR_NVS_NOT_FOUND 8
#define ESP_ERR_HTTP_EAGAIN 0x7007
#define NVS_READONLY 0
#define NVS_READWRITE 1
#define DEVICE_FIRMWARE_VERSION "host-original"
#define ESP_PARTITION_SUBTYPE_APP_OTA_MIN 16
#define ESP_PARTITION_SUBTYPE_APP_OTA_MAX 31
#define ESP_OTA_IMG_PENDING_VERIFY 1
#define ESP_OTA_IMG_VALID 2
#define ESP_LOGI(...) ((void)0)
#define ESP_LOGE(...) ((void)0)
#define ESP_LOGW(...) ((void)0)
#define MALLOC_CAP_INTERNAL (1U<<11)
#define MALLOC_CAP_8BIT (1U<<2)
#define OTA_WITH_SEQUENTIAL_WRITES 0
#define pdTRUE 1
#define pdPASS 1
#define portMAX_DELAY UINT32_MAX
typedef struct { uint32_t address,size,subtype; } esp_partition_t;
typedef int esp_ota_img_states_t;
typedef int esp_ota_handle_t;
static const esp_partition_t parts[]={{0x10000,131072,16},{0x410000,131072,17}};
static uint8_t flash[2][131072],image_bytes[65536+123];
static size_t image_size=2048;
static int running,boot_partition,image_states[2],mark_error,boot_error,end_error;
static int begun,written,aborted,restarts,rollbacks,marked,downloaded,reported_success,reported_failed,accepted;
static int manifest_requests,manifest_error,report_error,busy,maintenance,leaves,http_status,header_size,short_read,read_error;
static int timer_create_error,timer_start_error,timer_armed,timer_deleted;
static int64_t monotonic,clock_step;
static int read_script[64],read_script_len,read_script_index,read_calls,read_timeout_ms,http_errno;
static size_t read_at_offsets[64],read_wanted[64],write_sizes[64];
static int timeout_values[64],timeout_count,write_calls,write_fail_on,write_fail_code;
static int64_t read_advances[64],heartbeat_advance,write_advance,begin_advance,open_advance,header_advance,end_advance;
static int64_t sha_start_advance,sha_update_advance,sha_finish_advance;
static int begin_error,begin_error_handle,open_error,http_init_error,sha_error_stage;
static int opened,headers_fetched,closed,client_cleaned,client_live,ota_handle_live,ended,boot_sets,transfer_first_read_call;
static size_t begin_size;
static const esp_partition_t *begin_target;
static unsigned alloc_calls,heap_fail_first,heap_frees,heap_live;
static size_t alloc_sizes[8],allocated_size;
static unsigned alloc_caps[8];
static void *allocated_buffer;
static size_t read_offset,write_offset;
static char last_report[16];
static char last_report_message[100];
static cJSON *manifest_json;
static jmp_buf restart_jump;
static bool allow_restart;
// Execute the real production worker synchronously when the parent takes the
// semaphore. Suspend escapes with longjmp; parent must join/delete before reset.
// This proves SDK-call context and ownership, not ESP32 cache/MMU behavior.
typedef struct fake_sem { bool given; } *SemaphoreHandle_t;
typedef struct fake_task { void (*entry)(void *); void *arg; bool suspended; } *TaskHandle_t;
static TaskHandle_t live_worker;
static SemaphoreHandle_t live_sem;
static bool internal_worker_context;
static jmp_buf worker_suspend_jump;
static int sem_attempts,sem_created,sem_deleted,task_attempts,task_created,task_deleted;
static int sem_fail_on,task_fail_on,worker_runs,sem_gives,sem_takes,sha_live,rollback_error;
static int64_t cleanup_advance;
static SemaphoreHandle_t xSemaphoreCreateBinary(void){
assert(!internal_worker_context&&!live_sem&&!live_worker);
if(++sem_attempts==sem_fail_on)return NULL;
live_sem=calloc(1,sizeof(*live_sem));assert(live_sem);sem_created++;return live_sem;
}
static int xTaskCreateWithCaps(void (*entry)(void *),const char *name,unsigned stack,void *arg,unsigned priority,TaskHandle_t *result,unsigned caps){
assert(!internal_worker_context&&live_sem&&!live_worker);
assert(!strcmp(name,"ota_flash")&&stack==8192&&priority==3&&caps==(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT));
// The real TLS client, internal write buffer and SHA context must be released
// before the temporary 8KiB DRAM stack allocation, including low-memory cases.
assert(!client_live&&!heap_live&&!sha_live);
if(++task_attempts==task_fail_on)return 0;
live_worker=calloc(1,sizeof(*live_worker));assert(live_worker);
live_worker->entry=entry;live_worker->arg=arg;*result=live_worker;task_created++;return pdPASS;
}
static int xSemaphoreGive(SemaphoreHandle_t sem){
assert(internal_worker_context&&sem==live_sem&&!sem->given);sem->given=true;sem_gives++;return pdTRUE;
}
static void vTaskSuspend(TaskHandle_t task){
assert(internal_worker_context&&task==NULL&&live_worker&&live_sem->given);
live_worker->suspended=true;longjmp(worker_suspend_jump,1);
}
static int xSemaphoreTake(SemaphoreHandle_t sem,unsigned timeout){
assert(!internal_worker_context&&sem==live_sem&&live_worker&&timeout==portMAX_DELAY);
assert(!sem->given);worker_runs++;internal_worker_context=true;
if(!setjmp(worker_suspend_jump)){live_worker->entry(live_worker->arg);assert(!"worker returned without suspend");}
internal_worker_context=false;assert(sem->given&&live_worker->suspended);sem_takes++;return pdTRUE;
}
static void vTaskDeleteWithCaps(TaskHandle_t task){
assert(!internal_worker_context&&task==live_worker&&task->suspended&&live_sem->given);
free(task);live_worker=NULL;task_deleted++;
}
static void vSemaphoreDelete(SemaphoreHandle_t sem){
assert(!internal_worker_context&&sem==live_sem&&!live_worker);
free(sem);live_sem=NULL;sem_deleted++;
}
static void assert_worker_cleanup(void){
assert(!live_worker&&!live_sem&&!internal_worker_context);
assert(task_created==task_deleted&&sem_created==sem_deleted);
assert(worker_runs==sem_gives&&sem_gives==sem_takes&&sem_takes==task_deleted);
}
static const char *platform_error="";
static const char *task="40000000-0000-4000-8000-000000000001";
static struct {char key[16];unsigned char bytes[512];size_t size;} records[8];
static int record_count,fail_save_phase,fail_installed,fail_readback;
static bool fail_receipt_commit;
static unsigned char staged[512];static size_t staged_size;static char staged_key[16];
static void *heap_caps_malloc(size_t size,unsigned caps){
assert(alloc_calls<8);alloc_sizes[alloc_calls]=size;alloc_caps[alloc_calls]=caps;
if(++alloc_calls<=heap_fail_first)return NULL;
assert(!heap_live);allocated_buffer=malloc(size);assert(allocated_buffer);allocated_size=size;heap_live++;return allocated_buffer;
}
static void heap_caps_free(void *buffer){
if(!buffer)return;assert(heap_live==1&&buffer==allocated_buffer);free(buffer);allocated_buffer=NULL;heap_live--;heap_frees++;
}
static int nvs_open(const char *space,int mode,int *handle){assert(!strcmp(space,"control"));*handle=1;return 0;}
static void nvs_close(int n){}
static int nvs_set_blob(int n,const char *key,const void *bytes,size_t size){assert(size<=sizeof(staged)&&strlen(key)<sizeof(staged_key));strcpy(staged_key,key);memcpy(staged,bytes,size);staged_size=size;return 0;}
static int nvs_commit(int n){
if(!strcmp(staged_key,"ota")){uint32_t phase;memcpy(&phase,staged+4,4);if((fail_save_phase&&phase==(unsigned)fail_save_phase)||(fail_receipt_commit&&phase==0))return ESP_FAIL;}
if(!strncmp(staged_key,"installed",9)&&fail_installed)return ESP_FAIL;
int i;for(i=0;i<record_count;i++)if(!strcmp(records[i].key,staged_key))break;
assert(i<8);if(i==record_count)record_count++;strcpy(records[i].key,staged_key);memcpy(records[i].bytes,staged,staged_size);records[i].size=staged_size;return 0;
}
static int nvs_get_blob(int n,const char *key,void *bytes,size_t *size){
for(int i=0;i<record_count;i++)if(!strcmp(records[i].key,key)){if(*size<records[i].size)return ESP_ERR_INVALID_SIZE;*size=records[i].size;memcpy(bytes,records[i].bytes,*size);if(fail_readback)return ESP_FAIL;return 0;}return ESP_ERR_NVS_NOT_FOUND;
}
typedef struct {char item[37];bool occupied;} StaticQueue_t;
typedef StaticQueue_t *QueueHandle_t;
static QueueHandle_t xQueueCreateStatic(int n,int size,uint8_t *storage,StaticQueue_t *q){assert(n==1&&size==37);q->occupied=false;return q;}
static int xQueueSend(QueueHandle_t q,const void *item,int timeout){if(q->occupied)return 0;memcpy(q->item,item,37);q->occupied=true;return pdTRUE;}
static int xQueueReceive(QueueHandle_t q,void *item,int timeout){if(!q->occupied)return 0;memcpy(item,q->item,37);q->occupied=false;return pdTRUE;}
typedef struct {void (*callback)(void *);const char *name;} esp_timer_create_args_t;
typedef void *esp_timer_handle_t;
static void (*timer_callback)(void *);
static int esp_timer_create(const esp_timer_create_args_t *a,esp_timer_handle_t *h){if(timer_create_error)return ESP_FAIL;*h=(void*)1;timer_callback=a->callback;return 0;}
static int esp_timer_start_once(esp_timer_handle_t h,int64_t us){assert(us==180000000);if(timer_start_error)return ESP_FAIL;timer_armed=1;return 0;}
static int esp_timer_stop(esp_timer_handle_t h){timer_armed=0;return 0;}
static int esp_timer_delete(esp_timer_handle_t h){timer_deleted++;return 0;}
static int64_t esp_timer_get_time(void){monotonic+=clock_step;return monotonic;}
static const esp_partition_t *esp_ota_get_running_partition(void){return &parts[running];}
static const esp_partition_t *esp_ota_get_next_update_partition(const esp_partition_t *p){return &parts[1-running];}
static int esp_ota_get_state_partition(const esp_partition_t *p,int *value){*value=image_states[p==&parts[1]];return *value<0?ESP_ERR_NOT_FOUND:0;}
static int esp_ota_set_boot_partition(const esp_partition_t *p){assert(internal_worker_context&&!heap_live&&!client_live&&!sha_live);boot_sets++;if(boot_error)return ESP_FAIL;boot_partition=p==&parts[1];return 0;}
static int esp_ota_mark_app_valid_cancel_rollback(void){if(mark_error)return ESP_FAIL;marked++;image_states[running]=ESP_OTA_IMG_VALID;return 0;}
static void esp_restart(void){assert_worker_cleanup();restarts++;assert(allow_restart);longjmp(restart_jump,1);}
static int esp_ota_mark_app_invalid_rollback(void){assert(internal_worker_context);rollbacks++;if(rollback_error)return ESP_FAIL;boot_partition=1-running;return 0;}
static int esp_partition_read(const esp_partition_t *p,uint32_t offset,void *bytes,size_t size){assert(offset+size<=p->size);memcpy(bytes,flash[p==&parts[1]]+offset,size);return 0;}
static int esp_ota_begin(const esp_partition_t *p,size_t size,esp_ota_handle_t *handle){
assert(p==&parts[1-running]&&read_calls==transfer_first_read_call&&!ota_handle_live);begun++;begin_target=p;begin_size=size;write_offset=0;
assert(size>=1024&&size<=p->size);monotonic+=begin_advance;
if(!begin_error||begin_error_handle){*handle=(p==&parts[1])+1;ota_handle_live=*handle;}
if(begin_error)return begin_error;
size_t erased=(size+4095)&~(size_t)4095;assert(erased<=p->size);memset(flash[p==&parts[1]],0xff,erased);return 0;
}
static int esp_ota_write(esp_ota_handle_t handle,const void *bytes,size_t size){
assert(handle==ota_handle_live&&handle-1!=running&&write_calls<64);write_sizes[write_calls++]=size;monotonic+=write_advance;
if(write_fail_on&&write_calls==write_fail_on)return write_fail_code;
assert(write_offset+size<=parts[handle-1].size);memcpy(flash[handle-1]+write_offset,bytes,size);write_offset+=size;written++;return 0;
}
static int esp_ota_end(esp_ota_handle_t handle){assert(internal_worker_context&&!heap_live&&!client_live&&!sha_live);assert(handle==ota_handle_live);ended++;monotonic+=end_advance;ota_handle_live=0;return end_error?ESP_FAIL:0;}
static int esp_ota_abort(esp_ota_handle_t handle){assert(handle==ota_handle_live);ota_handle_live=0;aborted++;return 0;}
static const char *esp_err_to_name(int err){return "INJECTED_FAILURE";}
static int esp_crt_bundle_attach(void *v){return 0;}
typedef struct {const char *url;int (*crt_bundle_attach)(void *);int timeout_ms,disable_auto_redirect,buffer_size,buffer_size_tx,keep_alive_enable;} esp_http_client_config_t;
typedef void *esp_http_client_handle_t;
static void *esp_http_client_init(const esp_http_client_config_t *cfg){assert(cfg->disable_auto_redirect&&cfg->crt_bundle_attach&&cfg->buffer_size==16384&&!client_live);read_offset=0;transfer_first_read_call=read_calls;read_timeout_ms=cfg->timeout_ms;downloaded++;if(http_init_error)return NULL;client_live=1;return (void*)1;}
static int esp_http_client_open(void *h,int length){assert(h==(void*)1&&client_live);opened++;monotonic+=open_advance;return open_error;}
static int64_t esp_http_client_fetch_headers(void *h){assert(h==(void*)1&&client_live);headers_fetched++;monotonic+=header_advance;return header_size;}
static int esp_http_client_get_status_code(void *h){return http_status;}
static int esp_http_client_set_timeout_ms(void *h,int timeout_ms){assert(h==(void*)1&&client_live&&timeout_ms>0&&timeout_ms<=20000&&timeout_count<64);timeout_values[timeout_count++]=timeout_ms;read_timeout_ms=timeout_ms;return 0;}
static int esp_http_client_get_errno(void *h){assert(h==(void*)1);return http_errno;}
static int esp_http_client_read(void *h,char *buf,size_t size){
assert(h==(void*)1&&client_live&&read_calls<64&&heap_live&&buf==allocated_buffer&&size<=allocated_size);int index=read_calls++;read_at_offsets[index]=read_offset;read_wanted[index]=size;
int result=read_error?-1:short_read?0:read_script_index<read_script_len?read_script[read_script_index++]:(int)size;
http_errno=result==-ESP_ERR_HTTP_EAGAIN?11:result<0?5:0;
monotonic+=read_advances[index];if(result==-ESP_ERR_HTTP_EAGAIN)monotonic+=(int64_t)read_timeout_ms*1000;
if(result<=0)return result;
assert((size_t)result<=size&&read_offset+(size_t)result<=image_size);memcpy(buf,image_bytes+read_offset,result);read_offset+=result;return result;
}
static void esp_http_client_close(void *h){assert(h==(void*)1&&client_live);closed++;}
static void esp_http_client_cleanup(void *h){assert(h==(void*)1&&client_live);client_live=0;client_cleaned++;monotonic+=cleanup_advance;}
typedef SHA256_CTX mbedtls_sha256_context;
static void mbedtls_sha256_init(mbedtls_sha256_context *ctx){sha_live++;memset(ctx,0,sizeof(*ctx));}
static void mbedtls_sha256_free(mbedtls_sha256_context *ctx){assert(sha_live>0);sha_live--;}
static int mbedtls_sha256_starts(mbedtls_sha256_context *ctx,int is224){monotonic+=sha_start_advance;if(sha_error_stage==1)return -1;return SHA256_Init(ctx)?0:-1;}
static int mbedtls_sha256_update(mbedtls_sha256_context *ctx,const void *bytes,size_t size){monotonic+=sha_update_advance;if(sha_error_stage==2)return -1;return SHA256_Update(ctx,bytes,size)?0:-1;}
static int mbedtls_sha256_finish(mbedtls_sha256_context *ctx,unsigned char hash[32]){monotonic+=sha_finish_advance;if(sha_error_stage==3)return -1;return SHA256_Final(hash,ctx)?0:-1;}
static void mbedtls_platform_zeroize(void *bytes,size_t size){memset(bytes,0,size);}
static bool call_client_enter_maintenance(void){if(busy)return false;assert(!maintenance);maintenance=1;return true;}
static void call_client_leave_maintenance(void){assert(maintenance);maintenance=0;leaves++;}
static void device_control_heartbeat(void){monotonic+=heartbeat_advance;heartbeat_advance=0;}
static void device_control_prepare_restart(void){}
static const char *ble_cloud_origin(void){return "https://platform.example";}
static const char *ble_factory_model(void){return "ECHOEAR-S3";}
static const char *platform_last_error_code(void){return platform_error;}
const char *device_ota_version(void);
int64_t device_ota_version_code(void);
static int platform_request(const char *method,const char *path,const char *body,const char *id,char **raw){
assert(!strcmp(method,"POST"));*raw=NULL;cJSON *request=cJSON_Parse(body);assert(request);
assert(!strcmp(cJSON_GetObjectItem(request,"task_id")->valuestring,task));
if(!strcmp(path,"/api/v1/device/ota/manifest")){manifest_requests++;cJSON_Delete(request);if(manifest_error)return ESP_FAIL;cJSON *root=cJSON_CreateObject();cJSON_AddItemToObject(root,"data",cJSON_Duplicate(manifest_json,true));*raw=cJSON_PrintUnformatted(root);cJSON_Delete(root);return 0;}
assert(!strcmp(path,"/api/v1/device/ota/report"));const char *status=cJSON_GetObjectItem(request,"status")->valuestring;snprintf(last_report,sizeof(last_report),"%s",status);
const cJSON *message=cJSON_GetObjectItem(request,"message");snprintf(last_report_message,sizeof(last_report_message),"%s",cJSON_IsString(message)?message->valuestring:"");
if(!strcmp(status,"accepted"))accepted++;else if(!strcmp(status,"success"))reported_success++;else if(!strcmp(status,"failed"))reported_failed++;else assert(false);
cJSON_Delete(request);if(report_error)return ESP_FAIL;*raw=strdup("{\"data\":{}}");return 0;
}
/* PRODUCTION_OTA */
static void reboot_to(int slot){
running=slot;maintenance=0;ready=false;boot_timer=NULL;requests=NULL;timer_armed=0;timer_callback=NULL;memset(&state,0,sizeof(state));memset(&installed,0,sizeof(installed));
device_ota_boot_guard();
}
static void fresh(void){
assert(!heap_live&&!client_live&&!ota_handle_live&&!sha_live);assert_worker_cleanup();
sem_attempts=sem_created=sem_deleted=task_attempts=task_created=task_deleted=0;
sem_fail_on=task_fail_on=worker_runs=sem_gives=sem_takes=rollback_error=0;cleanup_advance=0;
task="40000000-0000-4000-8000-000000000001";
cJSON_Delete(manifest_json);manifest_json=NULL;
running=boot_partition=record_count=begun=written=aborted=restarts=rollbacks=marked=downloaded=reported_success=reported_failed=accepted=manifest_requests=0;
mark_error=boot_error=end_error=manifest_error=report_error=busy=maintenance=leaves=short_read=read_error=timer_create_error=timer_start_error=timer_deleted=0;
fail_save_phase=fail_installed=fail_readback=0;fail_receipt_commit=false;last_report[0]=0;monotonic=0;clock_step=1000;
read_script_len=read_script_index=read_calls=timeout_count=write_calls=write_fail_on=http_errno=0;write_fail_code=95;
heartbeat_advance=write_advance=begin_advance=open_advance=header_advance=end_advance=sha_start_advance=sha_update_advance=sha_finish_advance=0;
begin_error=open_error=http_init_error=sha_error_stage=opened=headers_fetched=closed=client_cleaned=ended=boot_sets=transfer_first_read_call=0;begin_error_handle=1;begin_target=NULL;begin_size=0;
alloc_calls=heap_fail_first=heap_frees=0;allocated_size=0;allocated_buffer=NULL;image_size=2048;
memset(alloc_sizes,0,sizeof(alloc_sizes));memset(alloc_caps,0,sizeof(alloc_caps));memset(write_sizes,0,sizeof(write_sizes));memset(read_wanted,0,sizeof(read_wanted));
memset(read_script,0,sizeof(read_script));memset(read_at_offsets,0,sizeof(read_at_offsets));memset(read_advances,0,sizeof(read_advances));last_report_message[0]=0;
platform_error="";
image_states[0]=ESP_OTA_IMG_VALID;image_states[1]=ESP_OTA_IMG_PENDING_VERIFY;
for(size_t i=0;i<sizeof(image_bytes);i++)image_bytes[i]=(i*31+17)%251;
memset(flash,0,sizeof(flash));header_size=image_size;http_status=200;
unsigned char hash[32];char hex[65];SHA256(image_bytes,image_size,hash);hash_hex(hash,hex);
struct timeval now;gettimeofday(&now,NULL);
manifest_json=cJSON_CreateObject();cJSON_AddStringToObject(manifest_json,"task_id",task);cJSON_AddStringToObject(manifest_json,"model","ECHOEAR-S3");
cJSON_AddStringToObject(manifest_json,"version","new-11");cJSON_AddNumberToObject(manifest_json,"version_code",11);cJSON_AddNumberToObject(manifest_json,"size",image_size);
cJSON_AddStringToObject(manifest_json,"sha256",hex);cJSON_AddStringToObject(manifest_json,"download_url","https://platform.example/f/test-ticket");cJSON_AddNumberToObject(manifest_json,"expires_at_ms",(double)now.tv_sec*1000+30000);
reboot_to(0);device_ota_boot_ready();assert(ready);
}
static void set_string(const char *key,const char *value){assert(cJSON_ReplaceItemInObject(manifest_json,key,cJSON_CreateString(value)));}
static void set_number(const char *key,double value){assert(cJSON_ReplaceItemInObject(manifest_json,key,cJSON_CreateNumber(value)));}
static void install(void){device_ota_request(task);allow_restart=true;if(!setjmp(restart_jump))device_ota_poll();allow_restart=false;}
static void boot_ready_allow_rollback(void){allow_restart=true;if(!setjmp(restart_jump))device_ota_boot_ready();allow_restart=false;}
static void original_record(void){installed_t old={.format=1,.address=parts[0].address,.code=10};strcpy(old.version,"old-10");assert(persist("installed",&old,sizeof(old))==0);reboot_to(0);device_ota_boot_ready();assert(device_ota_version_code()==10);}
static void test_normal(void){
fresh();original_record();install();assert(restarts==1&&boot_partition==1&&state.phase==2&&accepted==1);assert(!memcmp(flash[1],image_bytes,image_size));
reboot_to(1);assert(timer_armed);device_ota_boot_ready();assert(ready&&state.phase==3&&device_ota_version_code()==11&&marked==1&&!timer_armed);
device_ota_poll();assert(reported_success==1&&state.phase==0);int prior=manifest_requests;device_ota_request(task);device_ota_poll();assert(prior==manifest_requests&&downloaded==1);
reboot_to(1);device_ota_boot_ready();assert(device_ota_version_code()==11);
}
static void test_rejections(void){
for(int i=0;i<8;i++){fresh();switch(i){case 0:set_string("model","wrong");break;case 1:set_number("size",999999);break;case 2:set_number("version_code",0);break;case 3:set_number("expires_at_ms",1);break;case 4:set_string("sha256","bad");break;case 5:set_string("download_url","https://evil.example/f/test-ticket");break;case 6:set_string("task_id","wrong");break;case 7:busy=1;break;}
install();assert(!begun&&!restarts&&boot_partition==0&&state.phase==4&&!maintenance);device_ota_poll();assert(reported_failed==1);}
fresh();manifest_error=1;install();assert(!begun&&!restarts&&!maintenance);
}
static void test_download_failures(void){
for(int i=0;i<7;i++){fresh();switch(i){case 0:image_bytes[0]^=1;break;case 1:http_status=302;break;case 2:header_size--;break;case 3:short_read=1;break;case 4:read_error=1;break;case 5:end_error=1;break;case 6:clock_step=500000000;break;}
install();assert(!restarts&&boot_partition==0&&state.phase==4&&!maintenance);device_ota_poll();assert(reported_failed==1);}
for(int phase=1;phase<=2;phase++){fresh();fail_save_phase=phase;install();assert(!restarts&&boot_partition==0&&!maintenance);if(phase==1)assert(!accepted&&!begun);}
fresh();report_error=1;install();assert(!begun&&!restarts&&!maintenance);
fresh();boot_error=1;install();assert(!restarts&&boot_partition==0&&state.phase==4);
}
static void script_reads(const int *values,size_t count){assert(count<=64);memcpy(read_script,values,count*sizeof(*values));read_script_len=count;}
static void assert_download_receipt(const char *detail){
assert(state.phase==4&&!restarts&&boot_partition==0&&!maintenance&&aborted==1);
assert(strstr(state.message,detail)&&strlen(state.message)<sizeof(state.message));
assert(!strstr(state.message,"https://")&&!strstr(state.message,"test-ticket"));
char expected[sizeof(state.message)];strcpy(expected,state.message);device_ota_poll();
assert(reported_failed==1&&!strcmp(last_report_message,expected)&&state.phase==0);
}
static void test_read_recovery(void){
fresh();clock_step=0;const int reads[]={-ESP_ERR_HTTP_EAGAIN,-ESP_ERR_HTTP_EAGAIN,512,-ESP_ERR_HTTP_EAGAIN,-ESP_ERR_HTTP_EAGAIN,1536};script_reads(reads,6);
install();assert(restarts==1&&boot_partition==1&&state.phase==2&&downloaded==1&&begun==1&&read_calls==6&&written==2);
assert(!memcmp(flash[1],image_bytes,image_size)&&read_offset==image_size&&write_offset==image_size);
assert(read_at_offsets[0]==0&&read_at_offsets[1]==0&&read_at_offsets[2]==0&&read_at_offsets[3]==512&&read_at_offsets[4]==512&&read_at_offsets[5]==512);
assert(timeout_count==8);for(int i=0;i<timeout_count;i++)assert(timeout_values[i]==20000);
assert(monotonic==80000000&&!aborted&&strstr(state.message,"OTA ms="));
}
static void test_read_stall_limit(void){
fresh();clock_step=0;const int reads[]={-ESP_ERR_HTTP_EAGAIN,-ESP_ERR_HTTP_EAGAIN,-ESP_ERR_HTTP_EAGAIN,2048};script_reads(reads,4);
install();assert(read_calls==3&&downloaded==1&&begun==1&&!written&&read_offset==0&&write_offset==0&&monotonic==60000000);
assert_download_receipt("OTA read e=0x7007 off=0/2048 rc=-28679 errno=11");
}
static void test_read_eof_and_error(void){
const int failures[]={0,-1,-0x7005};
for(size_t i=0;i<sizeof(failures)/sizeof(failures[0]);i++){
fresh();clock_step=0;const int reads[]={512,failures[i],1536};script_reads(reads,3);install();
assert(read_calls==2&&written==1&&downloaded==1&&read_offset==512&&write_offset==512);
char detail[80];snprintf(detail,sizeof(detail),"OTA read e=0xffffffff off=512/2048 rc=%d errno=%d",failures[i],failures[i]<0?5:0);
assert_download_receipt(detail);
}
}
static void test_write_failure_offset(void){
fresh();clock_step=0;const int reads[]={512,512,1024};script_reads(reads,3);write_fail_on=2;install();
assert(read_calls==2&&write_calls==2&&written==1&&read_offset==1024&&write_offset==512);
assert(!memcmp(flash[1],image_bytes,512)&&flash[1][512]==0xff);
assert_download_receipt("OTA write e=0x5f off=512/2048 rc=512 errno=0");
}
static void test_read_deadline(void){
// A pending heartbeat must not extend the fixed 900-second budget.
fresh();clock_step=0;heartbeat_advance=900000001;install();
assert(!read_calls&&timeout_count==2&&!written);assert_download_receipt("OTA deadline e=0x6 off=0/2048 rc=0 errno=0");
// The last wait is clipped to the remaining 500ms and cannot be retried.
fresh();clock_step=0;heartbeat_advance=899500000;const int stalled[]={-ESP_ERR_HTTP_EAGAIN,2048};script_reads(stalled,2);install();
assert(read_calls==1&&timeout_count==3&&timeout_values[2]==500&&monotonic==900000000&&!written);
assert_download_receipt("OTA deadline e=0x6 off=0/2048 rc=-28679 errno=11");
// Even a positive read that finishes at the deadline cannot start a write.
fresh();clock_step=0;heartbeat_advance=899500000;const int positive[]={512,1536};script_reads(positive,2);read_advances[0]=500000;install();
assert(read_calls==1&&timeout_values[2]==500&&read_offset==512&&!written&&write_offset==0);
assert_download_receipt("OTA deadline e=0x6 off=0/2048 rc=512 errno=0");
// If a write reaches the deadline, diagnostics count only completed writes.
fresh();clock_step=0;script_reads(positive,2);write_advance=900000000;install();
assert(read_calls==1&&written==1&&write_offset==512&&monotonic==900000000);
assert_download_receipt("OTA deadline e=0x6 off=512/2048 rc=512 errno=0");
}
static void test_long_download(void){
fresh();clock_step=0;const int reads[]={1024,1024};script_reads(reads,2);read_advances[0]=read_advances[1]=350000000;install();
assert(restarts==1&&boot_partition==1&&state.phase==2&&monotonic==700000000&&downloaded==1&&begun==1&&written==2&&!aborted);
assert(!memcmp(flash[1],image_bytes,image_size)&&strstr(state.message,"OTA ms="));
}
static void assert_resources_clean(unsigned allocations,unsigned frees,int aborts,int clients){
assert(alloc_calls==allocations&&heap_frees==frees&&!heap_live&&!client_live&&!ota_handle_live);
assert(aborted==aborts&&closed==clients&&client_cleaned==clients);
}
static void assert_failed_safe(const char *detail,unsigned allocations,unsigned frees,int aborts,int clients){
assert(state.phase==4&&!restarts&&!boot_sets&&boot_partition==0&&!maintenance);
assert(strstr(state.message,detail)&&!strstr(state.message,"OTA ms=")&&!strstr(state.message,"https://")&&!strstr(state.message,"test-ticket"));
assert_resources_clean(allocations,frees,aborts,clients);
char expected[sizeof(state.message)];strcpy(expected,state.message);device_ota_poll();
assert(reported_failed==1&&!strcmp(last_report_message,expected)&&state.phase==0);
}
static void test_known_size_preerase(void){
fresh();clock_step=0;memset(flash[0],0xa7,sizeof(flash[0]));install();
assert(restarts==1&&boot_partition==1&&boot_sets==1&&begun==1&&begin_target==&parts[1]&&begin_size==image_size);
assert(opened==1&&headers_fetched==1&&read_calls==1&&read_wanted[0]==image_size&&write_sizes[0]==image_size);
assert(alloc_sizes[0]==8192&&alloc_caps[0]==(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT));
assert(!memcmp(flash[1],image_bytes,image_size));
for(size_t i=0;i<sizeof(flash[0]);i++)assert(flash[0][i]==0xa7);
for(size_t i=image_size;i<4096;i++)assert(flash[1][i]==0xff);
assert(flash[1][4096]==0);assert_resources_clean(1,1,0,1);
}
static void test_target_guard(void){
for(int i=0;i<5;i++){
fresh();clock_step=0;state.size=image_size;
esp_partition_t alias=parts[0];const esp_partition_t *target=&parts[1];
if(i==0)target=&parts[0];else if(i==1)target=&alias;else if(i==2)target=NULL;
else if(i==3)state.size=1023;else state.size=parts[1].size+1;
assert(download("https://platform.example/f/test-ticket",target)==ESP_ERR_INVALID_SIZE);
assert(!begun&&!downloaded&&!opened&&!headers_fetched&&!read_calls&&!written&&!restarts&&!boot_sets);
assert_resources_clean(0,0,0,0);
}
}
static void test_begin_error_and_deadline(void){
for(int returns_handle=0;returns_handle<2;returns_handle++){
fresh();clock_step=0;begin_error=ESP_FAIL;begin_error_handle=returns_handle;begin_advance=7000;install();
assert(begun==1&&begin_size==image_size&&!read_calls&&!written&&!ended&&monotonic==7000);
assert_failed_safe("OTA begin e=0xffffffff off=0/2048 rc=0 errno=0",1,1,returns_handle,1);
}
// Preerase consumes the same fixed budget, rather than creating a new one.
fresh();clock_step=0;begin_advance=900000000;install();
assert(begun==1&&!read_calls&&!written&&!ended&&monotonic==900000000&&timeout_count==2);
assert_failed_safe("OTA deadline e=0x6 off=0/2048 rc=0 errno=0",1,1,1,1);
// Only 500ms remains after erase; the first read cannot obtain a fresh 20s.
fresh();clock_step=0;begin_advance=899500000;const int stalled[]={-ESP_ERR_HTTP_EAGAIN,2048};script_reads(stalled,2);install();
assert(read_calls==1&&timeout_count==3&&timeout_values[0]==20000&&timeout_values[1]==20000&&timeout_values[2]==500&&!written);
assert_failed_safe("OTA deadline e=0x6 off=0/2048 rc=-28679 errno=11",1,1,1,1);
}
static void test_setup_deadlines(void){
// Neither open nor headers gets to reset the deadline or begin erasing late.
fresh();clock_step=0;open_advance=900000000;install();
assert(opened==1&&!headers_fetched&&!begun&&!read_calls&&timeout_count==1);
assert_failed_safe("OTA deadline e=0x6 off=0/2048 rc=0 errno=0",1,1,0,1);
fresh();clock_step=0;header_advance=900000000;install();
assert(opened==1&&headers_fetched==1&&!begun&&!read_calls&&timeout_count==2);
assert_failed_safe("OTA deadline e=0x6 off=0/2048 rc=0 errno=0",1,1,0,1);
fresh();clock_step=0;open_advance=899500000;header_advance=500000;install();
assert(timeout_count==2&&timeout_values[0]==20000&&timeout_values[1]==500&&!begun&&!read_calls);
assert_failed_safe("OTA deadline e=0x6 off=0/2048 rc=0 errno=0",1,1,0,1);
// SHA finalization and SDK end are inside the budget even after all writes.
fresh();clock_step=0;sha_finish_advance=900000000;install();
assert(written==1&&write_offset==image_size&&!ended);
assert_failed_safe("OTA deadline e=0x6 off=2048/2048",1,1,1,1);
fresh();clock_step=0;end_advance=900000000;install();assert(ended==1&&written==1);
assert_failed_safe("OTA deadline e=0x6 off=2048/2048",1,1,0,1);
}
static void test_buffer_fallbacks(void){
for(unsigned failures=0;failures<2;failures++){
fresh();clock_step=0;heap_fail_first=failures;image_size=sizeof(image_bytes);header_size=image_size;
unsigned char digest[32];char hex[65];SHA256(image_bytes,image_size,digest);hash_hex(digest,hex);set_number("size",image_size);set_string("sha256",hex);
install();size_t chunk=failures?4096:8192;unsigned count=(image_size+chunk-1)/chunk;
assert(restarts==1&&boot_partition==1&&begun==1&&begin_size==image_size&&read_calls==(int)count&&written==(int)count);
assert(allocated_size==chunk&&alloc_sizes[0]==8192&&(!failures||alloc_sizes[1]==4096));
for(unsigned i=0;i<alloc_calls;i++)assert(alloc_caps[i]==(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT));
for(unsigned i=0;i<count;i++){size_t expected=i+1<count?chunk:image_size-i*chunk;assert(read_wanted[i]==expected&&write_sizes[i]==expected);}
assert(read_offset==image_size&&write_offset==image_size&&!memcmp(flash[1],image_bytes,image_size));
char expected[20];snprintf(expected,sizeof(expected),"buf=%u",(unsigned)chunk);assert(strstr(state.message,expected));
assert_resources_clean(failures+1,1,0,1);
// Verify the large image through the real post-boot flash SHA path too.
reboot_to(1);device_ota_boot_ready();assert(ready&&state.phase==3&&device_ota_version_code()==11);device_ota_poll();assert(reported_success==1);
}
fresh();clock_step=0;heap_fail_first=2;install();
assert(!downloaded&&!opened&&!headers_fetched&&!begun&&!read_calls&&!written);
assert(alloc_sizes[0]==8192&&alloc_sizes[1]==4096);assert_failed_safe("OTA failed:",2,0,0,0);
}
static void test_failure_cleanup(void){
fresh();clock_step=0;http_init_error=1;install();assert(downloaded==1&&!opened&&!begun);assert_failed_safe("OTA failed:",1,1,0,0);
fresh();clock_step=0;open_error=ESP_FAIL;install();assert(opened==1&&!headers_fetched&&!begun);assert_failed_safe("OTA open",1,1,0,1);
for(int bad_header=0;bad_header<2;bad_header++){
fresh();clock_step=0;if(bad_header)header_size--;else http_status=302;install();
assert(headers_fetched==1&&!begun&&!read_calls);assert_failed_safe("OTA headers",1,1,0,1);
}
for(int sha_stage=1;sha_stage<=3;sha_stage++){
fresh();clock_step=0;sha_error_stage=sha_stage;install();assert(begun==1&&!ended);
assert_failed_safe(sha_stage==1?"OTA sha.init":sha_stage==2?"OTA sha.upd":"OTA sha.end",1,1,1,1);
}
fresh();clock_step=0;image_bytes[0]^=1;install();assert(!ended);assert_failed_safe("OTA sha.end e=0x7",1,1,1,1);
fresh();clock_step=0;end_error=1;install();assert(ended==1);assert_failed_safe("OTA end",1,1,0,1);
// Successful download metrics must never conceal later journal or boot failure.
fresh();clock_step=0;fail_save_phase=2;install();assert(ended==1);assert_failed_safe("OTA journal failed:",1,1,0,1);
fresh();clock_step=0;boot_error=1;install();assert(ended==1&&boot_sets==1&&state.phase==4&&!restarts&&boot_partition==0);
assert(strstr(state.message,"OTA boot selection failed:")&&!strstr(state.message,"OTA ms="));assert_resources_clean(1,1,0,1);
}
static void test_success_metrics(void){
fresh();clock_step=0;begin_advance=7000;open_advance=11000;header_advance=13000;end_advance=19000;write_advance=5000;sha_update_advance=3000;
const int reads[]={1024,1024};script_reads(reads,2);read_advances[0]=17000;read_advances[1]=23000;install();
const char *expected="OTA ms=106 erase=7 read=40 write=10 sha=6 buf=8192";
assert(restarts==1&&state.phase==2&&monotonic==106000&&!strcmp(state.message,expected));assert_resources_clean(1,1,0,1);
reboot_to(1);assert(!strcmp(state.message,expected));device_ota_boot_ready();assert(state.phase==3&&!strcmp(state.message,expected));
device_ota_poll();assert(reported_success==1&&!strcmp(last_report_message,expected)&&state.phase==0);
reboot_to(1);device_ota_boot_ready();assert(device_ota_version_code()==11&&!strcmp(state.message,expected));
}
static void test_power_and_boot(void){
fresh();original_record();install();reboot_to(0);device_ota_boot_ready();assert(state.phase==4&&ready&&device_ota_version_code()==10);device_ota_poll();assert(reported_failed==1);
fresh();install();flash[1][0]^=1;reboot_to(1);boot_ready_allow_rollback();assert(rollbacks==1&&boot_partition==0&&!ready);
fresh();install();reboot_to(1);assert(timer_callback&&timer_armed);allow_restart=true;if(!setjmp(restart_jump))timer_callback(NULL);allow_restart=false;assert(restarts==2&&!ready);
fresh();fail_save_phase=2;install();reboot_to(0);fail_save_phase=0;device_ota_boot_ready();assert(ready&&state.phase==4);
}
static void test_metadata_failure(void){
fresh();original_record();install();reboot_to(1);mark_error=1;device_ota_boot_ready();assert(!ready&&timer_armed);mark_error=0;
reboot_to(0);device_ota_boot_ready();assert(ready&&state.phase==4);assert(device_ota_version_code()==10);assert(!strcmp(device_ota_version(),"old-10"));
// Once both slots have been used, failing the next candidate still restores
// the exact version metadata for the previous, already updated slot.
fresh();original_record();install();reboot_to(1);device_ota_boot_ready();device_ota_poll();
task="40000000-0000-4000-8000-000000000002";set_string("task_id",task);set_string("version","new-12");set_number("version_code",12);
install();image_states[0]=ESP_OTA_IMG_PENDING_VERIFY;reboot_to(0);mark_error=1;device_ota_boot_ready();assert(!ready);mark_error=0;
reboot_to(1);device_ota_boot_ready();assert(ready&&device_ota_version_code()==11&&!strcmp(device_ota_version(),"new-11"));
fresh();original_record();install();reboot_to(1);fail_installed=1;device_ota_boot_ready();assert(!ready&&timer_armed);fail_installed=0;reboot_to(0);device_ota_boot_ready();assert(device_ota_version_code()==10);
}
static void test_timer_failure(void){
for(int i=0;i<2;i++){fresh();install();if(i)timer_start_error=1;else timer_create_error=1;
allow_restart=true;if(!setjmp(restart_jump))reboot_to(1);allow_restart=false;assert(rollbacks==1&&boot_partition==0&&!ready);}
}
static void test_numeric_validation(void){
for(int i=0;i<3;i++){fresh();set_number("version_code",i==0?11.5:i==1?1e100:INFINITY);install();assert(!begun&&!restarts);}
}
static void test_corrupt_records(void){
fresh();original_record();fail_readback=1;reboot_to(0);device_ota_boot_ready();assert(!ready&&!device_ota_supported());device_ota_request(task);device_ota_poll();assert(!begun);
fresh();install();for(int i=0;i<record_count;i++)if(!strcmp(records[i].key,"ota"))records[i].size=3;
reboot_to(1);device_ota_boot_ready();assert(!ready&&timer_armed);assert(!marked);
}
static void test_receipt_and_manifest_recovery(void){
fresh();manifest_error=1;install();assert(manifest_requests==1&&!begun&&!maintenance);
manifest_error=0;allow_restart=true;if(!setjmp(restart_jump))device_ota_poll();allow_restart=false;
assert(manifest_requests==2&&restarts==1&&boot_partition==1);
fresh();manifest_error=1;install();device_ota_poll();device_ota_poll();device_ota_poll();
assert(manifest_requests==3&&!begun&&!maintenance);
fresh();manifest_error=1;platform_error="OTA_TASK_UNAVAILABLE";install();device_ota_poll();assert(manifest_requests==1);
fresh();install();reboot_to(1);device_ota_boot_ready();
// Force the terminal receipt's phase=0 commit to fail and retain the journal.
fail_receipt_commit=true;device_ota_poll();assert(state.phase==3);
fail_receipt_commit=false;device_ota_poll();assert(state.phase==0);
fresh();install();reboot_to(1);device_ota_boot_ready();
report_error=1;platform_error="OTA_FINISHED";device_ota_poll();assert(state.phase==0);
fresh();install();reboot_to(1);device_ota_boot_ready();report_error=1;
device_ota_poll();assert(state.phase==3);report_error=0;device_ota_poll();assert(state.phase==0);
}
static void test_internal_finalize(void){
// The first internal operation finalizes an active OTA handle. Allocation
// failure must abort it, whereas an SDK end failure has consumed it already.
fresh();clock_step=0;sem_fail_on=1;install();
assert(ended==0&&aborted==1&&task_attempts==0&&state.phase==4&&boot_partition==0&&!restarts);
assert_resources_clean(1,1,1,1);assert_worker_cleanup();
fresh();clock_step=0;task_fail_on=1;install();
assert(ended==0&&aborted==1&&sem_created==1&&sem_deleted==1&&task_created==0&&state.phase==4&&boot_partition==0&&!restarts);
assert_resources_clean(1,1,1,1);assert_worker_cleanup();
fresh();clock_step=0;end_error=1;install();
assert(ended==1&&aborted==0&&boot_sets==0&&task_created==1&&state.phase==4&&boot_partition==0&&!restarts);
assert_resources_clean(1,1,0,1);assert_worker_cleanup();
// The second operation selects the candidate. It must never abort an ended
// SDK handle or restart after either allocation or boot-selection failure.
fresh();clock_step=0;sem_fail_on=2;install();
assert(ended==1&&aborted==0&&boot_sets==0&&task_created==1&&state.phase==4&&boot_partition==0&&!restarts);
assert(strstr(state.message,"OTA boot selection failed:"));assert_worker_cleanup();
fresh();clock_step=0;task_fail_on=2;install();
assert(ended==1&&aborted==0&&boot_sets==0&&task_created==1&&sem_created==2&&state.phase==4&&boot_partition==0&&!restarts);
assert_worker_cleanup();
fresh();clock_step=0;boot_error=1;install();
assert(ended==1&&aborted==0&&boot_sets==1&&task_created==2&&state.phase==4&&boot_partition==0&&!restarts);
assert_worker_cleanup();
fresh();clock_step=0;install();
assert(ended==1&&boot_sets==1&&restarts==1&&boot_partition==1&&state.phase==2&&task_created==2);
assert_resources_clean(1,1,0,1);assert_worker_cleanup();
// A global deadline reached while releasing TLS prevents verification.
fresh();clock_step=0;cleanup_advance=900000000;install();
assert(ended==0&&aborted==1&&task_attempts==0&&state.phase==4&&strstr(state.message,"OTA deadline"));
assert_resources_clean(1,1,1,1);assert_worker_cleanup();
// If verification consumed the handle before its deadline expires, the
// caller records timeout and retains the old slot without aborting it again.
fresh();clock_step=0;end_advance=900000000;install();
assert(ended==1&&aborted==0&&boot_sets==0&&state.phase==4&&boot_partition==0&&strstr(state.message,"OTA deadline"));
assert_worker_cleanup();
// Completion distinguishes a worker allocation error from a completed SDK
// operation error; callers use this to maintain the handle's ownership.
fresh();bool completed=true;sem_fail_on=1;
assert(ota_internal_flash_call(OTA_FLASH_BOOT,0,&parts[0],&completed)==ESP_ERR_NO_MEM&&!completed);assert_worker_cleanup();
fresh();completed=true;task_fail_on=1;
assert(ota_internal_flash_call(OTA_FLASH_BOOT,0,&parts[0],&completed)==ESP_ERR_NO_MEM&&!completed);assert_worker_cleanup();
fresh();completed=false;boot_error=1;
assert(ota_internal_flash_call(OTA_FLASH_BOOT,0,&parts[0],&completed)==ESP_FAIL&&completed);assert_worker_cleanup();
}
static void test_internal_boot_and_rollback(void){
// First serial-installed image establishes its running slot as fallback.
fresh();clock_step=0;image_states[0]=-1;reboot_to(0);device_ota_boot_ready();
assert(ready&&boot_sets==1&&marked==1&&boot_partition==0&&task_created==1);assert_worker_cleanup();
fresh();image_states[0]=-1;reboot_to(0);task_fail_on=1;device_ota_boot_ready();
assert(!ready&&boot_sets==0&&marked==0&&boot_partition==0);assert_worker_cleanup();
fresh();image_states[0]=-1;reboot_to(0);boot_error=1;device_ota_boot_ready();
assert(!ready&&boot_sets==1&&marked==0&&boot_partition==0);assert_worker_cleanup();
// Corrupt candidate executes rollback internally; reset happens in parent
// only after both worker and semaphore have been deleted.
fresh();install();flash[1][0]^=1;reboot_to(1);boot_ready_allow_rollback();
assert(rollbacks==1&&boot_partition==0&&restarts==2&&!ready&&task_created==3);assert_worker_cleanup();
fresh();install();flash[1][0]^=1;reboot_to(1);rollback_error=1;device_ota_boot_ready();
assert(rollbacks==1&&boot_partition==1&&restarts==1&&!ready&&timer_armed&&state.phase==4);assert_worker_cleanup();
for(int fail=0;fail<2;fail++){
fresh();install();flash[1][0]^=1;reboot_to(1);
if(fail)task_fail_on=task_attempts+1;else sem_fail_on=sem_attempts+1;
device_ota_boot_ready();assert(rollbacks==0&&boot_partition==1&&restarts==1&&!ready&&timer_armed);assert_worker_cleanup();
}
// Failed health-timer setup must use the same safe worker, then restart even
// if explicit rollback fails: the bootloader owns pending-image rollback.
for(int fail=0;fail<2;fail++){
fresh();install();timer_create_error=1;rollback_error=fail;allow_restart=true;
if(!setjmp(restart_jump))reboot_to(1);allow_restart=false;
assert(rollbacks==1&&restarts==2&&!ready&&boot_partition==(fail?1:0));assert_worker_cleanup();
}
fresh();bool completed=false;rollback_error=1;
assert(ota_internal_flash_call(OTA_FLASH_ROLLBACK,0,NULL,&completed)==ESP_FAIL&&completed);
assert(rollbacks==1&&!restarts&&boot_partition==0);assert_worker_cleanup();
}
int main(int argc,char **argv){
const char *pick=argc>1?argv[1]:"all";
if(!strcmp(pick,"all")||!strcmp(pick,"normal"))test_normal();
if(!strcmp(pick,"all")||!strcmp(pick,"rejections"))test_rejections();
if(!strcmp(pick,"all")||!strcmp(pick,"download"))test_download_failures();
if(!strcmp(pick,"all")||!strcmp(pick,"read-recovery"))test_read_recovery();
if(!strcmp(pick,"all")||!strcmp(pick,"read-stall"))test_read_stall_limit();
if(!strcmp(pick,"all")||!strcmp(pick,"read-eof"))test_read_eof_and_error();
if(!strcmp(pick,"all")||!strcmp(pick,"write-offset"))test_write_failure_offset();
if(!strcmp(pick,"all")||!strcmp(pick,"deadline"))test_read_deadline();
if(!strcmp(pick,"all")||!strcmp(pick,"long-download"))test_long_download();
if(!strcmp(pick,"all")||!strcmp(pick,"preerase"))test_known_size_preerase();
if(!strcmp(pick,"all")||!strcmp(pick,"target-guard"))test_target_guard();
if(!strcmp(pick,"all")||!strcmp(pick,"erase-deadline"))test_begin_error_and_deadline();
if(!strcmp(pick,"all")||!strcmp(pick,"setup-deadline"))test_setup_deadlines();
if(!strcmp(pick,"all")||!strcmp(pick,"buffer"))test_buffer_fallbacks();
if(!strcmp(pick,"all")||!strcmp(pick,"cleanup"))test_failure_cleanup();
if(!strcmp(pick,"all")||!strcmp(pick,"metrics"))test_success_metrics();
if(!strcmp(pick,"all")||!strcmp(pick,"power"))test_power_and_boot();
if(!strcmp(pick,"all")||!strcmp(pick,"metadata"))test_metadata_failure();
if(!strcmp(pick,"all")||!strcmp(pick,"timer"))test_timer_failure();
if(!strcmp(pick,"all")||!strcmp(pick,"numeric"))test_numeric_validation();
if(!strcmp(pick,"all")||!strcmp(pick,"corrupt"))test_corrupt_records();
if(!strcmp(pick,"all")||!strcmp(pick,"recovery"))test_receipt_and_manifest_recovery();
if(!strcmp(pick,"all")||!strcmp(pick,"internal-finalize"))test_internal_finalize();
if(!strcmp(pick,"all")||!strcmp(pick,"internal-boot-rollback"))test_internal_boot_and_rollback();
assert_worker_cleanup();assert(!heap_live&&!client_live&&!ota_handle_live&&!sha_live);cJSON_Delete(manifest_json);printf("PASS production OTA: %s\n",pick);return 0;
}