Files
utun2/tests/test_dm_media_cleanup.c
T

179 lines
11 KiB
C

/* Координатор S: реальные SQLite/таймеры, ошибки чтения/ACK и повторы после restart.
* Квитанция уже сохранена; сеть перехвачена, криптография и удаление M проверяются test_dm_media. */
#include "../lib/platform_compat.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include "../src/utun_instance.h"
#include "../src/dm/dm_mailbox_media.h"
#include "../src/dm/dm_media.h"
#include "test_clock.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
enum operation { NO_FAULT, PREPARE, BIND_BLOB, BIND_INT, STEP, FINALIZE };
enum statement { SCAN, ACK, BODY };
static enum operation fault;
static enum statement target;
static unsigned hits, deletes;
static uint64_t last_peer;
static const uint8_t id[16] = {1};
static uint8_t receipt[DM_MEDIA_RECEIPT_SIZE];
static int inject(enum operation op, const char* sql) {
const char* prefixes[] = { "SELECT node_id,group_id FROM dm_media_holders", "UPDATE dm_media_holders SET released=1",
"UPDATE dm_media_jobs SET body=NULL" };
if (fault != op || !sql || strncmp(sql, prefixes[target], strlen(prefixes[target]))) return 0;
fault = NO_FAULT; hits++;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "DM cleanup test: statement=%d operation=%d SQL failure", target, op);
return 1;
}
int __real_sqlite3_prepare_v2(sqlite3*, const char*, int, sqlite3_stmt**, const char**);
int __wrap_sqlite3_prepare_v2(sqlite3* db, const char* sql, int len, sqlite3_stmt** st, const char** tail) {
if (inject(PREPARE, sql)) { *st = NULL; return SQLITE_IOERR; }
return __real_sqlite3_prepare_v2(db, sql, len, st, tail);
}
int __real_sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int, void (*)(void*));
int __wrap_sqlite3_bind_blob(sqlite3_stmt* st, int index, const void* data, int len, void (*destroy)(void*)) {
return inject(BIND_BLOB, sqlite3_sql(st)) ? SQLITE_NOMEM : __real_sqlite3_bind_blob(st, index, data, len, destroy);
}
int __real_sqlite3_bind_int64(sqlite3_stmt*, int, sqlite3_int64);
int __wrap_sqlite3_bind_int64(sqlite3_stmt* st, int index, sqlite3_int64 value) {
return inject(BIND_INT, sqlite3_sql(st)) ? SQLITE_NOMEM : __real_sqlite3_bind_int64(st, index, value);
}
int __real_sqlite3_step(sqlite3_stmt*);
int __wrap_sqlite3_step(sqlite3_stmt* st) { return inject(STEP, sqlite3_sql(st)) ? SQLITE_IOERR : __real_sqlite3_step(st); }
int __real_sqlite3_finalize(sqlite3_stmt*);
int __wrap_sqlite3_finalize(sqlite3_stmt* st) {
int fail = st && inject(FINALIZE, sqlite3_sql(st));
int rc = __real_sqlite3_finalize(st);
return fail ? SQLITE_IOERR : rc;
}
/* Пакеты теряются до явного recv: успех send не является подтверждением удаления M. */
int __wrap_dm_media_send(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t peer,
uint8_t cmd, const uint8_t* body, size_t len) {
(void)inst;
assert(group == 77 && cmd == DM_MEDIA_DELETE && len == sizeof(receipt) && !memcmp(body, receipt, len));
deletes++; last_peer = peer; return 0;
}
int __wrap_dm_route_group(struct UTUN_INSTANCE* inst, uint64_t peer, uint64_t preferred, uint64_t* group) {
(void)inst; (void)peer; (void)preferred; (void)group; return 0;
}
static void exec_sql(sqlite3* db, const char* sql) { assert(sqlite3_exec(db, sql, NULL, NULL, NULL) == SQLITE_OK); }
static int scalar(sqlite3* db, const char* sql) {
sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(db, sql, -1, &st, NULL) == SQLITE_OK);
assert(sqlite3_step(st) == SQLITE_ROW); int result = sqlite3_column_int(st, 0);
assert(sqlite3_finalize(st) == SQLITE_OK); return result;
}
static void fixture(sqlite3* db, int pending) {
exec_sql(db, "DELETE FROM dm_media_holders; DELETE FROM dm_media_jobs");
sqlite3_stmt* st = NULL;
assert(sqlite3_prepare_v2(db, "INSERT INTO dm_media_jobs(id,author,recipient,seq,body,body_hash,group_id,holder,stored,receipt)"
" VALUES(?,11,12,1,x'01020304',zeroblob(32),77,102,1,?)", -1, &st, NULL) == SQLITE_OK);
assert(sqlite3_bind_blob(st, 1, id, sizeof(id), SQLITE_STATIC) == SQLITE_OK);
assert(sqlite3_bind_blob(st, 2, receipt, sizeof(receipt), SQLITE_STATIC) == SQLITE_OK);
assert(sqlite3_step(st) == SQLITE_DONE && sqlite3_finalize(st) == SQLITE_OK);
assert(sqlite3_prepare_v2(db, "INSERT INTO dm_media_holders(id,node_id,group_id,released) VALUES(?,101,77,?)",
-1, &st, NULL) == SQLITE_OK);
assert(sqlite3_bind_blob(st, 1, id, sizeof(id), SQLITE_STATIC) == SQLITE_OK);
assert(sqlite3_bind_int(st, 2, !pending) == SQLITE_OK);
assert(sqlite3_step(st) == SQLITE_DONE && sqlite3_finalize(st) == SQLITE_OK);
}
static void check_receipt(sqlite3* db) {
sqlite3_stmt* st = NULL;
assert(sqlite3_prepare_v2(db, "SELECT receipt FROM dm_media_jobs", -1, &st, NULL) == SQLITE_OK);
assert(sqlite3_step(st) == SQLITE_ROW && sqlite3_column_bytes(st, 0) == sizeof(receipt));
assert(!memcmp(sqlite3_column_blob(st, 0), receipt, sizeof(receipt)));
assert(sqlite3_finalize(st) == SQLITE_OK && sqlite3_get_autocommit(db));
}
/* Каждый отказ чтения должен сохранить тело и отложить отправку DELETE до успешного снимка. */
static void scan_failures(struct UTUN_INSTANCE* inst) {
const enum operation ops[] = {PREPARE, BIND_BLOB, STEP, FINALIZE};
for (unsigned i = 0; i < sizeof(ops) / sizeof(*ops); i++) {
fixture(inst->topo_sqlite_db, 1);
unsigned before = deletes;
target = SCAN; fault = ops[i]; hits = 0;
test_clock_next_timer(inst->ua);
assert(hits == 1 && deletes == before);
assert(scalar(inst->topo_sqlite_db, "SELECT body IS NOT NULL FROM dm_media_jobs") == 1);
assert(scalar(inst->topo_sqlite_db, "SELECT released FROM dm_media_holders") == 0);
check_receipt(inst->topo_sqlite_db);
exec_sql(inst->topo_sqlite_db, "UPDATE dm_media_jobs SET retry_at=0");
test_clock_next_timer(inst->ua);
assert(deletes == before + 1 && last_peer == 101);
}
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "DM cleanup test: scan prepare/bind/step/finalize preserve pending body and retry PASS");
}
/* Запись ACK атомарна сама по себе; finalize после успешного autocommit уже не отменяет запись. */
static void write_failures(struct UTUN_INSTANCE* inst, struct dm_mb_media* media) {
for (target = ACK; target <= BODY; target++) for (enum operation op = PREPARE; op <= FINALIZE; op++) {
if (target == BODY && op == BIND_INT) continue;
fixture(inst->topo_sqlite_db, target == ACK);
fault = op; hits = 0;
if (target == ACK) dm_mailbox_media_recv(media, 77, 101, DM_MEDIA_DELETE_ACK, id, sizeof(id));
else test_clock_next_timer(inst->ua);
assert(hits == 1);
if (target == ACK) {
assert(scalar(inst->topo_sqlite_db, "SELECT released FROM dm_media_holders") == (op == FINALIZE));
dm_mailbox_media_recv(media, 77, 101, DM_MEDIA_DELETE_ACK, id, sizeof(id));
} else {
assert(scalar(inst->topo_sqlite_db, "SELECT body IS NULL FROM dm_media_jobs") == (op == FINALIZE));
}
check_receipt(inst->topo_sqlite_db);
exec_sql(inst->topo_sqlite_db, "UPDATE dm_media_jobs SET retry_at=0");
test_clock_next_timer(inst->ua);
assert(scalar(inst->topo_sqlite_db, "SELECT body IS NULL FROM dm_media_jobs") == 1);
check_receipt(inst->topo_sqlite_db);
}
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "DM cleanup test: ACK/body SQL failures retry without losing receipt PASS");
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_DM, DEBUG_LEVEL_DEBUG);
size_t before = u_get_allocated_count();
test_clock_init(10000); test_clock_sync_wall_time();
for (unsigned i = 0; i < sizeof(receipt); i++) receipt[i] = (uint8_t)(i + 3);
char path[] = "/tmp/utun_dm_cleanup_XXXXXX"; int fd = mkstemp(path); assert(fd >= 0); close(fd);
struct UTUN_INSTANCE inst = {0}; inst.ua = uasync_create(); assert(inst.ua);
assert(sqlite3_open(path, &inst.topo_sqlite_db) == SQLITE_OK);
struct dm_mb_media* media = dm_mailbox_media_create(&inst); assert(media);
scan_failures(&inst); write_failures(&inst, media);
fixture(inst.topo_sqlite_db, 1);
test_clock_next_timer(inst.ua); // DELETE потерян; повтор восстанавливается из той же БД.
unsigned sent = deletes;
dm_mailbox_media_destroy(media); assert(sqlite3_close(inst.topo_sqlite_db) == SQLITE_OK);
assert(sqlite3_open(path, &inst.topo_sqlite_db) == SQLITE_OK);
media = dm_mailbox_media_create(&inst); assert(media);
test_clock_next_timer(inst.ua); assert(deletes == sent + 1);
assert(scalar(inst.topo_sqlite_db, "SELECT body IS NOT NULL FROM dm_media_jobs") == 1);
check_receipt(inst.topo_sqlite_db);
dm_mailbox_media_recv(media, 78, 101, DM_MEDIA_DELETE_ACK, id, sizeof(id));
dm_mailbox_media_recv(media, 77, 999, DM_MEDIA_DELETE_ACK, id, sizeof(id));
assert(scalar(inst.topo_sqlite_db, "SELECT released FROM dm_media_holders") == 0);
exec_sql(inst.topo_sqlite_db, "INSERT INTO dm_media_holders SELECT id,102,77,0 FROM dm_media_jobs");
dm_mailbox_media_recv(media, 77, 101, DM_MEDIA_DELETE_ACK, id, sizeof(id));
dm_mailbox_media_recv(media, 77, 101, DM_MEDIA_DELETE_ACK, id, sizeof(id));
exec_sql(inst.topo_sqlite_db, "UPDATE dm_media_jobs SET retry_at=0"); test_clock_next_timer(inst.ua);
assert(last_peer == 102 && scalar(inst.topo_sqlite_db, "SELECT body IS NOT NULL FROM dm_media_jobs") == 1);
dm_mailbox_media_recv(media, 77, 102, DM_MEDIA_DELETE_ACK, id, sizeof(id));
exec_sql(inst.topo_sqlite_db, "UPDATE dm_media_jobs SET retry_at=0"); test_clock_next_timer(inst.ua);
assert(scalar(inst.topo_sqlite_db, "SELECT body IS NULL FROM dm_media_jobs") == 1);
uint8_t stored[24] = {1};
dm_mailbox_media_recv(media, 77, 101, DM_MEDIA_STORED, stored, sizeof(stored));
assert(last_peer == 101 && scalar(inst.topo_sqlite_db, "SELECT count(*) FROM dm_media_holders WHERE released=0") == 0);
check_receipt(inst.topo_sqlite_db);
dm_mailbox_media_destroy(media); assert(sqlite3_close(inst.topo_sqlite_db) == SQLITE_OK);
uasync_destroy(inst.ua, 0); assert(!unlink(path)); test_clock_finish();
assert(u_get_allocated_count() == before);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "DM cleanup test: restart, lost/duplicate/foreign ACK, multiple holders, late STORED PASS");
return 0;
}