Implement HTTPS lifecycle and reboot controls
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
/* Compiled after real web_server lifecycle. HTTP/auth/scheduler dependencies are
|
||||
* doubles; ACK/operation functions below are extracted verbatim from production. */
|
||||
typedef uint64_t web_session_id_t;
|
||||
typedef struct { unsigned role; } user_principal_t;
|
||||
#define USER_ROLE_ADMIN 1
|
||||
#define portMUX_INITIALIZER_UNLOCKED 0
|
||||
typedef unsigned portMUX_TYPE;
|
||||
#define taskENTER_CRITICAL(lock) do { assert(!*(lock)); ++*(lock); } while (0)
|
||||
#define taskEXIT_CRITICAL(lock) do { assert(*(lock) == 1); --*(lock); } while (0)
|
||||
static int64_t pipeline_now;
|
||||
static bool pipeline_current = true, pipeline_queue_fail;
|
||||
static unsigned pipeline_reboots, pipeline_submits;
|
||||
static uint32_t pipeline_id;
|
||||
static void (*validation_hook)(void);
|
||||
static int64_t esp_timer_get_time(void) { return pipeline_now; }
|
||||
static esp_err_t web_session_store_check_principal(web_session_id_t session, const user_principal_t *principal, bool *current) {
|
||||
assert(!locked && session == 1 && principal->role == USER_ROLE_ADMIN);
|
||||
if (validation_hook) { void (*hook)(void) = validation_hook; validation_hook = NULL; hook(); }
|
||||
*current = pipeline_current && auth_live; return ESP_OK;
|
||||
}
|
||||
static esp_err_t admin_ssh_console_submit_lifecycle_settings(uint32_t id) {
|
||||
assert(!locked && id); ++pipeline_submits;
|
||||
if (pipeline_queue_fail) return ESP_ERR_TIMEOUT;
|
||||
pipeline_id = id; return ESP_OK;
|
||||
}
|
||||
static void esp_restart(void) {
|
||||
assert(!locked && s_transitioning && s_server == SERVER);
|
||||
++pipeline_reboots;
|
||||
}
|
||||
/* PRODUCTION_PIPELINE */
|
||||
static void pipeline_reset(void) {
|
||||
memset(&s_operation, 0, sizeof(s_operation)); s_ack_id = 0; s_ack_server = NULL;
|
||||
pipeline_now = 0; pipeline_current = true; pipeline_queue_fail = false;
|
||||
pipeline_reboots = pipeline_submits = 0; pipeline_id = 0; validation_hook = NULL;
|
||||
reset(); start();
|
||||
}
|
||||
static uint32_t pipeline_admit(unsigned action) {
|
||||
assert(s_operation.state != PENDING && s_operation.state != EXECUTING && !s_ack_id);
|
||||
s_operation = (lifecycle_operation_t){.id=++s_next_id, .generation=s_generation,
|
||||
.session=1, .principal={USER_ROLE_ADMIN}, .ack_deadline=pipeline_now+2000000,
|
||||
.deadline=pipeline_now+30000000, .action=action, .state=PENDING};
|
||||
s_ack_id = s_operation.id; s_ack_server = SERVER;
|
||||
return s_operation.id;
|
||||
}
|
||||
static void pipeline_callback(uint32_t id) {
|
||||
unsigned starts = ssl_starts, stops = ssl_stops, reboots = pipeline_reboots;
|
||||
ack_handoff((void *)(uintptr_t)id);
|
||||
assert(ssl_starts == starts && ssl_stops == stops && pipeline_reboots == reboots);
|
||||
}
|
||||
static void validation_aba(void) {
|
||||
assert(web_server_stop() == ESP_OK); fresh_registration(); start();
|
||||
}
|
||||
static void pipeline_tests(void) {
|
||||
for (unsigned action = 0; action < 3; ++action) {
|
||||
pipeline_reset(); uint32_t id = pipeline_admit(action), generation = s_generation;
|
||||
web_lifecycle_settings_execute(id); assert(!ssl_stops && !pipeline_reboots);
|
||||
pipeline_callback(id); assert(pipeline_id == id && s_operation.queued && !s_ack_id);
|
||||
pipeline_callback(id); assert(pipeline_submits == 1);
|
||||
if (action == 1) fresh_registration();
|
||||
web_lifecycle_settings_execute(id);
|
||||
assert(s_operation.state == (action == 2 ? FAILED : OK));
|
||||
assert(s_generation == generation + (action == 1 ? 2 : 1));
|
||||
if (action == 0) assert(!s_server && !auth_live && !ssl_live);
|
||||
if (action == 1) assert(auth_live && ssl_live && !s_transitioning && ssl_starts == 2);
|
||||
if (action == 2) assert(pipeline_reboots == 1 && s_transitioning && !ssl_stops);
|
||||
web_lifecycle_settings_execute(id); pipeline_callback(id);
|
||||
assert(pipeline_submits == 1 && pipeline_reboots == (action == 2 ? 1U : 0U));
|
||||
}
|
||||
puts("PASS real ACK-ID-dispatch-to-canonical stop/reserved restart/reboot; no lifecycle on callback or duplicate IDs");
|
||||
|
||||
for (unsigned failure = 0; failure < 4; ++failure) {
|
||||
pipeline_reset(); uint32_t id = pipeline_admit(1);
|
||||
if (failure == 0) idle_detach_error = ESP_ERR_TIMEOUT;
|
||||
if (failure == 1) admin_detach_error = ESP_ERR_TIMEOUT;
|
||||
if (failure == 2) serial_detach_error = ESP_ERR_INVALID_STATE;
|
||||
if (failure == 3) ssl_stop_error = ESP_FAIL;
|
||||
pipeline_callback(id); web_lifecycle_settings_execute(id);
|
||||
assert(s_operation.state == FAILED && s_server == SERVER && !auth_live && ssl_starts == 1);
|
||||
assert(!s_transitioning && !idle_stoppeds && !admin_stoppeds);
|
||||
web_lifecycle_settings_execute(id); assert(ssl_starts == 1);
|
||||
idle_detach_error = admin_detach_error = serial_detach_error = ssl_stop_error = ESP_OK;
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
puts("PASS real ACK dispatcher lifecycle failure retains owners, skips restart and never mislabels admitted invalid-state as cancellation");
|
||||
|
||||
pipeline_reset(); uint32_t old = pipeline_admit(0); pipeline_now = 2000000;
|
||||
expire_locked(pipeline_now); assert(s_operation.state == CANCELLED && s_ack_id == old);
|
||||
ssl_stop_error = ESP_FAIL; assert(web_server_stop() == ESP_FAIL && s_ack_id == old);
|
||||
ssl_stop_error = ESP_OK; assert(web_server_stop() == ESP_OK && !s_ack_id);
|
||||
fresh_registration(); start(); uint32_t next = pipeline_admit(1);
|
||||
pipeline_callback(old); assert(s_ack_id == next && !pipeline_submits);
|
||||
pipeline_callback(next); fresh_registration(); web_lifecycle_settings_execute(next);
|
||||
assert(s_operation.state == OK && ssl_starts == 3);
|
||||
puts("PASS lost/delayed ACK reservation survives failed stop, retires only after successful destruction; same-handle callback ABA inert");
|
||||
|
||||
for (unsigned mode = 0; mode < 5; ++mode) {
|
||||
pipeline_reset(); uint32_t id = pipeline_admit(2);
|
||||
if (mode == 0) pipeline_now = 2000000;
|
||||
if (mode == 1) pipeline_queue_fail = true;
|
||||
pipeline_callback(id);
|
||||
if (mode == 2) pipeline_current = false;
|
||||
if (mode == 3) pipeline_now = 30000000;
|
||||
if (mode == 4) validation_hook = validation_aba;
|
||||
web_lifecycle_settings_execute(id);
|
||||
assert(!pipeline_reboots && s_operation.state == (mode == 4 ? FAILED : CANCELLED));
|
||||
assert(ssl_starts == (mode == 4 ? 2U : 1U));
|
||||
}
|
||||
puts("PASS actual reboot denied on ACK/dequeue expiry, queue failure, revoked login and canonical ABA during validation");
|
||||
|
||||
pipeline_reset(); uint32_t generation = s_generation;
|
||||
assert(web_server_reboot_current(0) == ESP_ERR_INVALID_ARG);
|
||||
mutex_busy = true; assert(web_server_reboot_current(generation) == ESP_ERR_TIMEOUT); mutex_busy = false;
|
||||
assert(web_server_reboot_current(generation + 1) == ESP_ERR_INVALID_STATE);
|
||||
s_transitioning = true; assert(web_server_reboot_current(generation) == ESP_ERR_INVALID_STATE); s_transitioning = false;
|
||||
s_last_error = ESP_FAIL; assert(web_server_reboot_current(generation) == ESP_ERR_INVALID_STATE); s_last_error = ESP_OK;
|
||||
s_generation = UINT32_MAX; assert(web_server_reboot_current(UINT32_MAX) == ESP_ERR_INVALID_STATE);
|
||||
s_generation = generation; assert(!pipeline_reboots);
|
||||
assert(web_server_reboot_current(generation) == ESP_FAIL && pipeline_reboots == 1);
|
||||
assert(web_server_reboot_current(generation + 1) == ESP_ERR_INVALID_STATE && pipeline_reboots == 1);
|
||||
assert(web_server_start() == ESP_ERR_INVALID_STATE && web_server_stop() == ESP_ERR_INVALID_STATE);
|
||||
puts("PASS canonical reboot zero-wait/current-generation admission reserves transition before esp_restart and cannot duplicate on unexpected return");
|
||||
|
||||
for (unsigned failure = 1; failure <= 6; ++failure) {
|
||||
reset(); lifecycle_fail_at = failure; start();
|
||||
unsigned failed_route = (failure + 1) / 2;
|
||||
assert(lifecycle_calls == failed_route && lifecycle_allocations == failure);
|
||||
assert(registered_count == (failed_route == 1 ? 36 : 37));
|
||||
assert(!method_route("/api/settings/lifecycle-operation", HTTP_POST));
|
||||
assert(!method_route("/api/settings/lifecycle-operation", HTTP_GET));
|
||||
assert(!!method_route("/api/settings/lifecycle", HTTP_GET) == (failed_route != 1));
|
||||
other_domains_complete(); network_complete(); display_complete(); broker_complete(); ssh_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
lifecycle_fail_at = 0; fresh_registration(); start(); assert(registered_count == 39);
|
||||
assert(method_route("/api/settings/lifecycle-operation", HTTP_POST)->handler == web_lifecycle_operation_handler);
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
puts("PASS all six lifecycle route allocation positions preserve other domains and restart recovers the complete optional API");
|
||||
for (unsigned failure = 5; failure <= 6; ++failure) {
|
||||
reset(); lifecycle_fail_at = failure; unregister_fail = true; start();
|
||||
assert(registered_count == 38 && unregister_calls == 1);
|
||||
assert(method_route("/api/settings/lifecycle-operation", HTTP_GET));
|
||||
assert(!method_route("/api/settings/lifecycle-operation", HTTP_POST));
|
||||
other_domains_complete(); ssh_complete();
|
||||
ssl_stop_error = ESP_FAIL; assert(web_server_stop() == ESP_FAIL && s_server == SERVER);
|
||||
assert(web_server_start() == ESP_ERR_INVALID_STATE);
|
||||
ssl_stop_error = ESP_OK; assert(web_server_stop() == ESP_OK);
|
||||
unregister_fail = false; lifecycle_fail_at = 0; fresh_registration(); start();
|
||||
assert(registered_count == 39 && web_server_stop() == ESP_OK);
|
||||
}
|
||||
puts("PASS lifecycle failed unregister leaves reads only; failed shutdown preserves ownership before successful restart");
|
||||
}
|
||||
@@ -19,7 +19,7 @@ source = SOURCE.read_text()
|
||||
|
||||
|
||||
def function(name):
|
||||
match = re.search(r'^(?:static )?esp_err_t ' + name + r'\(void\)\n\{.*?^\}',
|
||||
match = re.search(r'^(?:static )?esp_err_t ' + name + r'\([^\n]*\)\n\{.*?^\}',
|
||||
source, re.M | re.S)
|
||||
if not match:
|
||||
raise RuntimeError('Production function shape changed: ' + name)
|
||||
@@ -37,8 +37,8 @@ def define(path, name):
|
||||
uri_tables = re.findall(r'^static const httpd_uri_t(?: \*const)? \w+\[?\]? = \{.*?^\};',
|
||||
source, re.M | re.S)
|
||||
# Non-array declarations have no brackets; explicit shape avoids silent omission.
|
||||
if len(uri_tables) != 33:
|
||||
raise RuntimeError('Review URI extraction: expected 31 descriptors and two tables')
|
||||
if len(uri_tables) != 36:
|
||||
raise RuntimeError('Review URI extraction: expected 34 descriptors and two tables')
|
||||
state = source[source.index('static SemaphoreHandle_t s_server_mutex;'):
|
||||
source.index('static esp_err_t ensure_mutex(void)')]
|
||||
header = (ROOT / 'src/web_server.h').read_text()
|
||||
@@ -61,7 +61,8 @@ FAKES = r'''
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
typedef int esp_err_t;
|
||||
enum { ESP_OK, ESP_FAIL, ESP_ERR_INVALID_STATE, ESP_ERR_NO_MEM, ESP_ERR_TIMEOUT };
|
||||
enum { ESP_OK, ESP_FAIL, ESP_ERR_INVALID_STATE, ESP_ERR_NO_MEM, ESP_ERR_TIMEOUT, ESP_ERR_INVALID_ARG };
|
||||
#define pdTRUE 1
|
||||
typedef void *SemaphoreHandle_t;
|
||||
typedef void *httpd_handle_t;
|
||||
typedef struct { int unused; } httpd_req_t;
|
||||
@@ -83,10 +84,16 @@ typedef struct {
|
||||
} httpd_ssl_config_t;
|
||||
/* Nonproduction defaults deliberately make explicit overrides observable. */
|
||||
#define HTTPD_SSL_CONFIG_DEFAULT() ((httpd_ssl_config_t){.httpd = {.max_open_sockets = 1, .lru_purge_enable = true}})
|
||||
#define portMAX_DELAY 0
|
||||
#define portMAX_DELAY 99
|
||||
static int mutex_storage, server_storage, locked;
|
||||
#define SERVER ((void *)&server_storage)
|
||||
static bool mutex_fail, auth_live, ssl_live, admin_owned, serial_live;
|
||||
static void esp_restart(void);
|
||||
void web_lifecycle_settings_stopped(httpd_handle_t server);
|
||||
static bool mutex_fail, auth_live, ssl_live, admin_owned, serial_live, mutex_busy;
|
||||
static void (*unlock_hook)(void);
|
||||
static esp_err_t serial_detach_error;
|
||||
static unsigned ssl_stop_fail_at;
|
||||
static void web_cookie_auth_clear_counters(void) {}
|
||||
static esp_err_t serial_init_error, admin_init_error, admin_attach_error;
|
||||
static esp_err_t auth_error, ssl_start_error, ssl_stop_error, admin_detach_error;
|
||||
static unsigned serial_inits, admin_inits, auth_starts, auth_stops;
|
||||
@@ -97,12 +104,18 @@ static bool unregister_fail;
|
||||
static bool settings_fail;
|
||||
static unsigned settings_calls;
|
||||
static unsigned operation_calls, operation_fail_at;
|
||||
static const httpd_uri_t *registered[36];
|
||||
static const httpd_uri_t *registered[39];
|
||||
static char events[128]; static size_t event_length;
|
||||
static void event(char value) { assert(!locked && event_length + 1 < sizeof(events)); events[event_length++] = value; events[event_length] = 0; }
|
||||
static SemaphoreHandle_t xSemaphoreCreateMutex(void) { assert(!locked); return mutex_fail ? NULL : &mutex_storage; }
|
||||
static void xSemaphoreTake(SemaphoreHandle_t m, int wait) { (void)wait; assert(m && !locked); locked = 1; }
|
||||
static void xSemaphoreGive(SemaphoreHandle_t m) { assert(m && locked); locked = 0; }
|
||||
static int xSemaphoreTake(SemaphoreHandle_t m, int wait) {
|
||||
assert(m && !locked);
|
||||
if (mutex_busy) { assert(wait == 0); return 0; }
|
||||
locked = 1; return pdTRUE;
|
||||
}
|
||||
static void xSemaphoreGive(SemaphoreHandle_t m) {
|
||||
assert(m && locked); locked = 0; if (unlock_hook) unlock_hook();
|
||||
}
|
||||
static void secure_wipe(void *p, size_t n) { assert(!locked); memset(p, 0, n); }
|
||||
#define HANDLER(name) static esp_err_t name(httpd_req_t *r) { (void)r; assert(!"HTTP handler must not run in lifecycle harness"); return ESP_FAIL; }
|
||||
HANDLER(root_handler) HANDLER(status_handler) HANDLER(traced_ticket_handler)
|
||||
@@ -154,6 +167,25 @@ static esp_err_t display_register(httpd_handle_t s, const httpd_uri_t *uri) {
|
||||
registered[registered_count++] = uri;
|
||||
return ESP_OK;
|
||||
}
|
||||
HANDLER(web_lifecycle_settings_handler) HANDLER(web_lifecycle_operation_handler)
|
||||
static unsigned lifecycle_calls, lifecycle_allocations, lifecycle_fail_at;
|
||||
static esp_err_t lifecycle_register(httpd_handle_t s, const httpd_uri_t *uri) {
|
||||
assert(s == SERVER && auth_live && ssl_live && !locked);
|
||||
assert(!uri->is_websocket && !uri->handle_ws_control_frames && !uri->user_ctx);
|
||||
++lifecycle_calls;
|
||||
if (lifecycle_calls == 1) {
|
||||
assert(!strcmp(uri->uri, "/api/settings/lifecycle") && uri->method == HTTP_GET);
|
||||
assert(uri->handler == web_lifecycle_settings_handler);
|
||||
} else {
|
||||
assert(!strcmp(uri->uri, "/api/settings/lifecycle-operation"));
|
||||
assert(uri->method == (lifecycle_calls == 2 ? HTTP_GET : HTTP_POST));
|
||||
assert(uri->handler == web_lifecycle_operation_handler && lifecycle_calls <= 3);
|
||||
}
|
||||
for (unsigned allocation = 0; allocation < 2; ++allocation)
|
||||
if (++lifecycle_allocations == lifecycle_fail_at) return ESP_ERR_NO_MEM;
|
||||
registered[registered_count++] = uri;
|
||||
return ESP_OK;
|
||||
}
|
||||
HANDLER(web_ssh_settings_handler) HANDLER(web_ssh_operation_handler)
|
||||
static unsigned ssh_calls, ssh_allocations, ssh_fail_at;
|
||||
static esp_err_t ssh_register(httpd_handle_t s, const httpd_uri_t *uri) {
|
||||
@@ -229,7 +261,7 @@ static esp_err_t web_security_copy_tls_material(uint8_t *cert, size_t nc, size_t
|
||||
static esp_err_t httpd_ssl_start(httpd_handle_t *server, const httpd_ssl_config_t *config) {
|
||||
assert(!locked && auth_live && !ssl_live); ++ssl_starts;
|
||||
assert(config->httpd.max_open_sockets == 6 && !config->httpd.lru_purge_enable);
|
||||
assert(config->httpd.max_uri_handlers == 36 && config->port_secure == 443);
|
||||
assert(config->httpd.max_uri_handlers == 39 && config->port_secure == 443);
|
||||
assert(config->httpd.recv_wait_timeout == 1 && config->httpd.send_wait_timeout == 1);
|
||||
assert(config->tls_handshake_timeout_ms == 5000);
|
||||
assert(config->user_cb == tls_session_callback);
|
||||
@@ -257,7 +289,7 @@ static esp_err_t httpd_register_uri_handler(httpd_handle_t s, const httpd_uri_t
|
||||
assert(serial_init_error != ESP_OK || serial_live);
|
||||
} else assert(registration_calls < 14);
|
||||
esp_err_t error = register_one(s);
|
||||
if (error == ESP_OK) { assert(registered_count < 36); registered[registered_count++] = uri; }
|
||||
if (error == ESP_OK) { assert(registered_count < 39); registered[registered_count++] = uri; }
|
||||
return error;
|
||||
}
|
||||
static esp_err_t account_register(httpd_handle_t s, const httpd_uri_t *uri) {
|
||||
@@ -267,6 +299,7 @@ static esp_err_t account_register(httpd_handle_t s, const httpd_uri_t *uri) {
|
||||
}
|
||||
static esp_err_t web_httpd_register_optional_get(httpd_handle_t s, const httpd_uri_t *uri) {
|
||||
assert(uri->method == HTTP_GET);
|
||||
if (uri->handler == web_lifecycle_settings_handler || uri->handler == web_lifecycle_operation_handler) return lifecycle_register(s, uri);
|
||||
if (uri->handler == web_ssh_settings_handler || uri->handler == web_ssh_operation_handler) return ssh_register(s, uri);
|
||||
if (uri->handler == web_broker_settings_handler || uri->handler == web_broker_operation_handler) return broker_register(s, uri);
|
||||
if (uri->handler == web_display_settings_handler || uri->handler == web_display_operation_handler) return display_register(s, uri);
|
||||
@@ -277,6 +310,7 @@ static esp_err_t web_httpd_register_optional_get(httpd_handle_t s, const httpd_u
|
||||
return httpd_register_uri_handler(s, uri);
|
||||
}
|
||||
static esp_err_t web_httpd_register_optional(httpd_handle_t s, const httpd_uri_t *uri) {
|
||||
if (uri->handler == web_lifecycle_settings_handler || uri->handler == web_lifecycle_operation_handler) return lifecycle_register(s, uri);
|
||||
if (uri->handler == web_ssh_settings_handler || uri->handler == web_ssh_operation_handler) return ssh_register(s, uri);
|
||||
if (uri->handler == web_broker_settings_handler || uri->handler == web_broker_operation_handler) return broker_register(s, uri);
|
||||
if (uri->handler == web_display_operation_handler) return display_register(s, uri);
|
||||
@@ -306,7 +340,7 @@ static esp_err_t web_httpd_register_optional(httpd_handle_t s, const httpd_uri_t
|
||||
static esp_err_t httpd_unregister_uri_handler(httpd_handle_t s, const char *uri, int method) {
|
||||
assert(!locked && s == SERVER && ssl_live && auth_live && serial_live);
|
||||
assert((registration_calls == 18 && !strcmp(uri, "/api/admin/ws-ticket") && method == HTTP_POST) ||
|
||||
((!strcmp(uri, "/api/settings/serial-operation") || !strcmp(uri, "/api/settings/account-operation") || !strcmp(uri, "/api/settings/network-operation") || !strcmp(uri, "/api/settings/display-operation") || !strcmp(uri, "/api/settings/broker-operation") || !strcmp(uri, "/api/settings/ssh-operation")) && method == HTTP_GET));
|
||||
((!strcmp(uri, "/api/settings/serial-operation") || !strcmp(uri, "/api/settings/account-operation") || !strcmp(uri, "/api/settings/network-operation") || !strcmp(uri, "/api/settings/display-operation") || !strcmp(uri, "/api/settings/broker-operation") || !strcmp(uri, "/api/settings/ssh-operation") || !strcmp(uri, "/api/settings/lifecycle-operation")) && method == HTTP_GET));
|
||||
++unregister_calls;
|
||||
for (unsigned i = 0; i < registered_count; ++i) {
|
||||
if (!strcmp(registered[i]->uri, uri) && registered[i]->method == method) {
|
||||
@@ -341,10 +375,13 @@ static esp_err_t web_admin_transport_detach(httpd_handle_t s) {
|
||||
}
|
||||
static esp_err_t web_serial_transport_detach_server(httpd_handle_t s) {
|
||||
assert(s == SERVER && ssl_live && serial_live && !auth_live);
|
||||
event('S'); ++serial_detaches; serial_live = false; return ESP_OK;
|
||||
event('S'); ++serial_detaches;
|
||||
if (serial_detach_error != ESP_OK && serial_detach_error != ESP_ERR_TIMEOUT) return serial_detach_error;
|
||||
serial_live = false; return serial_detach_error;
|
||||
}
|
||||
static esp_err_t httpd_ssl_stop(httpd_handle_t s) {
|
||||
assert(s == SERVER && ssl_live && !auth_live && idle_fenced); event('H'); ++ssl_stops;
|
||||
if (ssl_stops == ssl_stop_fail_at) return ESP_FAIL;
|
||||
if (ssl_stop_error == ESP_OK) ssl_live = false;
|
||||
return ssl_stop_error;
|
||||
}
|
||||
@@ -359,6 +396,8 @@ static void clear_events(void) { event_length = 0; events[0] = 0; }
|
||||
static void reset(void) {
|
||||
assert(!locked);
|
||||
s_server_mutex = NULL; s_server = NULL; s_initialized = s_transitioning = false;
|
||||
s_generation = 1U; mutex_busy = false; unlock_hook = NULL; serial_detach_error = ESP_OK;
|
||||
ssl_stop_fail_at = 0;
|
||||
s_serial_transport_init_attempted = s_serial_transport_initialized = false;
|
||||
s_serial_transport_attached = s_admin_transport_owned = false;
|
||||
s_last_error = s_serial_transport_error = ESP_ERR_INVALID_STATE;
|
||||
@@ -378,6 +417,7 @@ static void reset(void) {
|
||||
display_calls = display_allocations = display_fail_at = 0;
|
||||
broker_calls = broker_allocations = broker_fail_at = 0;
|
||||
ssh_calls = ssh_allocations = ssh_fail_at = 0;
|
||||
lifecycle_calls = lifecycle_allocations = lifecycle_fail_at = 0;
|
||||
account_calls = account_fail_at = generation_calls = keys_calls = 0;
|
||||
generation_fail = keys_fail = false;
|
||||
}
|
||||
@@ -387,6 +427,7 @@ static void fresh_registration(void) {
|
||||
display_calls = display_allocations = 0;
|
||||
broker_calls = broker_allocations = 0;
|
||||
ssh_calls = ssh_allocations = 0;
|
||||
lifecycle_calls = lifecycle_allocations = 0;
|
||||
}
|
||||
static void start(void) {
|
||||
assert(web_server_start() == ESP_OK);
|
||||
@@ -474,7 +515,7 @@ int main(void) {
|
||||
}
|
||||
puts("PASS optional admin init/attach failures do not disable M1 auth or serial attachment");
|
||||
|
||||
reset(); start(); assert(registered_count == 36 && registration_calls == 18 && settings_calls == 1 && operation_calls == 2);
|
||||
reset(); start(); assert(registered_count == 39 && registration_calls == 18 && settings_calls == 1 && operation_calls == 2);
|
||||
assert(generation_calls == 1 && route("/api/settings/accounts/generate-password")->handler == web_account_generate_password_handler);
|
||||
assert(route("/api/settings/serial")->handler == serial_settings_handler);
|
||||
assert(keys_calls == 1 && route("/api/settings/accounts/keys")->handler == web_account_keys_handler);
|
||||
@@ -528,7 +569,7 @@ int main(void) {
|
||||
assert(s_serial_transport_attached && !s_admin_transport_owned && !admin_owned);
|
||||
assert(!admin_inits && !admin_attaches && !auth_stops && !ssl_stops);
|
||||
assert(!s_transitioning && s_last_error == ESP_OK && s_counters.starts == 1 && !s_counters.start_failures);
|
||||
assert(registered_count == 34 && unregister_calls == failure - 17);
|
||||
assert(registered_count == 37 && unregister_calls == failure - 17);
|
||||
for (unsigned i = 0; i < registered_count; ++i)
|
||||
assert(strcmp(registered[i]->uri, "/api/admin/ws-ticket") && strcmp(registered[i]->uri, "/ws/admin"));
|
||||
assert(route("/ws/serial")->handler == traced_websocket_handler);
|
||||
@@ -537,13 +578,13 @@ int main(void) {
|
||||
clear_events(); assert(web_server_stop() == ESP_OK && !strcmp(events, "ASH"));
|
||||
assert(!admin_detaches && !admin_stoppeds);
|
||||
registration_fail_at = 0; fresh_registration(); start();
|
||||
assert(registered_count == 36 && admin_attaches == 1 && s_counters.starts == 2);
|
||||
assert(registered_count == 39 && admin_attaches == 1 && s_counters.starts == 2);
|
||||
assert(web_server_stop() == ESP_OK && admin_stoppeds == 1);
|
||||
}
|
||||
puts("PASS optional positions 17..18 preserve M1, roll back ticket when needed and recover after stop/restart");
|
||||
|
||||
reset(); registration_fail_at = 18; unregister_fail = true;
|
||||
assert(web_server_start() == ESP_OK && unregister_calls == 1 && registered_count == 35);
|
||||
assert(web_server_start() == ESP_OK && unregister_calls == 1 && registered_count == 38);
|
||||
assert(auth_live && ssl_live && serial_live && s_serial_transport_attached);
|
||||
assert(!admin_inits && !admin_attaches && !admin_owned && !s_admin_transport_owned);
|
||||
ticket = route("/api/admin/ws-ticket");
|
||||
@@ -555,7 +596,7 @@ int main(void) {
|
||||
clear_events(); assert(web_server_stop() == ESP_OK && !strcmp(events, "ASH"));
|
||||
assert(!admin_detaches && !admin_stoppeds);
|
||||
unregister_fail = false; registration_fail_at = 0; fresh_registration(); start();
|
||||
assert(registered_count == 36 && admin_attaches == 1 && web_server_stop() == ESP_OK);
|
||||
assert(registered_count == 39 && admin_attaches == 1 && web_server_stop() == ESP_OK);
|
||||
puts("PASS failed unregister retains only original ticket handler, no admin attachment, and permits restart");
|
||||
|
||||
reset(); registration_fail_at = 6; ssl_stop_error = ESP_FAIL;
|
||||
@@ -577,7 +618,7 @@ int main(void) {
|
||||
assert(web_server_stop() == ESP_ERR_INVALID_STATE && !auth_stops);
|
||||
puts("PASS auth/start failure gates and invalid/transitioning lifecycle rejection");
|
||||
reset(); settings_fail = true; start();
|
||||
assert(settings_calls == 1 && registered_count == 35);
|
||||
assert(settings_calls == 1 && registered_count == 38);
|
||||
assert(auth_live && serial_live && admin_owned && web_server_stop() == ESP_OK);
|
||||
settings_fail = false; fresh_registration(); start();
|
||||
assert(route("/api/settings/serial")->handler == serial_settings_handler);
|
||||
@@ -585,7 +626,7 @@ int main(void) {
|
||||
puts("PASS optional Settings registration failure preserves auth and both transports; restart recovers");
|
||||
for (unsigned failure = 1; failure <= 2; ++failure) {
|
||||
reset(); operation_fail_at = failure; start();
|
||||
assert(registered_count == 34 && operation_calls == failure && unregister_calls == failure - 1);
|
||||
assert(registered_count == 37 && operation_calls == failure && unregister_calls == failure - 1);
|
||||
assert(auth_live && serial_live && admin_owned);
|
||||
for (unsigned i = 0; i < registered_count; ++i) assert(strcmp(registered[i]->uri, "/api/settings/serial-operation"));
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
@@ -593,7 +634,7 @@ int main(void) {
|
||||
puts("PASS optional Serial operation GET/POST failure never publishes a mutation-only route or disables transports");
|
||||
for (unsigned failure = 1; failure <= 3; ++failure) {
|
||||
reset(); account_calls = 0; account_fail_at = failure; start();
|
||||
assert(account_calls == failure && registered_count == (failure == 1 ? 33 : 34));
|
||||
assert(account_calls == failure && registered_count == (failure == 1 ? 36 : 37));
|
||||
assert(keys_calls == 1 && route("/api/settings/accounts/keys")->handler == web_account_keys_handler);
|
||||
assert(generation_calls == 1 && route("/api/settings/accounts/generate-password")->handler == web_account_generate_password_handler);
|
||||
assert(auth_live && serial_live && admin_owned);
|
||||
@@ -601,17 +642,17 @@ int main(void) {
|
||||
assert(strcmp(registered[i]->uri, "/api/settings/account-operation"));
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
account_fail_at = 0; account_calls = 0; fresh_registration(); start();
|
||||
assert(registered_count == 36 && account_calls == 3);
|
||||
assert(registered_count == 39 && account_calls == 3);
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
reset(); account_calls = 0; account_fail_at = 3; unregister_fail = true; start();
|
||||
assert(registered_count == 35 && auth_live && serial_live && admin_owned);
|
||||
assert(registered_count == 38 && auth_live && serial_live && admin_owned);
|
||||
for (unsigned i = 0; i < registered_count; ++i)
|
||||
assert(strcmp(registered[i]->uri, "/api/settings/account-operation") || registered[i]->method == HTTP_GET);
|
||||
assert(web_server_stop() == ESP_OK); account_fail_at = 0;
|
||||
puts("PASS optional Accounts list/result/mutation allocation failures preserve transports and never expose mutation without reads (including failed unregister)");
|
||||
reset(); generation_fail = true; start();
|
||||
assert(generation_calls == 1 && registered_count == 35 && account_calls == 3);
|
||||
assert(generation_calls == 1 && registered_count == 38 && account_calls == 3);
|
||||
assert(keys_calls == 1 && route("/api/settings/accounts/keys")->handler == web_account_keys_handler);
|
||||
assert(!auth_stops && !ssl_stops && !unregister_calls && !s_counters.start_failures);
|
||||
assert(route("/api/settings/accounts")->handler == web_account_settings_handler);
|
||||
@@ -623,12 +664,12 @@ int main(void) {
|
||||
}
|
||||
assert(account_mutations == 1 && web_server_stop() == ESP_OK);
|
||||
generation_fail = false; fresh_registration(); start();
|
||||
assert(generation_calls == 2 && registered_count == 36);
|
||||
assert(generation_calls == 2 && registered_count == 39);
|
||||
assert(route("/api/settings/accounts/generate-password")->handler == web_account_generate_password_handler);
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
puts("PASS optional password generation allocation failure preserves account routes/auth/transports; restart recovers");
|
||||
reset(); keys_fail = true; start();
|
||||
assert(keys_calls == 1 && registered_count == 35 && account_calls == 3 && generation_calls == 1);
|
||||
assert(keys_calls == 1 && registered_count == 38 && account_calls == 3 && generation_calls == 1);
|
||||
assert(!auth_stops && !ssl_stops && !unregister_calls && !s_counters.start_failures);
|
||||
assert(route("/api/settings/accounts")->handler == web_account_settings_handler);
|
||||
assert(route("/api/settings/accounts/generate-password")->handler == web_account_generate_password_handler);
|
||||
@@ -643,7 +684,7 @@ int main(void) {
|
||||
}
|
||||
assert(account_mutations == 1 && web_server_stop() == ESP_OK);
|
||||
keys_fail = false; fresh_registration(); start();
|
||||
assert(keys_calls == 2 && registered_count == 36);
|
||||
assert(keys_calls == 2 && registered_count == 39);
|
||||
assert(route("/api/settings/accounts/keys")->handler == web_account_keys_handler);
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
puts("PASS optional account keys allocation failure preserves account/generation/auth/transports; restart recovers");
|
||||
@@ -668,7 +709,7 @@ int main(void) {
|
||||
reset(); network_fail_at = failure; start();
|
||||
unsigned failed_route = (failure + 1) / 2;
|
||||
assert(network_calls == failed_route && network_allocations == failure);
|
||||
assert(registered_count == (failed_route == 1 ? 33 : 34));
|
||||
assert(registered_count == (failed_route == 1 ? 36 : 37));
|
||||
assert(unregister_calls == (failed_route == 3 ? 1 : 0));
|
||||
assert(!method_route("/api/settings/network-operation", HTTP_GET));
|
||||
assert(!method_route("/api/settings/network-operation", HTTP_POST));
|
||||
@@ -676,13 +717,13 @@ int main(void) {
|
||||
other_domains_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
network_fail_at = 0; fresh_registration(); start();
|
||||
assert(registered_count == 36); network_complete();
|
||||
assert(registered_count == 39); network_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
puts("PASS all six Network descriptor/name allocation positions isolate failures and recover after restart");
|
||||
for (unsigned failure = 5; failure <= 6; ++failure) {
|
||||
reset(); network_fail_at = failure; unregister_fail = true; start();
|
||||
assert(registered_count == 35 && unregister_calls == 1);
|
||||
assert(registered_count == 38 && unregister_calls == 1);
|
||||
assert(route("/api/settings/network")->handler == web_network_snapshot_handler);
|
||||
assert(method_route("/api/settings/network-operation", HTTP_GET)->handler == web_network_operation_handler);
|
||||
assert(!method_route("/api/settings/network-operation", HTTP_POST));
|
||||
@@ -692,7 +733,7 @@ int main(void) {
|
||||
assert(web_server_start() == ESP_ERR_INVALID_STATE && ssl_starts == 1);
|
||||
ssl_stop_error = ESP_OK; assert(web_server_stop() == ESP_OK);
|
||||
unregister_fail = false; network_fail_at = 0; fresh_registration(); start();
|
||||
assert(registered_count == 36); network_complete();
|
||||
assert(registered_count == 39); network_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
puts("PASS failed Network result unregister leaves reads only and preserves stop-failure ownership/restart");
|
||||
@@ -700,7 +741,7 @@ int main(void) {
|
||||
reset(); display_fail_at = failure; start();
|
||||
unsigned failed_route = (failure + 1) / 2;
|
||||
assert(display_calls == failed_route && display_allocations == failure);
|
||||
assert(registered_count == (failed_route == 1 ? 33 : 34));
|
||||
assert(registered_count == (failed_route == 1 ? 36 : 37));
|
||||
assert(unregister_calls == (failed_route == 3 ? 1 : 0));
|
||||
assert(!method_route("/api/settings/display-operation", HTTP_GET));
|
||||
assert(!method_route("/api/settings/display-operation", HTTP_POST));
|
||||
@@ -708,13 +749,13 @@ int main(void) {
|
||||
other_domains_complete(); network_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
display_fail_at = 0; fresh_registration(); start();
|
||||
assert(registered_count == 36); display_complete();
|
||||
assert(registered_count == 39); display_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
puts("PASS all six Display descriptor/name allocation positions isolate failures and recover after restart");
|
||||
for (unsigned failure = 5; failure <= 6; ++failure) {
|
||||
reset(); display_fail_at = failure; unregister_fail = true; start();
|
||||
assert(registered_count == 35 && unregister_calls == 1);
|
||||
assert(registered_count == 38 && unregister_calls == 1);
|
||||
assert(route("/api/settings/display")->handler == web_display_settings_handler);
|
||||
assert(method_route("/api/settings/display-operation", HTTP_GET)->handler == web_display_operation_handler);
|
||||
assert(!method_route("/api/settings/display-operation", HTTP_POST));
|
||||
@@ -724,7 +765,7 @@ int main(void) {
|
||||
assert(web_server_start() == ESP_ERR_INVALID_STATE && ssl_starts == 1);
|
||||
ssl_stop_error = ESP_OK; assert(web_server_stop() == ESP_OK);
|
||||
unregister_fail = false; display_fail_at = 0; fresh_registration(); start();
|
||||
assert(registered_count == 36); display_complete();
|
||||
assert(registered_count == 39); display_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
puts("PASS failed Display result unregister leaves reads only and preserves stop-failure ownership/restart");
|
||||
@@ -732,7 +773,7 @@ int main(void) {
|
||||
reset(); broker_fail_at = failure; start();
|
||||
unsigned failed_route = (failure + 1) / 2;
|
||||
assert(broker_calls == failed_route && broker_allocations == failure);
|
||||
assert(registered_count == (failed_route == 1 ? 33 : 34));
|
||||
assert(registered_count == (failed_route == 1 ? 36 : 37));
|
||||
assert(unregister_calls == (failed_route == 3 ? 1 : 0));
|
||||
assert(!method_route("/api/settings/broker-operation", HTTP_GET));
|
||||
assert(!method_route("/api/settings/broker-operation", HTTP_POST));
|
||||
@@ -740,13 +781,13 @@ int main(void) {
|
||||
other_domains_complete(); network_complete(); display_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
broker_fail_at = 0; fresh_registration(); start();
|
||||
assert(registered_count == 36); broker_complete();
|
||||
assert(registered_count == 39); broker_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
puts("PASS all six Broker descriptor/name allocation positions isolate failures and recover after restart");
|
||||
for (unsigned failure = 5; failure <= 6; ++failure) {
|
||||
reset(); broker_fail_at = failure; unregister_fail = true; start();
|
||||
assert(registered_count == 35 && unregister_calls == 1);
|
||||
assert(registered_count == 38 && unregister_calls == 1);
|
||||
assert(route("/api/settings/broker")->handler == web_broker_settings_handler);
|
||||
assert(method_route("/api/settings/broker-operation", HTTP_GET)->handler == web_broker_operation_handler);
|
||||
assert(!method_route("/api/settings/broker-operation", HTTP_POST));
|
||||
@@ -756,7 +797,7 @@ int main(void) {
|
||||
assert(web_server_start() == ESP_ERR_INVALID_STATE && ssl_starts == 1);
|
||||
ssl_stop_error = ESP_OK; assert(web_server_stop() == ESP_OK);
|
||||
unregister_fail = false; broker_fail_at = 0; fresh_registration(); start();
|
||||
assert(registered_count == 36); broker_complete();
|
||||
assert(registered_count == 39); broker_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
puts("PASS failed Broker result unregister leaves reads only and preserves stop-failure ownership/restart");
|
||||
@@ -764,7 +805,7 @@ int main(void) {
|
||||
reset(); ssh_fail_at = failure; start();
|
||||
unsigned failed_route = (failure + 1) / 2;
|
||||
assert(ssh_calls == failed_route && ssh_allocations == failure);
|
||||
assert(registered_count == (failed_route == 1 ? 33 : 34));
|
||||
assert(registered_count == (failed_route == 1 ? 36 : 37));
|
||||
assert(unregister_calls == (failed_route == 3 ? 1 : 0));
|
||||
assert(!method_route("/api/settings/ssh-operation", HTTP_GET));
|
||||
assert(!method_route("/api/settings/ssh-operation", HTTP_POST));
|
||||
@@ -772,13 +813,13 @@ int main(void) {
|
||||
other_domains_complete(); network_complete(); display_complete(); broker_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
ssh_fail_at = 0; fresh_registration(); start();
|
||||
assert(registered_count == 36); ssh_complete();
|
||||
assert(registered_count == 39); ssh_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
puts("PASS all six SSH descriptor/name allocation positions isolate failures and recover after restart");
|
||||
for (unsigned failure = 5; failure <= 6; ++failure) {
|
||||
reset(); ssh_fail_at = failure; unregister_fail = true; start();
|
||||
assert(registered_count == 35 && unregister_calls == 1);
|
||||
assert(registered_count == 38 && unregister_calls == 1);
|
||||
assert(route("/api/settings/ssh")->handler == web_ssh_settings_handler);
|
||||
assert(method_route("/api/settings/ssh-operation", HTTP_GET)->handler == web_ssh_operation_handler);
|
||||
assert(!method_route("/api/settings/ssh-operation", HTTP_POST));
|
||||
@@ -788,7 +829,7 @@ int main(void) {
|
||||
assert(web_server_start() == ESP_ERR_INVALID_STATE && ssl_starts == 1);
|
||||
ssl_stop_error = ESP_OK; assert(web_server_stop() == ESP_OK);
|
||||
unregister_fail = false; ssh_fail_at = 0; fresh_registration(); start();
|
||||
assert(registered_count == 36); ssh_complete();
|
||||
assert(registered_count == 39); ssh_complete();
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
puts("PASS failed SSH result unregister leaves reads only and preserves stop-failure ownership/restart");
|
||||
@@ -802,18 +843,180 @@ int main(void) {
|
||||
start(); network_complete(); display_complete(); broker_complete(); ssh_complete(); assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
puts("PASS every other settings route failure leaves the complete Network domain available");
|
||||
puts("27 lifecycle groups passed (16 required fatal positions, 22 optional routes, Network/Display/Broker/SSH allocation positions and failed unregister)");
|
||||
management_tests();
|
||||
pipeline_tests();
|
||||
puts("41 lifecycle groups passed (34 prior owner/route groups plus 7 lifecycle integration groups)");
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
|
||||
MANAGEMENT_TESTS = r'''
|
||||
static unsigned reserved_gaps;
|
||||
static void observe_restart_gap(void) {
|
||||
if (s_server || !s_transitioning) return;
|
||||
unlock_hook = NULL;
|
||||
++reserved_gaps;
|
||||
web_server_management_snapshot_t snapshot;
|
||||
assert(web_server_get_management_snapshot(&snapshot) == ESP_OK);
|
||||
assert(!snapshot.running && snapshot.transitioning && !snapshot.controllable);
|
||||
unsigned starts = ssl_starts, stops = ssl_stops, auth = auth_starts;
|
||||
uint32_t generation = snapshot.generation;
|
||||
assert(web_server_start() == ESP_ERR_INVALID_STATE);
|
||||
assert(web_server_stop() == ESP_ERR_INVALID_STATE);
|
||||
assert(web_server_stop_current(generation) == ESP_ERR_INVALID_STATE);
|
||||
assert(web_server_restart_current(generation) == ESP_ERR_INVALID_STATE);
|
||||
assert(ssl_starts == starts && ssl_stops == stops && auth_starts == auth);
|
||||
assert(s_generation == generation && s_transitioning && !s_server);
|
||||
}
|
||||
static void management_tests(void) {
|
||||
web_server_management_snapshot_t snapshot;
|
||||
reset(); memset(&snapshot, 0xa5, sizeof(snapshot));
|
||||
assert(web_server_get_management_snapshot(NULL) == ESP_ERR_INVALID_ARG);
|
||||
assert(web_server_get_management_snapshot(&snapshot) == ESP_ERR_INVALID_STATE);
|
||||
assert(!snapshot.generation && !snapshot.running && !snapshot.controllable);
|
||||
assert(web_server_stop_current(0) == ESP_ERR_INVALID_ARG);
|
||||
assert(web_server_restart_current(0) == ESP_ERR_INVALID_ARG);
|
||||
assert(web_server_stop_current(1) == ESP_ERR_INVALID_STATE);
|
||||
assert(web_server_restart_current(1) == ESP_ERR_INVALID_STATE);
|
||||
assert(web_server_init() == ESP_OK);
|
||||
assert(web_server_get_management_snapshot(&snapshot) == ESP_OK);
|
||||
assert(snapshot.generation == 1 && !snapshot.running && !snapshot.controllable);
|
||||
start(); assert(web_server_get_management_snapshot(&snapshot) == ESP_OK);
|
||||
assert(snapshot.generation == 2 && snapshot.running && !snapshot.transitioning && snapshot.controllable);
|
||||
mutex_busy = true;
|
||||
assert(web_server_get_management_snapshot(&snapshot) == ESP_ERR_TIMEOUT);
|
||||
assert(!snapshot.generation && !snapshot.running && !snapshot.controllable);
|
||||
assert(web_server_stop_current(2) == ESP_ERR_TIMEOUT);
|
||||
assert(web_server_restart_current(2) == ESP_ERR_TIMEOUT);
|
||||
mutex_busy = false;
|
||||
assert(s_generation == 2 && !auth_stops && !ssl_stops);
|
||||
assert(web_server_stop_current(2) == ESP_OK);
|
||||
puts("PASS management snapshot preinit/zero-wait contention and argument rejection without lifecycle effects");
|
||||
|
||||
reset(); start(); uint32_t original = s_generation;
|
||||
assert(web_server_clear_counters() == ESP_OK && s_generation == original);
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
fresh_registration(); start(); assert(s_server == SERVER && s_generation == original + 2);
|
||||
clear_events();
|
||||
assert(web_server_stop_current(original) == ESP_ERR_INVALID_STATE);
|
||||
assert(web_server_restart_current(original) == ESP_ERR_INVALID_STATE);
|
||||
assert(!event_length && auth_live && ssl_live && s_generation == original + 2);
|
||||
assert(web_server_stop_current(s_generation) == ESP_OK);
|
||||
assert(web_server_stop_current(s_generation) == ESP_ERR_INVALID_STATE);
|
||||
puts("PASS canonical stop/start same-handle ABA and counter-clear generation fences");
|
||||
|
||||
reset(); start(); original = s_generation; reserved_gaps = 0;
|
||||
fresh_registration(); clear_events(); unlock_hook = observe_restart_gap;
|
||||
assert(web_server_restart_current(original) == ESP_OK);
|
||||
assert(reserved_gaps == 1 && !unlock_hook && !strcmp(events, "ADSHR"));
|
||||
assert(s_generation == original + 2 && s_server == SERVER && !s_transitioning);
|
||||
assert(auth_live && ssl_live && admin_owned && serial_live && idle_owned);
|
||||
assert(ssl_starts == 2 && ssl_stops == 1 && serial_inits == 1 && registered_count == 39);
|
||||
assert(web_server_stop_current(original) == ESP_ERR_INVALID_STATE);
|
||||
assert(web_server_stop_current(s_generation) == ESP_OK);
|
||||
puts("PASS conditional restart reserves stop-to-start gap against canonical and conditional callers");
|
||||
|
||||
for (unsigned failure = 0; failure < 4; ++failure) {
|
||||
reset(); start(); original = s_generation;
|
||||
if (failure == 0) idle_detach_error = ESP_ERR_TIMEOUT;
|
||||
if (failure == 1) admin_detach_error = ESP_ERR_TIMEOUT;
|
||||
if (failure == 2) serial_detach_error = ESP_ERR_INVALID_STATE;
|
||||
if (failure == 3) ssl_stop_error = ESP_FAIL;
|
||||
assert(web_server_restart_current(original) != ESP_OK);
|
||||
assert(s_generation == original + 1 && !s_transitioning && s_server == SERVER);
|
||||
assert(ssl_starts == 1 && auth_starts == 1 && !auth_live && !idle_stoppeds && !admin_stoppeds);
|
||||
assert(web_server_get_management_snapshot(&snapshot) == ESP_OK && !snapshot.controllable);
|
||||
assert(web_server_start() == ESP_ERR_INVALID_STATE);
|
||||
assert(web_server_init() == ESP_OK);
|
||||
assert(web_server_get_management_snapshot(&snapshot) == ESP_OK && !snapshot.controllable);
|
||||
clear_events();
|
||||
assert(web_server_stop_current(s_generation) == ESP_ERR_INVALID_STATE);
|
||||
assert(web_server_restart_current(s_generation) == ESP_ERR_INVALID_STATE);
|
||||
assert(!event_length && s_generation == original + 1);
|
||||
idle_detach_error = admin_detach_error = serial_detach_error = ssl_stop_error = ESP_OK;
|
||||
assert(web_server_stop() == ESP_OK && !s_server && !s_transitioning);
|
||||
fresh_registration(); start();
|
||||
assert(web_server_get_management_snapshot(&snapshot) == ESP_OK && snapshot.controllable);
|
||||
assert(web_server_stop_current(snapshot.generation) == ESP_OK);
|
||||
}
|
||||
puts("PASS all stop failure stages skip restart, retain ownership and require canonical cleanup even after init");
|
||||
|
||||
for (unsigned failure = 0; failure < 6; ++failure) {
|
||||
reset(); start(); original = s_generation; fresh_registration();
|
||||
if (failure == 0) auth_error = ESP_FAIL;
|
||||
if (failure == 1) idle_prepare_error = ESP_ERR_NO_MEM;
|
||||
if (failure == 2) ssl_start_error = ESP_FAIL;
|
||||
if (failure == 3 || failure == 5) registration_fail_at = 1;
|
||||
if (failure == 4) idle_attach_error = ESP_FAIL;
|
||||
/* Fail only cleanup of the newly started server, not the initial stop. */
|
||||
if (failure == 5) ssl_stop_fail_at = 2;
|
||||
assert(web_server_restart_current(original) != ESP_OK);
|
||||
assert(!s_transitioning && !auth_live);
|
||||
assert((s_server != NULL) == (failure == 5) && ssl_live == (failure == 5));
|
||||
assert(s_generation == original + 2);
|
||||
assert(web_server_get_management_snapshot(&snapshot) == ESP_OK && !snapshot.controllable);
|
||||
auth_error = idle_prepare_error = ssl_start_error = idle_attach_error = ESP_OK;
|
||||
registration_fail_at = 0;
|
||||
if (failure == 5) {
|
||||
assert(web_server_start() == ESP_ERR_INVALID_STATE);
|
||||
assert(web_server_restart_current(s_generation) == ESP_ERR_INVALID_STATE);
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
}
|
||||
fresh_registration(); start();
|
||||
assert(web_server_stop_current(s_generation) == ESP_OK);
|
||||
}
|
||||
puts("PASS restart start-side failures retain failed-cleanup ownership and permit canonical recovery");
|
||||
|
||||
reset(); start(); s_generation = UINT32_MAX - 1;
|
||||
fresh_registration();
|
||||
assert(web_server_restart_current(UINT32_MAX - 1) == ESP_OK);
|
||||
assert(s_generation == UINT32_MAX && !s_transitioning && ssl_live);
|
||||
assert(web_server_get_management_snapshot(&snapshot) == ESP_OK && !snapshot.controllable);
|
||||
assert(web_server_clear_counters() == ESP_OK && s_generation == UINT32_MAX);
|
||||
clear_events();
|
||||
assert(web_server_stop_current(UINT32_MAX) == ESP_ERR_INVALID_STATE);
|
||||
assert(web_server_restart_current(UINT32_MAX) == ESP_ERR_INVALID_STATE);
|
||||
assert(!event_length && s_generation == UINT32_MAX);
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
fresh_registration(); start(); assert(s_generation == UINT32_MAX);
|
||||
assert(web_server_stop() == ESP_OK);
|
||||
puts("PASS saturated generation never wraps; admitted restart completes and canonical recovery stays available");
|
||||
|
||||
reset(); start(); original = s_generation; s_transitioning = true;
|
||||
clear_events();
|
||||
assert(web_server_get_management_snapshot(&snapshot) == ESP_OK && snapshot.transitioning && !snapshot.controllable);
|
||||
assert(web_server_stop_current(original) == ESP_ERR_INVALID_STATE);
|
||||
assert(web_server_restart_current(original) == ESP_ERR_INVALID_STATE);
|
||||
assert(web_server_stop() == ESP_ERR_INVALID_STATE && web_server_start() == ESP_ERR_INVALID_STATE);
|
||||
assert(!event_length && s_generation == original);
|
||||
s_transitioning = false; serial_detach_error = ESP_ERR_TIMEOUT; fresh_registration();
|
||||
assert(web_server_restart_current(original) == ESP_OK);
|
||||
assert(s_generation == original + 2 && ssl_live && !s_transitioning);
|
||||
serial_detach_error = ESP_OK;
|
||||
assert(web_server_stop_current(s_generation) == ESP_OK);
|
||||
puts("PASS transitions reject stale admission; canonical serial detach timeout still permits successful restart");
|
||||
}
|
||||
'''
|
||||
|
||||
unit = FAKES + header + '\n' + constants + state + '\n'.join(uri_tables)
|
||||
callback = re.search(r'^static void tls_session_callback\(.*?^\}', source, re.M | re.S)
|
||||
assert callback
|
||||
unit += callback.group() + '\n'
|
||||
unit += function('ensure_mutex')
|
||||
unit += ''.join(function(name) for name in ('web_server_init', 'web_server_start', 'web_server_stop'))
|
||||
unit += TESTS
|
||||
unit += ''.join(function(name) for name in (
|
||||
'web_server_init', 'start_server', 'web_server_start', 'stop_server',
|
||||
'web_server_stop', 'web_server_stop_current', 'web_server_restart_current',
|
||||
'web_server_reboot_current', 'web_server_get_management_snapshot', 'web_server_clear_counters'))
|
||||
lifecycle_source = (ROOT / 'src/web_lifecycle_settings.c').read_text()
|
||||
pipeline_state = lifecycle_source[lifecycle_source.index('typedef struct {'):lifecycle_source.index('static void cancel_locked')]
|
||||
pipeline_state = 'enum { IDLE, PENDING, EXECUTING, OK, FAILED, CANCELLED };\n' + pipeline_state
|
||||
pipeline_functions = ''
|
||||
for name in ('cancel_locked', 'expire_locked', 'ack_handoff', 'web_lifecycle_settings_stopped', 'web_lifecycle_settings_execute'):
|
||||
match = re.search(r'^(?:static )?void ' + name + r'\([^\n]*\)\n\{.*?^\}', lifecycle_source, re.M | re.S)
|
||||
assert match, name
|
||||
pipeline_functions += match.group() + '\n'
|
||||
unit += 'static void management_tests(void);\nstatic void pipeline_tests(void);\n' + TESTS + MANAGEMENT_TESTS
|
||||
unit += (HERE / 'lifecycle_pipeline.c').read_text().replace('/* PRODUCTION_PIPELINE */', pipeline_state + pipeline_functions)
|
||||
with tempfile.TemporaryDirectory(prefix='web-admin-server-lifecycle-') as directory:
|
||||
temporary = Path(directory)
|
||||
c_file = temporary / 'test.c'
|
||||
|
||||
Reference in New Issue
Block a user