// test_media_delivery_download.c — юнит-тесты скачивания блоков (блочный конечный автомат) // // Покрытие: // - запись чанков по offset (идемпотентность при дублях) // - BLOCK_DONE: сигнатура валидна → сборка; неверная → фейловер // - фейловер блока по простою (фейковое время) // - исчерпание попыток → FAILED // - проверка простоя md_dl_check_stall (только зависшие блоки) // - OVERLOADED → следующий держатель / WAIT // - RELAY_FULL → добавление держателей // - пустой QUERY_RESP // - назначение держателей + разброс по узлам // - дедуп повторного media_download_start #include "test_utils.h" #include "media_delivery.h" #include "media_delivery_proto.h" #include "media_download.h" #include "media_index.h" #include "../lib/u_async.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #include "../transport_layer/secure_channel.h" #include "../routing_layer/topo_node_sqlite.h" #include "../routing_layer/conn_mgr.h" #include #include #include #include #include #include #include #define TEMP_DIR "/tmp/test_mdl_dl_XXXXXX" #define CHUNK_SIZE 100 #define NUM_BLOCKS 3 static int g_passed = 0, g_failed = 0, g_total = 0; static char g_temp_dir[256]; static struct UASYNC* g_ua = NULL; #define TEST(name) do { g_total++; printf(" %-58s", name); fflush(stdout); } 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) /* ── helpers ── */ static void make_uuid(uint8_t buf[16]) { for (int i = 0; i < 16; i++) buf[i] = (uint8_t)(rand() & 0xFF); } static void write_file(const char* path, const uint8_t* data, size_t len) { FILE* f = fopen(path, "wb"); if (f) { fwrite(data, 1, len, f); fclose(f); } } static int file_exists(const char* path) { return access(path, F_OK) == 0; } static int file_size(const char* path) { struct stat st; if (stat(path, &st) != 0) return -1; return (int)st.st_size; } static void test_setup(void) { snprintf(g_temp_dir, sizeof(g_temp_dir), "%s", TEMP_DIR); if (test_mkdtemp(g_temp_dir) != 0) { fprintf(stderr, "mkdtemp failed\n"); exit(1); } debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); if (getenv("UTUN_TEST_DEBUG")) { debug_set_category_level_by_name("media", "trace"); debug_set_category_level_by_name("general", "info"); } g_ua = uasync_create(); } static void test_cleanup(void) { char cmd[512]; snprintf(cmd, sizeof(cmd), "rm -rf %s", g_temp_dir); system(cmd); if (g_ua) { uasync_destroy(g_ua, 0); g_ua = NULL; } } /* ── минимальный UTUN_INSTANCE с media_delivery ── */ #include "../src/utun_instance.h" static struct UTUN_INSTANCE* make_minimal_instance(sqlite3* db) { struct UTUN_INSTANCE* inst = u_calloc(1, sizeof(*inst)); if (!inst) return NULL; inst->ua = g_ua; inst->node_id = 0xDEADBEEFDEADBEEFULL; inst->topo_sqlite_db = db; for (int i = 0; i < 32; i++) inst->my_ed25519_privkey[i] = (uint8_t)(rand() & 0xFF); memset(inst->my_ed25519_pubkey, 0xDD, 32); inst->md.inst = inst; inst->md.db = db; inst->md.self_node_id = inst->node_id; inst->md.initialized = 1; inst->md.dl_stall_timeout_tb = 20000; /* 2s */ inst->md.dl_max_attempts = 3; return inst; } /* создаёт загрузку и регистрирует в очереди; возвращает указатель на данные в очереди */ static struct media_download* make_test_dl(sqlite3* db, struct UTUN_INSTANCE* inst, int num_blocks) { if (!inst->md.downloads) inst->md.downloads = queue_new(inst->ua, 256, offsetof(struct media_download, media_id), 16, "md_dl_test"); struct media_download* dl = u_calloc(1, sizeof(*dl)); if (!dl) return NULL; make_uuid(dl->media_id); dl->num_blocks = num_blocks; snprintf(dl->dest_path, sizeof(dl->dest_path), "%s/test.bin", g_temp_dir); dl->blocks = u_calloc((size_t)num_blocks, sizeof(struct media_download_block)); dl->block_sigs = u_calloc((size_t)num_blocks, 64); for (int i = 0; i < num_blocks; i++) { make_uuid(dl->blocks[i].block_id); memset(dl->block_sigs + i * 64, (uint8_t)(0x42 + i), 64); dl->blocks[i].state = MD_BLK_IDLE; dl->blocks[i].holder_idx = -1; dl->blocks[i].expected_size = CHUNK_SIZE; } dl->block_size = CHUNK_SIZE; dl->file_size = (int64_t)CHUNK_SIZE * num_blocks; dl->active = 1; dl->inst = inst; dl->author_node_id = 0xAAAA000000000001ULL; memcpy(dl->ll.data, dl->media_id, 16); struct ll_entry* qe = queue_entry_new(sizeof(*dl)); if (!qe) { u_free(dl->blocks); u_free(dl->block_sigs); u_free(dl); return NULL; } memcpy(qe->data, dl, sizeof(*dl)); u_free(dl); queue_data_put_with_index(inst->md.downloads, qe); return (struct media_download*)qe->data; } static void free_test_dl(struct UTUN_INSTANCE* inst, struct media_download* dl) { if (!dl) return; if (dl->watchdog_timer) { uasync_cancel_timeout(inst->ua, dl->watchdog_timer); dl->watchdog_timer = NULL; } u_free(dl->blocks); u_free(dl->block_sigs); if (inst->md.downloads) { struct ll_entry* e = queue_find_data_by_index(inst->md.downloads, dl->media_id); if (e) { queue_remove_data(inst->md.downloads, e); queue_entry_free(e); } } } static void add_holder(struct media_download* dl, int bi, uint64_t node_id) { struct media_download_block* b = &dl->blocks[bi]; if (b->num_holders < MD_MAX_HOLDERS_PER_BLOCK) b->holders[b->num_holders++] = node_id; } /* ── 1. запись чанков по offset ── */ static void test_chunk_offset_write(void) { TEST("chunk write by offset (dup/idempotent)"); { sqlite3* db = NULL; sqlite3_open(":memory:", &db); struct UTUN_INSTANCE* inst = make_minimal_instance(db); struct media_download* dl = make_test_dl(db, inst, 1); if (!dl) { FAIL("make_test_dl"); sqlite3_close(db); u_free(inst); return; } uint8_t cdata[256]; for (int i = 0; i < 256; i++) cdata[i] = (uint8_t)(i & 0xFF); uint8_t pkt[MEDIA_BLOCK_CHUNK_HDR_SIZE + sizeof(cdata)]; struct media_pkt_block_chunk* ch = (struct media_pkt_block_chunk*)pkt; memset(ch, 0, MEDIA_BLOCK_CHUNK_HDR_SIZE); ch->subcmd = MEDIA_SUBCMD_BLOCK_CHUNK; memcpy(ch->media_id, dl->media_id, 16); memcpy(ch->block_id, dl->blocks[0].block_id, 16); ch->chunk = 0; ch->offset = 0; ch->data_len = (uint16_t)sizeof(cdata); memcpy(pkt + MEDIA_BLOCK_CHUNK_HDR_SIZE, cdata, sizeof(cdata)); media_download_handle_chunk(inst, pkt, sizeof(pkt)); char tmp[1024]; snprintf(tmp, sizeof(tmp), "%s.chunk_0", dl->dest_path); int s1 = file_size(tmp); /* дубль с тем же offset — не должен добавить данные */ media_download_handle_chunk(inst, pkt, sizeof(pkt)); int s2 = file_size(tmp); /* чанк со смещением 200 — пишется по offset, размер растёт до 200+256 */ ch->offset = 200; media_download_handle_chunk(inst, pkt, sizeof(pkt)); int s3 = file_size(tmp); int ok = s1 == 256 && s2 == 256 && s3 == 456 && dl->blocks[0].bytes_received == 456; if (ok) OK(); else FAIL("s1=%d s2=%d s3=%d bytes=%u", s1, s2, s3, dl->blocks[0].bytes_received); free_test_dl(inst, dl); sqlite3_close(db); u_free(inst); } } /* ── 2. BLOCK_DONE: сборка ── */ static void test_done_assembly(void) { TEST("BLOCK_DONE all blocks → assembly"); { sqlite3* db = NULL; sqlite3_open(":memory:", &db); struct UTUN_INSTANCE* inst = make_minimal_instance(db); struct media_download* dl = make_test_dl(db, inst, 2); if (!dl) { FAIL("make_test_dl"); sqlite3_close(db); u_free(inst); return; } uint8_t d0[CHUNK_SIZE], d1[CHUNK_SIZE]; memset(d0, 0xA0, CHUNK_SIZE); memset(d1, 0xB1, CHUNK_SIZE); { char c[1024]; snprintf(c, sizeof(c), "%s.chunk_0", dl->dest_path); write_file(c, d0, CHUNK_SIZE); } { char c[1024]; snprintf(c, sizeof(c), "%s.chunk_1", dl->dest_path); write_file(c, d1, CHUNK_SIZE); } char dest[1024]; snprintf(dest, sizeof(dest), "%s", dl->dest_path); for (int bi = 0; bi < 2; bi++) { uint8_t pkt[MEDIA_BLOCK_DONE_SIZE]; memset(pkt, 0, sizeof(pkt)); struct media_pkt_block_done* bd = (struct media_pkt_block_done*)pkt; bd->subcmd = MEDIA_SUBCMD_BLOCK_DONE; memcpy(bd->media_id, dl->media_id, 16); memcpy(bd->block_id, dl->blocks[bi].block_id, 16); bd->chunk = (uint32_t)bi; bd->total_size = CHUNK_SIZE; memcpy(bd->block_sig, dl->block_sigs + bi * 64, 64); media_download_handle_done(inst, pkt, sizeof(pkt)); } int sz = file_size(dest); if (sz == CHUNK_SIZE * 2) OK(); else FAIL("assembled size %d != %d", sz, CHUNK_SIZE * 2); /* dl уже освобождён через md_dl_finish */ sqlite3_close(db); u_free(inst); } } /* ── 3. BLOCK_DONE: неверная сигнатура → фейловер ── */ static void test_done_bad_sig(void) { TEST("BLOCK_DONE bad sig → failover (no assembly)"); { sqlite3* db = NULL; sqlite3_open(":memory:", &db); struct UTUN_INSTANCE* inst = make_minimal_instance(db); struct media_download* dl = make_test_dl(db, inst, 1); if (!dl) { FAIL("make_test_dl"); sqlite3_close(db); u_free(inst); return; } add_holder(dl, 0, 0x1111); dl->blocks[0].holder_idx = 0; uint8_t data[CHUNK_SIZE]; memset(data, 0xFF, CHUNK_SIZE); { char c[1024]; snprintf(c, sizeof(c), "%s.chunk_0", dl->dest_path); write_file(c, data, CHUNK_SIZE); } uint8_t pkt[MEDIA_BLOCK_DONE_SIZE]; memset(pkt, 0, sizeof(pkt)); struct media_pkt_block_done* bd = (struct media_pkt_block_done*)pkt; bd->subcmd = MEDIA_SUBCMD_BLOCK_DONE; memcpy(bd->media_id, dl->media_id, 16); memcpy(bd->block_id, dl->blocks[0].block_id, 16); bd->chunk = 0; bd->total_size = CHUNK_SIZE; memset(bd->block_sig, 0xAA, 64); /* WRONG */ media_download_handle_done(inst, pkt, sizeof(pkt)); /* файл не собран; блок в WAIT (1 держатель, попытка ушла) */ int ok = dl->blocks_validated == 0 && !dl->assembled && dl->blocks[0].attempts == 1 && dl->blocks[0].state == MD_BLK_WAIT; if (ok) OK(); else FAIL("validated=%d assembled=%d attempts=%d state=%d", dl->blocks_validated, dl->assembled, dl->blocks[0].attempts, dl->blocks[0].state); free_test_dl(inst, dl); sqlite3_close(db); u_free(inst); } } /* ── 4. фейловер блока по простою (фейковое время) ── */ static void test_failover_stall(void) { TEST("failover on stall → next holder"); { sqlite3* db = NULL; sqlite3_open(":memory:", &db); struct UTUN_INSTANCE* inst = make_minimal_instance(db); struct media_download* dl = make_test_dl(db, inst, 1); if (!dl) { FAIL("make_test_dl"); sqlite3_close(db); u_free(inst); return; } add_holder(dl, 0, 0x1111); add_holder(dl, 0, 0x2222); dl->blocks[0].holder_idx = 0; dl->blocks[0].state = MD_BLK_RECV; dl->blocks[0].bytes_received = 12345; dl->inflight = 1; uint64_t now = 1000000000ULL; dl->blocks[0].last_progress_tb = now - 30000; /* 3s назад */ int rc = md_dl_failover_block(dl, 0, now); int ok = rc == 0 && dl->blocks[0].attempts == 1 && dl->blocks[0].holder_idx == 1 && dl->blocks[0].bytes_received == 0 && dl->blocks[0].state == MD_BLK_REQ; if (ok) OK(); else FAIL("rc=%d attempts=%d hidx=%d bytes=%u state=%d", rc, dl->blocks[0].attempts, dl->blocks[0].holder_idx, dl->blocks[0].bytes_received, dl->blocks[0].state); free_test_dl(inst, dl); sqlite3_close(db); u_free(inst); } } /* ── 5. исчерпание попыток → FAILED ── */ static void test_attempts_exhausted(void) { TEST("attempts exhausted → FAILED"); { sqlite3* db = NULL; sqlite3_open(":memory:", &db); struct UTUN_INSTANCE* inst = make_minimal_instance(db); struct media_download* dl = make_test_dl(db, inst, 1); if (!dl) { FAIL("make_test_dl"); sqlite3_close(db); u_free(inst); return; } add_holder(dl, 0, 0x1111); dl->blocks[0].holder_idx = 0; dl->blocks[0].state = MD_BLK_RECV; dl->blocks[0].attempts = 2; /* уже 2 провала */ dl->inflight = 1; int rc = md_dl_failover_block(dl, 0, 1000000000ULL); int ok = rc == -1 && dl->blocks[0].state == MD_BLK_FAILED && dl->blocks[0].attempts == 3; if (ok) OK(); else FAIL("rc=%d state=%d attempts=%d", rc, dl->blocks[0].state, dl->blocks[0].attempts); free_test_dl(inst, dl); sqlite3_close(db); u_free(inst); } } /* ── 6. md_dl_check_stall: только зависшие блоки ── */ static void test_check_stall(void) { TEST("check_stall fails over only stalled block"); { sqlite3* db = NULL; sqlite3_open(":memory:", &db); struct UTUN_INSTANCE* inst = make_minimal_instance(db); struct media_download* dl = make_test_dl(db, inst, 2); if (!dl) { FAIL("make_test_dl"); sqlite3_close(db); u_free(inst); return; } for (int bi = 0; bi < 2; bi++) { add_holder(dl, bi, 0x1111); add_holder(dl, bi, 0x2222); dl->blocks[bi].holder_idx = 0; dl->blocks[bi].state = MD_BLK_RECV; } dl->inflight = 2; uint64_t now = 1000000000ULL; dl->blocks[0].last_progress_tb = now - 30000; /* завис */ dl->blocks[1].last_progress_tb = now - 100; /* живой */ int rc = md_dl_check_stall(dl, now); int ok = rc == 0 && dl->blocks[0].attempts == 1 && dl->blocks[1].attempts == 0; if (ok) OK(); else FAIL("rc=%d b0.attempts=%d b1.attempts=%d", rc, dl->blocks[0].attempts, dl->blocks[1].attempts); free_test_dl(inst, dl); sqlite3_close(db); u_free(inst); } } /* ── 7. OVERLOADED → следующий держатель ── */ static void test_overloaded_next_holder(void) { TEST("OVERLOADED → next holder (no attempt)"); { sqlite3* db = NULL; sqlite3_open(":memory:", &db); struct UTUN_INSTANCE* inst = make_minimal_instance(db); struct media_download* dl = make_test_dl(db, inst, 1); if (!dl) { FAIL("make_test_dl"); sqlite3_close(db); u_free(inst); return; } add_holder(dl, 0, 0x1111); add_holder(dl, 0, 0x2222); dl->blocks[0].holder_idx = 0; dl->blocks[0].state = MD_BLK_RECV; dl->inflight = 1; struct media_pkt_block_overloaded ov; memset(&ov, 0, sizeof(ov)); ov.subcmd = MEDIA_SUBCMD_BLOCK_OVERLOADED; memcpy(ov.media_id, dl->media_id, 16); memcpy(ov.block_id, dl->blocks[0].block_id, 16); ov.retry_after_ms = 2000; media_download_handle_overloaded(inst, (const uint8_t*)&ov, sizeof(ov)); int ok = dl->blocks[0].attempts == 0 && dl->blocks[0].holder_idx == 1 && dl->blocks[0].state == MD_BLK_REQ; if (ok) OK(); else FAIL("attempts=%d hidx=%d state=%d", dl->blocks[0].attempts, dl->blocks[0].holder_idx, dl->blocks[0].state); free_test_dl(inst, dl); sqlite3_close(db); u_free(inst); } } /* ── 8. OVERLOADED без кандидатов → WAIT ── */ static void test_overloaded_wait(void) { TEST("OVERLOADED no more holders → WAIT"); { sqlite3* db = NULL; sqlite3_open(":memory:", &db); struct UTUN_INSTANCE* inst = make_minimal_instance(db); struct media_download* dl = make_test_dl(db, inst, 1); if (!dl) { FAIL("make_test_dl"); sqlite3_close(db); u_free(inst); return; } add_holder(dl, 0, 0x1111); /* один держатель */ dl->blocks[0].holder_idx = 0; dl->blocks[0].state = MD_BLK_RECV; dl->inflight = 1; struct media_pkt_block_overloaded ov; memset(&ov, 0, sizeof(ov)); ov.subcmd = MEDIA_SUBCMD_BLOCK_OVERLOADED; memcpy(ov.media_id, dl->media_id, 16); memcpy(ov.block_id, dl->blocks[0].block_id, 16); media_download_handle_overloaded(inst, (const uint8_t*)&ov, sizeof(ov)); int ok = dl->blocks[0].state == MD_BLK_WAIT && dl->blocks[0].attempts == 0; if (ok) OK(); else FAIL("state=%d attempts=%d", dl->blocks[0].state, dl->blocks[0].attempts); free_test_dl(inst, dl); sqlite3_close(db); u_free(inst); } } /* ── 9. RELAY_FULL → добавление держателей ── */ static void test_relay_full(void) { TEST("RELAY_FULL → add holders + retry"); { sqlite3* db = NULL; sqlite3_open(":memory:", &db); struct UTUN_INSTANCE* inst = make_minimal_instance(db); struct media_download* dl = make_test_dl(db, inst, 1); if (!dl) { FAIL("make_test_dl"); sqlite3_close(db); u_free(inst); return; } add_holder(dl, 0, 0x1111); /* 1 держатель */ dl->blocks[0].holder_idx = 0; dl->blocks[0].state = MD_BLK_RECV; dl->inflight = 1; uint64_t nodes[2] = { 0x3333, 0x4444 }; media_download_handle_relay_full(inst, dl->media_id, dl->blocks[0].block_id, 0, nodes, 2); int ok = dl->blocks[0].num_holders == 3 && dl->blocks[0].state == MD_BLK_REQ; if (ok) OK(); else FAIL("holders=%d state=%d", dl->blocks[0].num_holders, dl->blocks[0].state); free_test_dl(inst, dl); sqlite3_close(db); u_free(inst); } } /* ── 10. пустой QUERY_RESP ── */ static void test_query_resp_empty(void) { TEST("QUERY_RESP empty → no crash"); { sqlite3* db = NULL; sqlite3_open(":memory:", &db); struct UTUN_INSTANCE* inst = make_minimal_instance(db); struct media_download* dl = make_test_dl(db, inst, 1); if (!dl) { FAIL("make_test_dl"); sqlite3_close(db); u_free(inst); return; } uint8_t pkt[MEDIA_QUERY_RESP_HDR_SIZE]; memset(pkt, 0, sizeof(pkt)); pkt[0] = MEDIA_SUBCMD_QUERY_RESP; /* num_entries = 0 */ media_download_handle_query_resp(inst, pkt, sizeof(pkt)); OK(); free_test_dl(inst, dl); sqlite3_close(db); u_free(inst); } } /* ── 11. назначение держателей + разброс ── */ static void test_query_resp_assign(void) { TEST("QUERY_RESP assigns holders (spread)"); { sqlite3* db = NULL; sqlite3_open(":memory:", &db); struct UTUN_INSTANCE* inst = make_minimal_instance(db); struct media_download* dl = make_test_dl(db, inst, 3); if (!dl) { FAIL("make_test_dl"); sqlite3_close(db); u_free(inst); return; } /* 2 держателя на каждый блок */ uint64_t n1 = 0x1111, n2 = 0x2222; struct media_pkt_query_resp_entry entries[6]; memset(entries, 0, sizeof(entries)); for (int i = 0; i < 3; i++) { entries[i * 2 + 0].node_id = n1; memcpy(entries[i * 2 + 0].block_id, dl->blocks[i].block_id, 16); entries[i * 2 + 1].node_id = n2; memcpy(entries[i * 2 + 1].block_id, dl->blocks[i].block_id, 16); } uint8_t pkt[3 + sizeof(entries)]; pkt[0] = MEDIA_SUBCMD_QUERY_RESP; uint16_t ne = 6; memcpy(pkt + 1, &ne, 2); memcpy(pkt + 3, entries, sizeof(entries)); media_download_handle_query_resp(inst, pkt, sizeof(pkt)); /* блоки разложены: holder_idx = i % num_holders (стартовый разброс) */ int ok = 1; for (int i = 0; i < 3; i++) if (dl->blocks[i].num_holders != 2) ok = 0; if (dl->num_peers != 2) ok = 0; if (ok) OK(); else FAIL("h0=%d h1=%d h2=%d peers=%d", dl->blocks[0].num_holders, dl->blocks[1].num_holders, dl->blocks[2].num_holders, dl->num_peers); free_test_dl(inst, dl); sqlite3_close(db); u_free(inst); } } /* ── 12. дедуп ── */ static void dedup_done_cb(void* arg, int err) { (void)arg; (void)err; } static void test_dedup(void) { TEST("duplicate start ignored"); { sqlite3* db = NULL; sqlite3_open(":memory:", &db); struct UTUN_INSTANCE* inst = make_minimal_instance(db); struct media_download* dl = make_test_dl(db, inst, 1); if (!dl) { FAIL("make_test_dl"); sqlite3_close(db); u_free(inst); return; } struct media_index_result r; memset(&r, 0, sizeof(r)); memcpy(r.media_id, dl->media_id, 16); r.num_blocks = 1; r.file_size = CHUNK_SIZE; r.block_size = CHUNK_SIZE; int rc = media_download_start(inst, 0x1234, &r, "/tmp/x.bin", "/tmp", 0xAAA, dedup_done_cb, NULL, NULL, NULL); /* rc == 0 (не ошибка), новая запись не создана (дедуп) */ int count = 0; if (inst->md.downloads) { struct ll_entry* e = inst->md.downloads->head; while (e) { count++; e = e->next; } } if (rc == 0 && count == 1) OK(); else FAIL("rc=%d count=%d", rc, count); free_test_dl(inst, dl); sqlite3_close(db); u_free(inst); } } int main(void) { test_setup(); printf("=== test_media_delivery_download ===\n"); test_chunk_offset_write(); test_done_assembly(); test_done_bad_sig(); test_failover_stall(); test_attempts_exhausted(); test_check_stall(); test_overloaded_next_holder(); test_overloaded_wait(); test_relay_full(); test_query_resp_empty(); test_query_resp_assign(); test_dedup(); printf("\n%d/%d passed, %d failed\n", g_passed, g_total, g_failed); test_cleanup(); return g_failed > 0 ? 1 : 0; }