feat: add bounded admin WebSocket backend (Phase 8D.5)
- Require current admin cookie sessions, Origin checks and single-use tickets - Reuse the shared console with session-aware authorization and slot allocation - Add HTTPD-owned I/O, bounded buffering and revocation cleanup - Prevent LRU eviction of serial clients and stale admin socket closure - Reject unsupported web-shell mutations before side effects - Add host regressions, a smoke client and resource accounting Validated by user sign-off after a 15-minute full-client soak at 230400 baud, with a few broker drops under heavy output. Browser UI remains for Phase 8D.6; numeric memory reserves remain open.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
# Admin ticket store host checks
|
||||
|
||||
Run from the repository root:
|
||||
|
||||
```sh
|
||||
python3 tests/web_admin_tickets/run.py
|
||||
python3 tests/web_admin_tickets/run.py --sanitize
|
||||
```
|
||||
|
||||
Requires a C11 `cc`, Python 3, OpenSSL development headers/libcrypto, and (for
|
||||
`--sanitize`) ASan/UBSan runtimes. No firmware build, network, generated assets,
|
||||
or persistent build output. The runner reuses the session-store harness's tiny
|
||||
platform header fakes. `test.c` includes the **unmodified production C**, using
|
||||
real project principal/session declarations and OpenSSL SHA-256. Inclusion gives
|
||||
white-box access for wipe, saturation and exhaustion assertions without adding
|
||||
production test hooks. RNG, time and session validation are deterministic fakes;
|
||||
all external calls assert that the ticket critical section is not held.
|
||||
|
||||
## Exact test groups
|
||||
|
||||
1. Stopped/start/idempotent-start lifecycle; 64 hex output; SHA-256 digest-only
|
||||
storage; success, replay denial and full record wipe.
|
||||
2. Two-ticket capacity and no live eviction; exact counters; nested competing
|
||||
issuance takes the last slot and the losing output is wiped.
|
||||
3. Issue rejects user role, public-key method, mismatched generation, zero ID,
|
||||
NULL principal/output, stale sessions and session-check errors.
|
||||
4. Consume burns matches before denying wrong session, user role, public-key
|
||||
method, generation, stale/check-error, zero ID or NULL principal; also rejects
|
||||
a different session with the *same* account principal.
|
||||
5. Empty/NULL/short/long/nonhex input; uppercase hex consumes the same secret.
|
||||
6. Success one microsecond before expiry; rejection at expiry; stale reclaim on
|
||||
issue/snapshot; snapshot expiry cleanup; signed deadline overflow rejection.
|
||||
7. Revocation ID precedence, exact username length/name and global scope;
|
||||
revocation never invalidates the fake sessions.
|
||||
8. RNG/SHA failures, failed output wipe, consume SHA failure leaves the
|
||||
unidentifiable ticket intact, duplicate live RNG/digest rejection.
|
||||
9. Issuance RNG/SHA hooks exercise stop/restart, global and nonmatching revoke,
|
||||
and session invalidation; currentness hook exercises stop/restart.
|
||||
10. Consume SHA/postcheck hooks exercise stop/restart, global/nonmatching revoke,
|
||||
stale sessions and expiry; nested competing consumes admit exactly once.
|
||||
11. Prune check races replacement with the same ID, digest and deadline; the
|
||||
non-reused record generation protects the replacement from stale cleanup.
|
||||
12. Nonwrapping epoch and record generation exhaustion, permanent lifecycle
|
||||
failure at exhaustion, saturated counters, NULL/count-only snapshots and
|
||||
host structure sizes.
|
||||
|
||||
## Contract and limits
|
||||
|
||||
The public API is in `src/web_admin_tickets.h`. This module is inert until wired
|
||||
by a later integration increment. It adds no routes, session invalidation,
|
||||
transport, task, socket, queue, timer or heap allocation. Callers must authorize
|
||||
HTTP cookie/Origin/CSRF, invalidate the authoritative session store **before**
|
||||
calling revoke, wipe successful token outputs and recheck currentness at later
|
||||
sensitive boundaries. A successful consume is not an authorization lease.
|
||||
|
||||
Two tickets, 32 RNG bytes each, 64 hex characters plus NUL, absolute 30-second
|
||||
lifetime. Only SHA-256 of decoded secret bytes is retained with copied principal,
|
||||
session ID, deadline and unique generation. Both hex cases are accepted. Live
|
||||
digest collisions fail rather than creating ambiguous tickets. No retry loop
|
||||
or live eviction. Pruning checks at most two copied records per invocation.
|
||||
Every revoke advances the epoch even if no record matches, conservatively
|
||||
cancelling unrelated in-flight issue/consume work. Start is idempotent while
|
||||
ready. Stop/start never resets counters, epoch or record generation.
|
||||
|
||||
`issued` counts published tickets, `consumed` counts burned matches (including
|
||||
subsequently denied admissions), `rejected` counts failed issue/consume calls;
|
||||
`capacity_rejections` is a subset of rejected. All counters saturate at UINT32_MAX.
|
||||
Snapshot prunes expired/stale records and exports counts, readiness and storage
|
||||
size only. A capacity failure is ESP_ERR_NO_MEM; malformed input INVALID_ARG;
|
||||
unauthorized/stale/lifecycle-raced work INVALID_STATE; no live consume match
|
||||
NOT_FOUND; SHA failure ESP_FAIL; RNG errors propagate. Failed issue wipes all 65
|
||||
output bytes when output is non-NULL. Output must not alias inputs.
|
||||
|
||||
Host measured sizes: ticket 104 B, two-ticket state 248 B, fake lock 4 B, snapshot
|
||||
40 B; snapshot `storage_bytes` = 252 B. Estimated 32-bit target sizes: ticket
|
||||
96 B, state 232 B, plus the target portMUX (typically 8 B), roughly **240 B static
|
||||
RAM**. These are estimates, not target linker measurements. Issue plus nested
|
||||
prune has 240 B of explicit ticket/random local payload on this host (about
|
||||
224 B on a 32-bit target), excluding scalar/compiler frames and session/RNG/SHA
|
||||
call stacks; caller also owns a 65 B token. No measured target stack/flash delta.
|
||||
|
||||
Hooks test deterministic interleavings, not true multicore scheduling or IDF
|
||||
portMUX semantics. They do not validate the real DRBG, mbedTLS, session database,
|
||||
HTTP admission, hardware, or full Phase 8D.5 integration. Post-check account
|
||||
changes without notification are subject to the same no-lease boundary as the
|
||||
session API. Combined hardware validation remains pending; no firmware build
|
||||
or device operation is part of this increment.
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile production ticket C with deterministic boundary fakes; no firmware build."""
|
||||
import os
|
||||
import pathlib
|
||||
import runpy
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
os.environ["CCACHE_DISABLE"] = "1"
|
||||
HERE = pathlib.Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
HEADERS = runpy.run_path(str(HERE.parent / "web_session_store/run.py"))["HEADERS"]
|
||||
with tempfile.TemporaryDirectory(prefix="web-admin-tickets-") as directory:
|
||||
tmp = pathlib.Path(directory)
|
||||
for name, text in HEADERS.items():
|
||||
path = tmp / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text)
|
||||
sanitize = ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"] if "--sanitize" in sys.argv else []
|
||||
subprocess.run(["cc", "-std=c11", "-Wall", "-Wextra", "-Werror", "-g",
|
||||
*sanitize, "-I" + str(tmp), "-I" + str(ROOT / "src"),
|
||||
str(HERE / "test.c"), "-lcrypto", "-o", str(tmp / "test")],
|
||||
check=True, timeout=30)
|
||||
subprocess.run([str(tmp / "test")], check=True, timeout=20)
|
||||
@@ -0,0 +1,285 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#include <assert.h>
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <openssl/sha.h>
|
||||
/* Include unmodified production C to inspect wipes, ABA and exhaustion without
|
||||
* adding firmware-only test hooks. Platform/user/session headers remain real. */
|
||||
#include "web_admin_tickets.c"
|
||||
|
||||
int host_lock_depth;
|
||||
static int64_t clock_us;
|
||||
static unsigned random_sequence;
|
||||
static bool rng_fail, sha_fail, session_fail, live[4];
|
||||
static user_principal_t principals[4];
|
||||
static void (*rng_hook)(void), (*sha_hook)(void), (*check_hook)(void);
|
||||
static unsigned check_calls, hook_at;
|
||||
static unsigned tests;
|
||||
|
||||
int64_t esp_timer_get_time(void) { assert(!host_lock_depth); return clock_us; }
|
||||
void secure_wipe(void *p, size_t n)
|
||||
{
|
||||
volatile unsigned char *v = p;
|
||||
while (n--) *v++ = 0;
|
||||
}
|
||||
static void fire(void (**hook)(void))
|
||||
{
|
||||
void (*call)(void) = *hook;
|
||||
*hook = NULL;
|
||||
if (call) call();
|
||||
}
|
||||
esp_err_t secure_random_fill(void *p, size_t n)
|
||||
{
|
||||
assert(!host_lock_depth && n == 32);
|
||||
memset(p, ++random_sequence, n);
|
||||
fire(&rng_hook);
|
||||
return rng_fail ? ESP_FAIL : ESP_OK;
|
||||
}
|
||||
int mbedtls_sha256(const unsigned char *p, size_t n, unsigned char *out, int mode)
|
||||
{
|
||||
assert(!host_lock_depth && n == 32 && mode == 0);
|
||||
assert(SHA256(p, n, out));
|
||||
fire(&sha_hook);
|
||||
return sha_fail ? -1 : 0;
|
||||
}
|
||||
esp_err_t web_session_store_check_principal(web_session_id_t id,
|
||||
const user_principal_t *p, bool *valid)
|
||||
{
|
||||
assert(!host_lock_depth);
|
||||
++check_calls;
|
||||
*valid = id < 4 && live[id] && same_principal(&principals[id], p);
|
||||
if (check_calls == hook_at) fire(&check_hook);
|
||||
/* Like the real session resolver, recheck liveness before returning;
|
||||
* never upgrade an already-failed check after a slot replacement. */
|
||||
*valid = *valid && id < 4 && live[id] && same_principal(&principals[id], p);
|
||||
return session_fail ? ESP_FAIL : ESP_OK;
|
||||
}
|
||||
static void zero(const void *p, size_t n)
|
||||
{
|
||||
const unsigned char *v = p;
|
||||
while (n--) assert(*v++ == 0);
|
||||
}
|
||||
static void reset(void)
|
||||
{
|
||||
/* Test isolation only; production never resets these generations. */
|
||||
memset(&s_state, 0, sizeof(s_state));
|
||||
clock_us = 100;
|
||||
random_sequence = 0;
|
||||
rng_fail = sha_fail = session_fail = false;
|
||||
rng_hook = sha_hook = check_hook = NULL;
|
||||
check_calls = hook_at = 0;
|
||||
for (unsigned i = 1; i < 4; ++i) {
|
||||
live[i] = true;
|
||||
principals[i] = (user_principal_t) {
|
||||
.user_id = i, .auth_generation = 1, .role = USER_ROLE_ADMIN,
|
||||
.method = USER_AUTH_METHOD_PASSWORD, .username_length = 1,
|
||||
.username = {(char)('a' + i - 1), 0},
|
||||
};
|
||||
}
|
||||
web_admin_tickets_start();
|
||||
}
|
||||
static void passed(const char *name) { ++tests; printf("PASS %s\n", name); }
|
||||
static void issue(unsigned id, char *token)
|
||||
{
|
||||
assert(web_admin_tickets_issue(id, &principals[id], token) == ESP_OK);
|
||||
}
|
||||
static void restart(void) { web_admin_tickets_stop(); web_admin_tickets_start(); }
|
||||
static void revoke_all(void) { web_admin_tickets_revoke(0, NULL, 0); }
|
||||
static void revoke_other(void) { web_admin_tickets_revoke(99, NULL, 0); }
|
||||
static void stale(void) { live[1] = false; }
|
||||
static void expire(void) { clock_us += WEB_ADMIN_TICKET_LIFETIME_US; }
|
||||
static void fail_issue(void)
|
||||
{
|
||||
char token[65];
|
||||
memset(token, 'x', sizeof(token));
|
||||
assert(web_admin_tickets_issue(1, &principals[1], token) != ESP_OK);
|
||||
zero(token, sizeof(token));
|
||||
zero(s_state.tickets, sizeof(s_state.tickets));
|
||||
}
|
||||
static char replacement[65];
|
||||
static void replace_stale(void)
|
||||
{
|
||||
revoke_all();
|
||||
live[1] = true;
|
||||
random_sequence = 0; /* Same digest, ID and deadline: only generation differs. */
|
||||
issue(1, replacement);
|
||||
}
|
||||
static char nested_token[65];
|
||||
static esp_err_t nested_result;
|
||||
static void nested_issue(void)
|
||||
{
|
||||
issue(2, nested_token);
|
||||
}
|
||||
static void nested_consume(void)
|
||||
{
|
||||
nested_result = web_admin_tickets_consume(nested_token, 1, &principals[1]);
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
char a[65], b[65], c[65];
|
||||
web_admin_tickets_snapshot_t snap;
|
||||
reset();
|
||||
web_admin_tickets_stop(); fail_issue();
|
||||
web_admin_tickets_start(); issue(1, a);
|
||||
uint64_t epoch = s_state.epoch;
|
||||
web_admin_tickets_start(); assert(s_state.epoch == epoch);
|
||||
assert(strlen(a) == 64);
|
||||
for (unsigned i = 0; i < 64; ++i) assert(isxdigit((unsigned char)a[i]));
|
||||
uint8_t raw[32]; memset(raw, 1, sizeof(raw));
|
||||
uint8_t expected[32]; assert(SHA256(raw, sizeof(raw), expected));
|
||||
assert(equal_digest(s_state.tickets[1].digest, expected));
|
||||
assert(memcmp(s_state.tickets[1].digest, raw, 32));
|
||||
assert(web_admin_tickets_consume(a, 1, &principals[1]) == ESP_OK);
|
||||
assert(web_admin_tickets_consume(a, 1, &principals[1]) == ESP_ERR_NOT_FOUND);
|
||||
zero(s_state.tickets, sizeof(s_state.tickets));
|
||||
passed("lifecycle, hex/digest storage, single use and wipe");
|
||||
|
||||
reset(); issue(1, a); issue(2, b);
|
||||
assert(web_admin_tickets_issue(3, &principals[3], c) == ESP_ERR_NO_MEM);
|
||||
zero(c, sizeof(c)); web_admin_tickets_get_snapshot(&snap);
|
||||
assert(snap.active == 2 && snap.issued == 2 && snap.rejected == 1 &&
|
||||
snap.capacity_rejections == 1 && snap.ready);
|
||||
assert(web_admin_tickets_consume(a, 1, &principals[1]) == ESP_OK);
|
||||
assert(web_admin_tickets_consume(b, 2, &principals[2]) == ESP_OK);
|
||||
reset(); issue(3, c); rng_hook = nested_issue;
|
||||
assert(web_admin_tickets_issue(1, &principals[1], a) == ESP_ERR_NO_MEM);
|
||||
zero(a, sizeof(a));
|
||||
assert(web_admin_tickets_consume(nested_token, 2, &principals[2]) == ESP_OK);
|
||||
assert(web_admin_tickets_consume(c, 3, &principals[3]) == ESP_OK);
|
||||
passed("capacity rejects without live eviction, competing issue and exact counters");
|
||||
|
||||
reset(); principals[1].role = USER_ROLE_USER; fail_issue();
|
||||
principals[1].role = USER_ROLE_ADMIN;
|
||||
principals[1].method = USER_AUTH_METHOD_SSH_PUBLIC_KEY; fail_issue();
|
||||
principals[1].method = USER_AUTH_METHOD_PASSWORD;
|
||||
user_principal_t bad = principals[1]; bad.auth_generation++;
|
||||
assert(web_admin_tickets_issue(1, &bad, a) == ESP_ERR_INVALID_STATE);
|
||||
assert(web_admin_tickets_issue(0, &principals[1], a) == ESP_ERR_INVALID_ARG);
|
||||
assert(web_admin_tickets_issue(1, NULL, a) == ESP_ERR_INVALID_ARG);
|
||||
assert(web_admin_tickets_issue(1, &principals[1], NULL) == ESP_ERR_INVALID_ARG);
|
||||
live[1] = false; fail_issue(); live[1] = true;
|
||||
session_fail = true; fail_issue();
|
||||
passed("issue role, password, session/principal binding and errors");
|
||||
|
||||
for (unsigned mode = 0; mode < 8; ++mode) {
|
||||
reset(); issue(1, a); bad = principals[1];
|
||||
unsigned id = 1;
|
||||
if (mode == 0) id = 2;
|
||||
if (mode == 1) bad.role = USER_ROLE_USER;
|
||||
if (mode == 2) bad.method = USER_AUTH_METHOD_SSH_PUBLIC_KEY;
|
||||
if (mode == 3) bad.auth_generation++;
|
||||
if (mode == 4) live[1] = false;
|
||||
if (mode == 5) session_fail = true;
|
||||
if (mode == 6) id = 0;
|
||||
assert(web_admin_tickets_consume(a, id, mode == 7 ? NULL : &bad) == ESP_ERR_INVALID_STATE);
|
||||
zero(s_state.tickets, sizeof(s_state.tickets));
|
||||
assert(s_state.consumed == 1);
|
||||
}
|
||||
reset(); principals[2] = principals[1]; issue(1, a);
|
||||
assert(web_admin_tickets_consume(a, 2, &principals[2]) == ESP_ERR_INVALID_STATE);
|
||||
zero(s_state.tickets, sizeof(s_state.tickets));
|
||||
passed("consume burns before wrong identity/role/currentness results, same-account session binding");
|
||||
|
||||
reset(); random_sequence = 170; issue(1, a);
|
||||
strcpy(b, a); b[63] = 0;
|
||||
assert(web_admin_tickets_consume(b, 1, &principals[1]) == ESP_ERR_INVALID_ARG);
|
||||
char long_token[66]; memcpy(long_token, a, 64); long_token[64] = 'a'; long_token[65] = 0;
|
||||
assert(web_admin_tickets_consume(long_token, 1, &principals[1]) == ESP_ERR_INVALID_ARG);
|
||||
assert(web_admin_tickets_consume("", 1, &principals[1]) == ESP_ERR_INVALID_ARG);
|
||||
assert(web_admin_tickets_consume(NULL, 1, &principals[1]) == ESP_ERR_INVALID_ARG);
|
||||
strcpy(b, a); b[30] = 'g';
|
||||
assert(web_admin_tickets_consume(b, 1, &principals[1]) == ESP_ERR_INVALID_ARG);
|
||||
for (unsigned i = 0; i < 64; ++i) a[i] = (char)toupper((unsigned char)a[i]);
|
||||
assert(web_admin_tickets_consume(a, 1, &principals[1]) == ESP_OK);
|
||||
passed("exact bounded hex validation and uppercase equivalence");
|
||||
|
||||
reset(); issue(1, a); clock_us += WEB_ADMIN_TICKET_LIFETIME_US - 1;
|
||||
assert(web_admin_tickets_consume(a, 1, &principals[1]) == ESP_OK);
|
||||
issue(1, a); expire();
|
||||
assert(web_admin_tickets_consume(a, 1, &principals[1]) == ESP_ERR_NOT_FOUND);
|
||||
issue(1, a); issue(2, b); live[1] = false; issue(3, c);
|
||||
web_admin_tickets_get_snapshot(&snap); assert(snap.active == 2);
|
||||
live[2] = false; web_admin_tickets_get_snapshot(&snap); assert(snap.active == 1);
|
||||
expire(); web_admin_tickets_get_snapshot(&snap); assert(snap.active == 0);
|
||||
clock_us = INT64_MAX - WEB_ADMIN_TICKET_LIFETIME_US + 1;
|
||||
fail_issue();
|
||||
passed("absolute expiry boundary, stale cleanup and time overflow");
|
||||
|
||||
reset(); issue(1, a); issue(2, b);
|
||||
web_admin_tickets_revoke(1, (const uint8_t *)"b", 1);
|
||||
assert(web_admin_tickets_consume(a, 1, &principals[1]) == ESP_ERR_NOT_FOUND);
|
||||
assert(web_admin_tickets_consume(b, 2, &principals[2]) == ESP_OK);
|
||||
issue(1, a); issue(2, b);
|
||||
web_admin_tickets_revoke(0, (const uint8_t *)"a", 0);
|
||||
web_admin_tickets_get_snapshot(&snap); assert(snap.active == 2);
|
||||
web_admin_tickets_revoke(0, (const uint8_t *)"a", 1);
|
||||
web_admin_tickets_get_snapshot(&snap); assert(snap.active == 1);
|
||||
revoke_all(); zero(s_state.tickets, sizeof(s_state.tickets));
|
||||
assert(live[1] && live[2]);
|
||||
passed("revoke ID precedence, exact username, all; no session invalidation");
|
||||
|
||||
reset(); rng_fail = true; fail_issue();
|
||||
reset(); sha_fail = true; fail_issue();
|
||||
reset(); issue(1, a); sha_fail = true;
|
||||
assert(web_admin_tickets_consume(a, 1, &principals[1]) == ESP_FAIL);
|
||||
sha_fail = false;
|
||||
assert(web_admin_tickets_consume(a, 1, &principals[1]) == ESP_OK);
|
||||
reset(); issue(1, a); random_sequence = 0;
|
||||
assert(web_admin_tickets_issue(2, &principals[2], b) == ESP_FAIL);
|
||||
zero(b, sizeof(b));
|
||||
assert(web_admin_tickets_consume(a, 1, &principals[1]) == ESP_OK);
|
||||
passed("RNG/SHA failure, output wipe and live digest collision rejection");
|
||||
|
||||
void (*actions[])(void) = {restart, revoke_all, revoke_other, stale};
|
||||
for (unsigned i = 0; i < sizeof(actions) / sizeof(actions[0]); ++i) {
|
||||
reset(); rng_hook = actions[i]; fail_issue();
|
||||
reset(); sha_hook = actions[i]; fail_issue();
|
||||
}
|
||||
reset(); hook_at = 1; check_hook = restart; fail_issue();
|
||||
passed("issue stop/restart, revoke and stale races across RNG/SHA/currentness");
|
||||
|
||||
for (unsigned i = 0; i < sizeof(actions) / sizeof(actions[0]); ++i) {
|
||||
reset(); issue(1, a); sha_hook = actions[i];
|
||||
assert(web_admin_tickets_consume(a, 1, &principals[1]) != ESP_OK);
|
||||
reset(); issue(1, a); hook_at = check_calls + 2; check_hook = actions[i];
|
||||
assert(web_admin_tickets_consume(a, 1, &principals[1]) != ESP_OK);
|
||||
assert(s_state.consumed == 1);
|
||||
}
|
||||
reset(); issue(1, a); sha_hook = expire;
|
||||
assert(web_admin_tickets_consume(a, 1, &principals[1]) == ESP_ERR_NOT_FOUND);
|
||||
reset(); issue(1, a); hook_at = check_calls + 2; check_hook = expire;
|
||||
assert(web_admin_tickets_consume(a, 1, &principals[1]) == ESP_ERR_INVALID_STATE);
|
||||
reset(); issue(1, nested_token); sha_hook = nested_consume;
|
||||
assert(web_admin_tickets_consume(nested_token, 1, &principals[1]) == ESP_ERR_NOT_FOUND);
|
||||
assert(nested_result == ESP_OK && s_state.consumed == 1);
|
||||
passed("consume crypto/postcheck lifecycle/expiry races and competing consume");
|
||||
|
||||
reset(); issue(1, a); live[1] = false;
|
||||
hook_at = check_calls + 1; check_hook = replace_stale;
|
||||
web_admin_tickets_get_snapshot(&snap); assert(snap.active == 1);
|
||||
assert(web_admin_tickets_consume(replacement, 1, &principals[1]) == ESP_OK);
|
||||
passed("stale-prune slot replacement ABA");
|
||||
|
||||
reset(); issue(1, a); s_state.epoch = UINT64_MAX - 1;
|
||||
revoke_all(); web_admin_tickets_start();
|
||||
assert(s_state.epoch == UINT64_MAX && !s_state.ready); fail_issue();
|
||||
restart(); assert(s_state.epoch == UINT64_MAX && !s_state.ready);
|
||||
reset(); s_state.generation = UINT64_MAX - 1; issue(1, a);
|
||||
assert(web_admin_tickets_issue(1, &principals[1], b) == ESP_ERR_INVALID_STATE);
|
||||
assert(web_admin_tickets_consume(a, 1, &principals[1]) == ESP_OK);
|
||||
restart(); assert(!s_state.ready);
|
||||
reset(); s_state.issued = s_state.consumed = s_state.rejected = UINT32_MAX;
|
||||
s_state.capacity_rejections = UINT32_MAX;
|
||||
issue(1, a); issue(2, b);
|
||||
assert(web_admin_tickets_issue(3, &principals[3], c) == ESP_ERR_NO_MEM);
|
||||
assert(web_admin_tickets_consume(a, 1, &principals[1]) == ESP_OK);
|
||||
web_admin_tickets_get_snapshot(&snap);
|
||||
assert(snap.issued == UINT32_MAX && snap.consumed == UINT32_MAX &&
|
||||
snap.rejected == UINT32_MAX && snap.capacity_rejections == UINT32_MAX);
|
||||
web_admin_tickets_get_snapshot(NULL);
|
||||
printf("Host sizes: ticket=%zu state=%zu lock=%zu snapshot=%zu bytes\n",
|
||||
sizeof(ticket_t), sizeof(s_state), sizeof(s_lock), sizeof(snap));
|
||||
passed("nonwrapping epoch/generation, saturating counters, count-only snapshot");
|
||||
printf("%u test groups passed\n", tests);
|
||||
}
|
||||
Reference in New Issue
Block a user