You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
285 lines
13 KiB
285 lines
13 KiB
/* Проверяем границы автомата на точных wire-пакетах. Включение реализации позволяет |
|
* задать состояние без копирования private-структур и без подмены очередей/таймеров. */ |
|
#include "../src/chat/merkle_sync.c" |
|
#include <assert.h> |
|
|
|
struct fixture { |
|
struct UTUN_INSTANCE inst; |
|
struct ETCP_CONN conn; |
|
int callbacks, result, apply_error; |
|
}; |
|
|
|
static int t_hash(void* arg, const char* ns, uint8_t level, uint64_t prefix, EVP_MD_CTX* hash) { |
|
(void)arg; (void)ns; (void)level; (void)prefix; |
|
return EVP_DigestUpdate(hash, "record", 6) == 1 ? 1 : -1; |
|
} |
|
|
|
static int t_page(void* arg, const char* ns, uint64_t prefix, int has_after, uint64_t after, |
|
uint8_t* data, size_t* len, uint64_t* next, int* more) { |
|
(void)arg; (void)ns; (void)prefix; (void)has_after; (void)after; |
|
memset(data, 0, *len); *next = 1; *more = 1; return 0; |
|
} |
|
|
|
static int t_apply(void* arg, const char* ns, uint64_t peer, const uint8_t* data, size_t len) { |
|
(void)ns; (void)peer; (void)data; (void)len; |
|
return ((struct fixture*)arg)->apply_error; |
|
} |
|
|
|
static const struct merkle_sync_data_ops t_ops = { |
|
.update_bucket_hash = t_hash, .get_page = t_page, .apply_items = t_apply |
|
}; |
|
|
|
static void t_done(uint64_t peer, const char* ns, int result, void* arg) { |
|
(void)peer; (void)ns; |
|
struct fixture* f = arg; |
|
f->callbacks++; f->result = result; |
|
} |
|
|
|
static void t_init(struct fixture* f) { |
|
memset(f, 0, sizeof(*f)); |
|
f->inst.node_id = 1; |
|
f->inst.ua = uasync_create(); assert(f->inst.ua); |
|
assert(sqlite3_open(":memory:", &f->inst.topo_sqlite_db) == SQLITE_OK); |
|
f->inst.connections = queue_new(f->inst.ua, 16, 0, 8, "test connections"); assert(f->inst.connections); |
|
f->conn.instance = &f->inst; f->conn.peer_node_id = 2; f->conn.initialized = 1; f->conn.links_up = 1; |
|
f->conn.send_input_q = queue_new(f->inst.ua, 0, 0, 0, "test send"); assert(f->conn.send_input_q); |
|
queue_set_waiter_defer(f->conn.send_input_q, 1); |
|
struct ll_entry* e = queue_entry_new(sizeof(struct conn_queue_entry)); assert(e); |
|
struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; |
|
ce->peer_node_id = 2; ce->conn = &f->conn; |
|
assert(queue_data_put_with_index(f->inst.connections, e) == 0); |
|
assert(merkle_sync_init(&f->inst, 0x72, &t_ops, f) == 0); |
|
} |
|
|
|
static void t_clear_queue(struct ll_queue* q) { |
|
struct ll_entry* e; |
|
while ((e = queue_data_get(q))) { queue_dgram_free(e); queue_entry_free(e); } |
|
queue_free(q); |
|
} |
|
|
|
static void t_destroy(struct fixture* f) { |
|
merkle_sync_destroy(&f->inst); |
|
assert(f->conn.ref_count == 0); |
|
t_clear_queue(f->conn.send_input_q); t_clear_queue(f->inst.connections); |
|
sqlite3_close(f->inst.topo_sqlite_db); |
|
uasync_poll(f->inst.ua, 0); uasync_destroy(f->inst.ua, 0); |
|
} |
|
|
|
static struct ms_session* t_session(struct fixture* f) { |
|
struct ms_session* s = ms_create(f->inst.msync, &f->conn, 1001); assert(s); |
|
s->round = 7; s->state = MS_PULL; s->request_id = 1; s->waiting = 1; |
|
s->requests = u_calloc(1, sizeof(*s->requests)); assert(s->requests); |
|
s->requests->cb = t_done; s->requests->arg = f; |
|
return s; |
|
} |
|
|
|
static void t_receive(struct fixture* f, uint8_t type, uint64_t round, uint32_t request, const uint8_t* data, size_t len) { |
|
struct ll_entry* e = ll_alloc_lldgram((uint16_t)(MS_HEADER + len)); assert(e); |
|
e->len = (uint16_t)(MS_HEADER + len); e->dgram[0] = 0x72; |
|
ms_write64(e->dgram + 1, 1001); e->dgram[9] = type; |
|
ms_write64(e->dgram + 10, round); ms_write32(e->dgram + 18, request); |
|
if (len) memcpy(e->dgram + MS_HEADER, data, len); |
|
ms_receive(&f->conn, e); |
|
} |
|
|
|
static void test_malformed_and_old_round(void) { |
|
struct fixture f; t_init(&f); |
|
struct ms_session* s = t_session(&f); |
|
uint8_t short_hashes[4] = {1}; |
|
t_receive(&f, MS_HASHES, 6, 1, short_hashes, sizeof(short_hashes)); |
|
assert(!f.callbacks && s->waiting && s->round == 7); |
|
t_receive(&f, MS_HASHES, 7, 1, short_hashes, sizeof(short_hashes)); |
|
assert(!f.callbacks && s->state == MS_FAILING); |
|
uasync_poll(f.inst.ua, 0); |
|
assert(f.callbacks == 1 && f.result == MT_ERR_PROTOCOL && s->state == MS_FAILED); |
|
t_destroy(&f); |
|
puts("PASS: stale round ignored; truncated hashes rejected before reading"); |
|
} |
|
|
|
static void test_rejected_data(void) { |
|
struct fixture f; t_init(&f); |
|
struct ms_session* s = t_session(&f); s->stack[0].level = MT_MAX_LEVEL; |
|
f.apply_error = MT_ERR_DATA; |
|
uint8_t page[11] = {0}; |
|
t_receive(&f, MS_PAGE, 7, 1, page, sizeof(page)); |
|
assert(!f.callbacks && s->state == MS_FAILING); |
|
uasync_poll(f.inst.ua, 0); |
|
assert(f.callbacks == 1 && f.result == MT_ERR_DATA && s->state == MS_FAILED); |
|
t_destroy(&f); |
|
puts("PASS: failed apply cannot produce success"); |
|
} |
|
|
|
static void test_sibling_walk(void) { |
|
struct fixture f; t_init(&f); |
|
struct ms_session* s = t_session(&f); |
|
uint8_t hashes[32 * 32] = {0}; hashes[0] = 1; hashes[32] = 1; |
|
assert(ms_receive_hashes(s, 1, hashes, sizeof(hashes)) == 0); |
|
assert(s->depth == 1 && s->stack[1].prefix == 0 && s->stack[0].children == 2); |
|
ms_discard_tx(s); |
|
memset(hashes, 0, sizeof(hashes)); |
|
assert(ms_receive_hashes(s, 2, hashes, sizeof(hashes)) == 0); |
|
assert(s->depth == 1 && s->stack[1].prefix == (UINT64_C(1) << 59)); |
|
assert(s->waiting && !f.callbacks); |
|
t_destroy(&f); |
|
puts("PASS: completing one branch preserves the sibling request"); |
|
} |
|
|
|
static void test_cancel_waiters(void) { |
|
for (int blocked = 0; blocked < 2; blocked++) { |
|
struct fixture f; t_init(&f); |
|
struct ms_session* s = t_session(&f); |
|
if (blocked) { struct ll_entry* e = queue_entry_new(0); assert(e); assert(queue_data_put(f.conn.send_input_q, e) == 0); } |
|
assert(ms_send(s, MS_QUERY, 1, NULL, 0) == 0); |
|
assert(s->waiter.internal || s->waiter.call_soon_id); |
|
f.conn.links_up = 0; |
|
merkle_sync_cancel_peer(&f.inst, 2); |
|
assert(!f.inst.msync->sessions && !f.conn.send_input_q->waiter_head); |
|
uasync_poll(f.inst.ua, 0); |
|
assert(!f.callbacks); |
|
t_destroy(&f); |
|
} |
|
puts("PASS: cancel after links_down removes queued and deferred waiters"); |
|
} |
|
|
|
static void test_page_size_and_cursor(void) { |
|
struct fixture f; t_init(&f); |
|
struct ms_session* s = t_session(&f); s->state = MS_SERVE; |
|
uint8_t query[MS_QUERY_SIZE] = { MT_MAX_LEVEL }; |
|
assert(ms_serve_query(s, 1, query, sizeof(query)) == 0); |
|
assert(s->tx && s->tx->len == MT_PAGE_SIZE); |
|
ms_discard_tx(s); |
|
query[9] = 1; ms_write64(query + 10, 1); |
|
assert(ms_serve_query(s, 2, query, sizeof(query)) == MT_ERR_DATA); |
|
query[0] = 255; |
|
assert(ms_serve_query(s, 3, query, sizeof(query)) == MT_ERR_PROTOCOL); |
|
t_destroy(&f); |
|
puts("PASS: page bounded; non-advancing cursor and invalid level rejected"); |
|
} |
|
|
|
static void test_readonly_tree(void) { |
|
struct fixture f; t_init(&f); |
|
assert(sqlite3_exec(f.inst.topo_sqlite_db, "PRAGMA query_only=ON", NULL, NULL, NULL) == SQLITE_OK); |
|
assert(merkle_sync_recompute_path(&f.inst, "1001", 0) < 0); |
|
uint8_t hash[32], empty[32] = {0}; |
|
assert(merkle_sync_read_hash(&f.inst, "1001", 0, 0, hash) == 0 && !memcmp(hash, empty, 32)); |
|
t_destroy(&f); |
|
puts("PASS: SQLite failure is reported and cannot publish a root"); |
|
} |
|
|
|
static void test_backpressure_and_disconnect(void) { |
|
struct fixture f; t_init(&f); |
|
struct ll_entry* blocker = queue_entry_new(0); assert(blocker); |
|
assert(queue_data_put(f.conn.send_input_q, blocker) == 0); |
|
assert(merkle_sync_start(&f.inst, 2, "1001", t_done, &f) == 0); |
|
uasync_poll(f.inst.ua, 0); |
|
struct ms_session* s = f.inst.msync->sessions; |
|
assert(s->tx && s->waiter.internal); |
|
for (int i = 0; i < 1000; i++) merkle_sync_changed(&f.inst, "1001"); |
|
assert(s->tx && queue_entry_count(f.conn.send_input_q) == 1 && s->dirty); |
|
f.conn.links_up = 0; |
|
ms_conn_status(&f.conn, ETCP_CONN_STATUS_DOWN, f.inst.msync); |
|
assert(!f.inst.msync->sessions && f.callbacks == 1 && f.result == MT_ERR_DISCONNECTED); |
|
t_destroy(&f); |
|
puts("PASS: 1000 changes coalesce behind backpressure; disconnect completes request with error"); |
|
} |
|
|
|
static void test_peer_restart(void) { |
|
struct fixture f; t_init(&f); |
|
struct ms_session* s = t_session(&f); |
|
assert(ms_send(s, MS_QUERY, 1, NULL, 0) == 0); |
|
t_receive(&f, MS_WAKE, 0, 0, NULL, 0); |
|
assert(s->round > 7 && s->state == MS_BEGIN_WAIT && s->tx && s->tx->dgram[9] == MS_BEGIN); |
|
uint8_t hashes[32 * 32] = {0}; |
|
t_receive(&f, MS_HASHES, 7, 1, hashes, sizeof(hashes)); |
|
assert(!f.callbacks && s->state == MS_BEGIN_WAIT); |
|
t_destroy(&f); |
|
puts("PASS: reattaching follower restarts the round; old responses cannot affect it"); |
|
} |
|
|
|
static void test_send_rejection(void) { |
|
struct fixture f; t_init(&f); |
|
struct ms_session* s = t_session(&f); |
|
queue_set_size_limit(f.conn.send_input_q, 0); |
|
assert(ms_send(s, MS_QUERY, 1, NULL, 0) == 0); |
|
uasync_poll(f.inst.ua, 0); |
|
assert(f.callbacks == 1 && f.result == MT_ERR_IO && !s->tx); |
|
t_destroy(&f); |
|
puts("PASS: transport rejection retains ownership; packet is freed exactly once"); |
|
} |
|
|
|
static void t_cancel_done(uint64_t peer, const char* ns, int result, void* arg) { |
|
struct fixture* f = arg; |
|
t_done(peer, ns, result, arg); |
|
merkle_sync_cancel(&f->inst, peer, ns); |
|
} |
|
|
|
static void test_error_handoff(void) { |
|
struct fixture f; t_init(&f); |
|
struct ms_session* s = t_session(&f); s->requests->cb = t_cancel_done; |
|
s->stack[0].level = MT_MAX_LEVEL; f.apply_error = MT_ERR_DATA; |
|
uint8_t page[11] = {0}; |
|
t_receive(&f, MS_PAGE, 7, 1, page, sizeof(page)); |
|
assert(!f.callbacks && s->state == MS_FAILING); |
|
uasync_poll(f.inst.ua, 0); |
|
assert(f.callbacks == 1 && f.result == MT_ERR_DATA && !f.inst.msync->sessions); |
|
assert(f.conn.send_input_q->head && f.conn.send_input_q->head->dgram[9] == MS_ERROR); |
|
t_destroy(&f); |
|
puts("PASS: ERROR reaches transport before cancellation inside callback"); |
|
} |
|
|
|
static void test_blocked_error_timeout(void) { |
|
struct fixture f; t_init(&f); |
|
struct ll_entry* blocker = queue_entry_new(0); assert(blocker); |
|
assert(queue_data_put(f.conn.send_input_q, blocker) == 0); |
|
struct ms_session* s = t_session(&f); s->requests->cb = t_cancel_done; |
|
ms_fail(s, MT_ERR_DATA, "injected apply failure", 1); |
|
assert(s->deadline && s->state == MS_FAILING && !f.callbacks); |
|
ms_stop_deadline(s); ms_timeout(s); |
|
assert(f.callbacks == 1 && f.result == MT_ERR_TIMEOUT && !f.inst.msync->sessions); |
|
uasync_poll(f.inst.ua, 0); |
|
assert(f.callbacks == 1 && !f.conn.send_input_q->waiter_head); |
|
t_destroy(&f); |
|
puts("PASS: blocked terminal packet times out once and releases waiter/session"); |
|
} |
|
|
|
static int pull_created, pull_committed, pull_released; |
|
static int t_begin_pull(void* ctx, const char* ns, uint64_t peer, uint64_t round, void** pull) { |
|
(void)ctx; (void)ns; (void)peer; (void)round; |
|
*pull = u_malloc(1); assert(*pull); pull_created++; return 0; |
|
} |
|
static int t_commit_pull(void* ctx, void* pull) { (void)ctx; assert(pull); pull_committed++; return 0; } |
|
static void t_abort_pull(void* ctx, void* pull) { (void)ctx; assert(pull); pull_released++; u_free(pull); } |
|
static int t_stage_page(void* ctx, void* pull, uint64_t prefix, int av, uint64_t after, |
|
uint64_t next, int more, const uint8_t* data, size_t len) { |
|
(void)ctx; (void)pull; (void)prefix; (void)av; (void)after; (void)next; (void)more; (void)data; (void)len; return 0; |
|
} |
|
static void test_pull_lifecycle(void) { |
|
struct fixture f; t_init(&f); |
|
struct merkle_sync_data_ops ops = t_ops; |
|
ops.begin_pull = t_begin_pull; ops.commit_pull = t_commit_pull; ops.abort_pull = t_abort_pull; ops.stage_page = t_stage_page; |
|
f.inst.msync->ops = &ops; |
|
struct ms_session* s = t_session(&f); |
|
assert(ms_pull_start(s) == 0 && s->pull); |
|
ms_discard_tx(s); s->stack[0].level = MT_MAX_LEVEL; |
|
assert(ms_walk_next(s) == 0 && !s->pull && pull_committed == 1 && pull_released == 1); |
|
ms_discard_tx(s); |
|
assert(ms_pull_start(s) == 0); |
|
ms_discard_tx(s); assert(ms_begin(s) == 0 && !s->pull && pull_released == 2); |
|
ms_discard_tx(s); assert(ms_pull_start(s) == 0); |
|
ms_conn_status(&f.conn, ETCP_CONN_STATUS_DOWN, f.inst.msync); |
|
assert(pull_created == 3 && pull_committed == 1 && pull_released == 3); |
|
t_destroy(&f); |
|
puts("PASS: pull commits once; restart and disconnect abort exactly their own context"); |
|
} |
|
|
|
int main(void) { |
|
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); |
|
size_t baseline = u_get_allocated_count(); |
|
test_malformed_and_old_round(); test_rejected_data(); test_sibling_walk(); test_cancel_waiters(); |
|
test_page_size_and_cursor(); test_readonly_tree(); test_backpressure_and_disconnect(); test_peer_restart(); |
|
test_send_rejection(); |
|
test_error_handoff(); test_blocked_error_timeout(); test_pull_lifecycle(); |
|
assert(u_get_allocated_count() == baseline); |
|
puts("ALL PASS: protocol regressions, no tracked allocations leaked"); |
|
return 0; |
|
}
|
|
|