Implement HTTPS lifecycle and reboot controls

This commit is contained in:
2026-09-13 17:24:00 +02:00
parent 737bd29f9e
commit 36e80811e8
24 changed files with 1392 additions and 75 deletions
@@ -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");
}
+249 -46
View File
@@ -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'