// test_media_delivery_sql.c — SQL-таблицы + протокольные структуры media_delivery // // Покрытие: // 1. Создание таблиц block_availability + super_sync + индексов // 2. INSERT OR REPLACE (upsert) — дубликат не создаёт вторую строку // 3. SELECT с фильтром по block_uuid, сортировка timestamp DESC // 4. SELECT с id > N, сортировка ASC, LIMIT // 5. super_sync: INSERT OR REPLACE + SELECT (дефолт 0) // 6. DELETE WHERE node_id=? // 7. DELETE WHERE node_id!=? (чистка чужих при выключении суперузла) // 8. Размеры packed-структур // 9. Проверка полей packed-структур (нет padding-сюрпризов) // 10. Построение/разбор MEDIA_QUERY // 11. Построение/разбор MEDIA_HAVE_BLOCK // 12. Построение/разбор MEDIA_SUPER_HELLO #include "../src/media_delivery/media_delivery_proto.h" #include "../lib/debug_config.h" #include #include #include #include #include static int g_passed = 0, g_failed = 0, g_total = 0; #define TEST(name) do { g_total++; printf(" %-50s", name); } while(0) #define OK() do { g_passed++; printf("OK\n"); } while(0) #define FAIL(fmt, ...) do { g_failed++; printf("FAIL: " fmt "\n", ##__VA_ARGS__); } while(0) static void make_uuid(uint8_t buf[16]) { for (int i = 0; i < 16; i++) buf[i] = (uint8_t)(rand() & 0xFF); } static sqlite3* make_db(void) { sqlite3* db = NULL; sqlite3_open(":memory:", &db); sqlite3_exec(db, "PRAGMA journal_mode=memory", NULL, NULL, NULL); return db; } static int create_tables(sqlite3* db) { const char* sql_ba = "CREATE TABLE IF NOT EXISTS block_availability (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " block_uuid BLOB NOT NULL," " group_id INTEGER NOT NULL," " node_id INTEGER NOT NULL," " chunk INTEGER NOT NULL," " timestamp INTEGER NOT NULL)"; int rc = sqlite3_exec(db, sql_ba, NULL, NULL, NULL); if (rc != SQLITE_OK) return -1; sqlite3_exec(db, "CREATE UNIQUE INDEX IF NOT EXISTS idx_ba_uuid_node ON block_availability(block_uuid, node_id)", NULL, NULL, NULL); sqlite3_exec(db, "CREATE INDEX IF NOT EXISTS idx_ba_id ON block_availability(id)", NULL, NULL, NULL); sqlite3_exec(db, "CREATE INDEX IF NOT EXISTS idx_ba_node_id ON block_availability(node_id)", NULL, NULL, NULL); const char* sql_ss = "CREATE TABLE IF NOT EXISTS super_sync (" " peer_node_id INTEGER PRIMARY KEY," " last_recv_id INTEGER NOT NULL DEFAULT 0)"; rc = sqlite3_exec(db, sql_ss, NULL, NULL, NULL); return (rc == SQLITE_OK) ? 0 : -1; } static int table_exists(sqlite3* db, const char* name) { sqlite3_stmt* s = NULL; int ok = 0; if (sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type='table' AND name=?", -1, &s, NULL) == SQLITE_OK) { sqlite3_bind_text(s, 1, name, -1, SQLITE_STATIC); ok = (sqlite3_step(s) == SQLITE_ROW); sqlite3_finalize(s); } return ok; } static int index_exists(sqlite3* db, const char* name) { sqlite3_stmt* s = NULL; int ok = 0; if (sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type='index' AND name=?", -1, &s, NULL) == SQLITE_OK) { sqlite3_bind_text(s, 1, name, -1, SQLITE_STATIC); ok = (sqlite3_step(s) == SQLITE_ROW); sqlite3_finalize(s); } return ok; } static int count_rows(sqlite3* db, const char* table) { char sql[128]; snprintf(sql, sizeof(sql), "SELECT COUNT(*) FROM %s", table); sqlite3_stmt* s = NULL; int n = -1; if (sqlite3_prepare_v2(db, sql, -1, &s, NULL) == SQLITE_OK) { if (sqlite3_step(s) == SQLITE_ROW) n = sqlite3_column_int(s, 0); sqlite3_finalize(s); } return n; } /* ── test cases ── */ static void test_tables(void) { TEST("create tables exist"); { sqlite3* db = make_db(); if (create_tables(db) != 0) { FAIL("create_tables failed"); sqlite3_close(db); return; } int ba = table_exists(db, "block_availability"); int ss = table_exists(db, "super_sync"); if (ba && ss) OK(); else FAIL("ba=%d ss=%d", ba, ss); sqlite3_close(db); } TEST("create tables idempotent"); { sqlite3* db = make_db(); if (create_tables(db) != 0) { FAIL("first create"); sqlite3_close(db); return; } if (create_tables(db) != 0) { FAIL("second create"); sqlite3_close(db); return; } OK(); sqlite3_close(db); } TEST("indices exist"); { sqlite3* db = make_db(); create_tables(db); int i1 = index_exists(db, "idx_ba_uuid_node"); int i2 = index_exists(db, "idx_ba_id"); int i3 = index_exists(db, "idx_ba_node_id"); if (i1 && i2 && i3) OK(); else FAIL("i1=%d i2=%d i3=%d", i1, i2, i3); sqlite3_close(db); } } static void test_ba_insert(void) { TEST("ba_insert basic"); { sqlite3* db = make_db(); create_tables(db); uint8_t uuid[16]; make_uuid(uuid); uint64_t gid = 0x100, nid = 0x200; const char* sql = "INSERT INTO block_availability(block_uuid,group_id,node_id,chunk,timestamp) VALUES(?,?,?,?,?)"; for (int i = 0; i < 3; i++) { sqlite3_stmt* s = NULL; sqlite3_prepare_v2(db, sql, -1, &s, NULL); sqlite3_bind_blob(s, 1, uuid, 16, SQLITE_STATIC); sqlite3_bind_int64(s, 2, (sqlite3_int64)gid); sqlite3_bind_int64(s, 3, (sqlite3_int64)(nid + (uint64_t)i)); sqlite3_bind_int(s, 4, i); sqlite3_bind_int64(s, 5, (sqlite3_int64)time(NULL)); sqlite3_step(s); sqlite3_finalize(s); } int n = count_rows(db, "block_availability"); if (n == 3) OK(); else FAIL("expected 3 rows got %d", n); sqlite3_close(db); } TEST("ba_insert upsert (unique uuid+node)"); { sqlite3* db = make_db(); create_tables(db); uint8_t uuid[16]; make_uuid(uuid); const char* sql = "INSERT OR REPLACE INTO block_availability(block_uuid,group_id,node_id,chunk,timestamp) VALUES(?,?,?,?,?)"; for (int pass = 0; pass < 2; pass++) { sqlite3_stmt* s = NULL; sqlite3_prepare_v2(db, sql, -1, &s, NULL); sqlite3_bind_blob(s, 1, uuid, 16, SQLITE_STATIC); sqlite3_bind_int64(s, 2, (sqlite3_int64)1); sqlite3_bind_int64(s, 3, (sqlite3_int64)0x300); sqlite3_bind_int(s, 4, 0); sqlite3_bind_int64(s, 5, (sqlite3_int64)(pass + 100)); sqlite3_step(s); sqlite3_finalize(s); } int n = count_rows(db, "block_availability"); if (n == 1) OK(); else FAIL("expected 1 row (upsert) got %d", n); sqlite3_close(db); } } static void test_ba_find(void) { TEST("ba_find nodes by uuid"); { sqlite3* db = make_db(); create_tables(db); uint8_t uuid[16]; make_uuid(uuid); const char* ins = "INSERT INTO block_availability(block_uuid,group_id,node_id,chunk,timestamp) VALUES(?,?,?,?,?)"; for (int i = 0; i < 5; i++) { sqlite3_stmt* s = NULL; sqlite3_prepare_v2(db, ins, -1, &s, NULL); sqlite3_bind_blob(s, 1, uuid, 16, SQLITE_STATIC); sqlite3_bind_int64(s, 2, (sqlite3_int64)1); sqlite3_bind_int64(s, 3, (sqlite3_int64)(0x10 + i)); sqlite3_bind_int(s, 4, i); sqlite3_bind_int64(s, 5, (sqlite3_int64)(2000 - i)); sqlite3_step(s); sqlite3_finalize(s); } const char* q = "SELECT node_id FROM block_availability WHERE block_uuid=? ORDER BY timestamp DESC LIMIT 3"; sqlite3_stmt* s = NULL; sqlite3_prepare_v2(db, q, -1, &s, NULL); sqlite3_bind_blob(s, 1, uuid, 16, SQLITE_STATIC); int count = 0, first_node = -1; while (sqlite3_step(s) == SQLITE_ROW) { if (count == 0) first_node = sqlite3_column_int(s, 0); count++; } sqlite3_finalize(s); if (count == 3 && first_node == 0x10) OK(); else FAIL("count=%d first_node=0x%x", count, first_node); sqlite3_close(db); } } static void test_ba_get_since(void) { TEST("ba_get_since id > N"); { sqlite3* db = make_db(); create_tables(db); uint8_t uuid[16]; make_uuid(uuid); const char* ins = "INSERT INTO block_availability(block_uuid,group_id,node_id,chunk,timestamp) VALUES(?,?,?,?,?)"; for (int i = 0; i < 6; i++) { sqlite3_stmt* s = NULL; sqlite3_prepare_v2(db, ins, -1, &s, NULL); sqlite3_bind_blob(s, 1, uuid, 16, SQLITE_STATIC); sqlite3_bind_int64(s, 2, (sqlite3_int64)1); sqlite3_bind_int64(s, 3, (sqlite3_int64)(0x20 + i)); sqlite3_bind_int(s, 4, i); sqlite3_bind_int64(s, 5, (sqlite3_int64)1000); sqlite3_step(s); sqlite3_finalize(s); } const char* q = "SELECT id FROM block_availability WHERE id > ? ORDER BY id ASC LIMIT 4"; sqlite3_stmt* s = NULL; sqlite3_prepare_v2(db, q, -1, &s, NULL); sqlite3_bind_int64(s, 1, 1); int count = 0; while (sqlite3_step(s) == SQLITE_ROW) count++; sqlite3_finalize(s); if (count == 4) OK(); else FAIL("expected 4 rows got %d", count); sqlite3_close(db); } } static void test_ss_ops(void) { TEST("super_sync insert+select"); { sqlite3* db = make_db(); create_tables(db); sqlite3_exec(db, "INSERT OR REPLACE INTO super_sync(peer_node_id,last_recv_id) VALUES(0xAA, 42)", NULL, NULL, NULL); sqlite3_stmt* s = NULL; sqlite3_prepare_v2(db, "SELECT last_recv_id FROM super_sync WHERE peer_node_id=?", -1, &s, NULL); sqlite3_bind_int64(s, 1, 0xAA); int v = -1; if (sqlite3_step(s) == SQLITE_ROW) v = sqlite3_column_int(s, 0); sqlite3_finalize(s); if (v == 42) OK(); else FAIL("expected 42 got %d", v); sqlite3_close(db); } TEST("super_sync default 0"); { sqlite3* db = make_db(); create_tables(db); sqlite3_stmt* s = NULL; sqlite3_prepare_v2(db, "SELECT last_recv_id FROM super_sync WHERE peer_node_id=?", -1, &s, NULL); sqlite3_bind_int64(s, 1, 0xCC); int v = -1; if (sqlite3_step(s) == SQLITE_ROW) v = sqlite3_column_int(s, 0); else v = 0; /* no row = default */ sqlite3_finalize(s); if (v == 0) OK(); else FAIL("expected 0 got %d", v); sqlite3_close(db); } } static void test_ba_delete(void) { TEST("ba_delete by node_id"); { sqlite3* db = make_db(); create_tables(db); uint8_t uuid[16]; make_uuid(uuid); const char* ins = "INSERT INTO block_availability(block_uuid,group_id,node_id,chunk,timestamp) VALUES(?,?,?,?,?)"; sqlite3_stmt* s = NULL; sqlite3_prepare_v2(db, ins, -1, &s, NULL); sqlite3_bind_blob(s, 1, uuid, 16, SQLITE_STATIC); sqlite3_bind_int64(s, 2, 1); sqlite3_bind_int64(s, 3, 0x500); sqlite3_bind_int(s, 4, 0); sqlite3_bind_int64(s, 5, 100); sqlite3_step(s); sqlite3_finalize(s); sqlite3_prepare_v2(db, ins, -1, &s, NULL); sqlite3_bind_blob(s, 1, uuid, 16, SQLITE_STATIC); sqlite3_bind_int64(s, 2, 1); sqlite3_bind_int64(s, 3, 0x600); sqlite3_bind_int(s, 4, 0); sqlite3_bind_int64(s, 5, 100); sqlite3_step(s); sqlite3_finalize(s); if (count_rows(db, "block_availability") != 2) { FAIL("setup failed"); sqlite3_close(db); return; } sqlite3_exec(db, "DELETE FROM block_availability WHERE node_id=0x500", NULL, NULL, NULL); int n = count_rows(db, "block_availability"); if (n == 1) OK(); else FAIL("expected 1 got %d", n); sqlite3_close(db); } TEST("ba_delete foreign (node_id != self)"); { sqlite3* db = make_db(); create_tables(db); uint8_t uuid[16]; make_uuid(uuid); const char* ins = "INSERT INTO block_availability(block_uuid,group_id,node_id,chunk,timestamp) VALUES(?,?,?,?,?)"; sqlite3_stmt* s = NULL; for (int i = 0; i < 3; i++) { sqlite3_prepare_v2(db, ins, -1, &s, NULL); sqlite3_bind_blob(s, 1, uuid, 16, SQLITE_STATIC); sqlite3_bind_int64(s, 2, 1); sqlite3_bind_int64(s, 3, (sqlite3_int64)(0x700 + i)); sqlite3_bind_int(s, 4, i); sqlite3_bind_int64(s, 5, 100); sqlite3_step(s); sqlite3_finalize(s); } char buf[128]; snprintf(buf, sizeof(buf), "DELETE FROM block_availability WHERE node_id!=0x700"); sqlite3_exec(db, buf, NULL, NULL, NULL); int n = count_rows(db, "block_availability"); if (n == 1) OK(); else FAIL("expected 1 (self) got %d", n); sqlite3_close(db); } } static void test_proto_sizes(void) { TEST("packed struct sizes"); { int ok = 1; if (sizeof(struct media_pkt_query) != 27) { FAIL("query: %zu != 27", sizeof(struct media_pkt_query)); ok = 0; } if (sizeof(struct media_pkt_have_block) != 117) { FAIL("have_block: %zu != 117", sizeof(struct media_pkt_have_block)); ok = 0; } if (sizeof(struct media_pkt_super_hello) != 9) { FAIL("super_hello: %zu != 9", sizeof(struct media_pkt_super_hello)); ok = 0; } if (sizeof(struct media_pkt_super_ack) != 5) { FAIL("super_ack: %zu != 5", sizeof(struct media_pkt_super_ack)); ok = 0; } if (sizeof(struct media_pkt_block_req) != 53) { FAIL("block_req: %zu != 53", sizeof(struct media_pkt_block_req)); ok = 0; } if (sizeof(struct media_pkt_cancel) != 37) { FAIL("cancel: %zu != 37", sizeof(struct media_pkt_cancel)); ok = 0; } if (sizeof(struct media_pkt_block_done) != 105) { FAIL("block_done: %zu != 105", sizeof(struct media_pkt_block_done)); ok = 0; } if (sizeof(struct media_pkt_query_resp_entry) != 30) { FAIL("query_resp_entry: %zu != 30", sizeof(struct media_pkt_query_resp_entry)); ok = 0; } if (ok) OK(); /* continue running even if sizes mismatch */ } } static void test_proto_build(void) { TEST("build MEDIA_QUERY"); { uint8_t mid[16]; memset(mid, 0xAB, 16); uint8_t bids[32]; memset(bids, 0xCD, 32); uint8_t pkt[256]; struct media_pkt_query* q = (struct media_pkt_query*)pkt; q->subcmd = MEDIA_SUBCMD_QUERY; q->group_id = 0x12345678; memcpy(q->media_id, mid, 16); q->num_blocks = 2; memcpy(pkt + MEDIA_QUERY_HDR_SIZE, bids, 32); if (q->subcmd == MEDIA_SUBCMD_QUERY && q->group_id == 0x12345678 && q->num_blocks == 2 && memcmp(q->media_id, mid, 16) == 0 && memcmp(pkt + MEDIA_QUERY_HDR_SIZE, bids, 32) == 0) OK(); else FAIL(); } TEST("build MEDIA_HAVE_BLOCK"); { struct media_pkt_have_block hb; memset(&hb, 0, sizeof(hb)); hb.subcmd = MEDIA_SUBCMD_HAVE_BLOCK; hb.group_id = 0xAA; hb.chunk = 5; hb.timestamp = 999; if (hb.subcmd == MEDIA_SUBCMD_HAVE_BLOCK && hb.group_id == 0xAA && hb.chunk == 5 && hb.timestamp == 999) OK(); else FAIL(); } TEST("build MEDIA_SUPER_HELLO"); { struct media_pkt_super_hello sh; sh.subcmd = MEDIA_SUBCMD_SUPER_HELLO; sh.last_recv_id = 12345; if (sh.subcmd == MEDIA_SUBCMD_SUPER_HELLO && sh.last_recv_id == 12345) OK(); else FAIL(); } } int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); srand((unsigned)time(NULL)); printf("=== test_media_delivery_sql ===\n"); test_tables(); test_ba_insert(); test_ba_find(); test_ba_get_since(); test_ss_ops(); test_ba_delete(); test_proto_sizes(); test_proto_build(); printf("\n%d/%d passed, %d failed\n", g_passed, g_total, g_failed); return g_failed > 0 ? 1 : 0; }