#include #include #include #include #include #include #include using String = std::string; constexpr int HTTP_GET=0, HTTP_POST=1, U_FLASH=0, U_SPIFFS=100; constexpr unsigned UPDATE_SIZE_UNKNOWN=~0U; constexpr int pdPASS=1; constexpr int ERR_OK=0; #define async_tcp_log_d(...) ((void)0) #define pdMS_TO_TICKS(value) (value) const char ELEGANT_HTML[] = "page"; const unsigned ELEGANT_HTML_SIZE=sizeof(ELEGANT_HTML); int Serial=0; bool in_network_callback=false; struct Esp { int restarts=0; void restart() { assert(!in_network_callback); ++restarts; } } ESP; void yield() { assert(!in_network_callback); } void delay(unsigned) { assert(!in_network_callback); } void vTaskDelay(unsigned) { assert(!in_network_callback); } void vTaskDelete(void*) {} using Task = void (*)(void*); Task pending_task=nullptr; bool allow_task=true; int xTaskCreate(Task task, const char*, unsigned, void*, unsigned, void*) { if (!allow_task) return 0; assert(!pending_task); pending_task=task; return pdPASS; } @REBOOT_STATE@ struct Parameter { String text="valid"; String value() { return text; } }; struct Response { int code; String body; void addHeader(const char*, const char*) {} }; unsigned fixture_now=0; struct Client { unsigned _rx_timeout=0, _rx_last_packet=fixture_now; bool closed=false; std::vector timeout_changes; std::function disconnected; void setRxTimeout(unsigned seconds) { _rx_timeout=seconds; timeout_changes.push_back(seconds); } void received() { _rx_last_packet=fixture_now; } void _close() { closed=true; if (disconnected) disconnected(); } int poll() { const unsigned now=fixture_now; @RX_TIMEOUT@ return ERR_OK; } }; void accept(Client* c) { @ACCEPT_TIMEOUT@ } struct Request { void* _tempObject=nullptr; bool md5_present=true, authorized=true, sent=false; int code=0; String body; Response* queued_response=nullptr; std::function disconnected; Parameter md5; Client connection; Request() { accept(&connection); } ~Request() { free(_tempObject); delete queued_response; } Client* client() { return &connection; } bool authenticate(const char*, const char*) { return authorized; } void requestAuthentication() { send(401,"text/plain","unauthorized"); } bool hasParam(const char*, bool) { return md5_present; } Parameter* getParam(const char*, bool) { return &md5; } Response* beginResponse(int code, const char*, const char* text) { return new Response{code,text}; } Response* beginResponse_P(int code, const char*, const char* text, unsigned) { return new Response{code,text}; } void send(Response* response) { delete queued_response; queued_response=response; code=response->code; body=response->body; } void send(int status, const char*, const char* text) { send(new Response{status,text}); } void send(int status, const char*, const String& text) { send(new Response{status,text}); } Response* getResponse() const { return queued_response; } void flush_response() { assert(queued_response); sent=true; connection.setRxTimeout(0); } bool isSent() const { return sent; } void onDisconnect(std::function fn) { disconnected=fn; connection.disconnected=fn; } }; using AsyncWebServerRequest=Request; static AsyncWebServerRequest* ota_upload_owner=nullptr; using AsyncWebServerResponse=Response; struct AsyncWebServer { std::function completion; std::function upload; template void on(const char*, int, F) {} template void on(const char*, int method, F callback, U chunks) { assert(method==HTTP_POST); completion=callback; upload=chunks; } }; struct UpdateFake { bool error=false, fail_begin=false, fail_end=false, fail_write=false; int aborts=0, writes=0; bool hasError() const { return error; } bool setMD5(const char* md5) { return String(md5)=="valid"; } bool begin(unsigned, int) { error=fail_begin; return !error; } size_t write(uint8_t*, size_t size) { ++writes; error=fail_write; return fail_write ? size-1 : size; } bool end(bool) { error=fail_end; return !error; } void abort() { error=true; ++aborts; } // A connected USB host which does not drain TX can block raw Serial. Every // upload error path must complete without attempting that raw print. void printError(int) { assert(false && "Raw USB printing can block OTA callbacks"); } } Update; struct AsyncElegantOtaClass { AsyncWebServer* _server=nullptr; bool _authRequired=false; String _username, _password, _id="fixture"; void begin(AsyncWebServer*, const char*, const char*); void restart(); bool setEnabled(bool); }; @BEGIN@ @RESTART@ int main() { AsyncWebServer server; AsyncElegantOtaClass ota; ota.begin(&server,"",""); auto complete=[&](Request& request) { in_network_callback=true; server.completion(&request); request.flush_response(); in_network_callback=false; }; auto upload=[&](Request& request) { uint8_t data[8]={}; in_network_callback=true; server.upload(&request,"firmware",0,data,sizeof(data),true); in_network_callback=false; }; #ifdef TEST_OTA_BODY_TIMEOUT { uint8_t chunk[4]={}; auto receive=[&](Request& request,size_t index,bool final) { assert(!request.connection.closed); request.connection.received(); in_network_callback=true; server.upload(&request,"firmware",index,chunk,sizeof(chunk),final); in_network_callback=false; }; auto idle=[&](Request& request,unsigned ms) { fixture_now+=ms; in_network_callback=true; request.connection.poll(); in_network_callback=false; }; Request ordinary; assert(ordinary.connection._rx_timeout==3); idle(ordinary,4000); assert(ordinary.connection.closed && !ota_upload_owner); Request admitted; receive(admitted,0,false); assert(ota_upload_owner==&admitted && !admitted.sent && admitted.connection.timeout_changes==std::vector({3,30})); Request concurrent; upload(concurrent); assert(!concurrent.isSent() && concurrent.getResponse() && concurrent.connection.timeout_changes==std::vector({3})); complete(concurrent); assert(concurrent.code==409 && ota_upload_owner==&admitted && concurrent.connection.timeout_changes==std::vector({3,0})); Request untouched; assert(untouched.connection._rx_timeout==3); idle(admitted,4000); assert(!admitted.connection.closed && ota_upload_owner==&admitted); idle(admitted,25999); assert(!admitted.connection.closed); idle(admitted,1); assert(admitted.connection.closed && Update.aborts==1 && !ota_upload_owner && !ota_upload_busy.load() && !pending_task && ESP.restarts==0); Request missing_md5; missing_md5.md5_present=false; upload(missing_md5); assert(!missing_md5.isSent() && missing_md5.getResponse() && missing_md5.connection.timeout_changes==std::vector({3})); complete(missing_md5); assert(missing_md5.code==400 && missing_md5.connection.timeout_changes==std::vector({3,0}) && !ota_upload_owner && !ota_upload_busy.load()); Request bad_md5; bad_md5.md5.text="invalid"; upload(bad_md5); complete(bad_md5); assert(bad_md5.code==400 && !ota_upload_owner && !ota_upload_busy.load() && bad_md5.connection.timeout_changes==std::vector({3,0})); ota.begin(&server,"admin","password"); Request unauthorized; unauthorized.authorized=false; upload(unauthorized); complete(unauthorized); assert(unauthorized.code==401 && !ota_upload_owner && !ota_upload_busy.load() && unauthorized.connection.timeout_changes==std::vector({3,0})); ota.begin(&server,"",""); Request successful; receive(successful,0,false); idle(successful,4000); assert(!successful.connection.closed && ota_upload_owner==&successful); receive(successful,sizeof(chunk),true); complete(successful); assert(successful.code==200 && successful.body=="OK" && successful.connection._rx_timeout==0 && !pending_task); in_network_callback=true; successful.disconnected(); in_network_callback=false; assert(pending_task && !ota_upload_owner); auto task=pending_task; pending_task=nullptr; task(nullptr); assert(ESP.restarts==1); return 0; } #endif // A POST without an actual completed upload cannot report OK or reboot. Request empty; complete(empty); assert(empty.code>=400 && !pending_task && ESP.restarts==0); Request invalid; invalid.md5_present=false; upload(invalid); complete(invalid); assert(invalid.code==400 && !pending_task && ESP.restarts==0); Request begin_failed; Update.fail_begin=true; upload(begin_failed); complete(begin_failed); assert(begin_failed.code==400 && begin_failed.body=="OTA could not begin" && !ota_upload_owner && !ota_upload_busy.load() && Update.aborts==0 && !pending_task && ESP.restarts==0); Update.fail_begin=false; Request corrupt; Update.fail_end=true; upload(corrupt); complete(corrupt); assert(corrupt.code==400 && !pending_task && ESP.restarts==0); in_network_callback=true; corrupt.disconnected(); in_network_callback=false; assert(Update.aborts==1 && ota_upload_owner==nullptr); Update.fail_end=false; Request partial; uint8_t chunk[4]={}; server.upload(&partial,"firmware",0,chunk,sizeof(chunk),false); assert(!ota.setEnabled(false)); // An admitted upload keeps its listener alive. Request concurrent; upload(concurrent); complete(concurrent); assert(concurrent.code==409 && ota_upload_owner==&partial); in_network_callback=true; partial.disconnected(); in_network_callback=false; assert(Update.aborts==2 && ota_upload_owner==nullptr && ESP.restarts==0); assert(ota.setEnabled(false)); Request stopped; upload(stopped); complete(stopped); assert(stopped.code==503 && !ota_upload_owner && !pending_task); assert(ota.setEnabled(true)); Request write_failed; Update.fail_write=true; server.upload(&write_failed,"firmware",0,chunk,sizeof(chunk),false); assert(!write_failed.isSent() && write_failed.getResponse() && write_failed.code==400 && write_failed.body=="OTA flash write failed"); const int writes_after_error=Update.writes; server.upload(&write_failed,"firmware",sizeof(chunk),chunk,sizeof(chunk),true); assert(Update.writes==writes_after_error); server.upload(&write_failed,"firmware",0,chunk,sizeof(chunk),false); assert(Update.writes==writes_after_error && write_failed.code==400 && write_failed.body=="OTA flash write failed"); complete(write_failed); assert(write_failed.code==400 && write_failed.body=="OTA flash write failed" && !pending_task); in_network_callback=true; write_failed.disconnected(); in_network_callback=false; assert(Update.aborts==3 && !ota_upload_owner && !ota_upload_busy.load()); Update.fail_write=false; Request valid; upload(valid); complete(valid); assert(valid.code==200 && valid.disconnected && !pending_task && ESP.restarts==0); // The response drains before disconnect; its callback only schedules work. in_network_callback=true; valid.disconnected(); in_network_callback=false; assert(pending_task && ESP.restarts==0); auto task=pending_task; pending_task=nullptr; task(nullptr); assert(ESP.restarts==1); }