|
|
|
|
@ -1,13 +1,16 @@
|
|
|
|
|
// test_media_delivery_download.c — тест скачивания блоков (chunk → done → assembly)
|
|
|
|
|
// test_media_delivery_download.c — юнит-тесты скачивания блоков (блочный конечный автомат)
|
|
|
|
|
//
|
|
|
|
|
// Покрытие:
|
|
|
|
|
// 1. media_download_start — регистрация в очереди, сбор суперузлов
|
|
|
|
|
// 2. media_download_handle_chunk — запись чанка в .chunk_N
|
|
|
|
|
// 3. media_download_handle_chunk — неизвестный media_id → игнор
|
|
|
|
|
// 4. media_download_handle_done — проверка подписи, отметка validated
|
|
|
|
|
// 5. media_download_handle_done — неверная подпись → сброс started/received
|
|
|
|
|
// 6. media_download_handle_done — все блоки получены → сборка файла
|
|
|
|
|
// 7. media_download_cancel — отправка CANCEL, done_cb с err=-2
|
|
|
|
|
// - запись чанков по offset (идемпотентность при дублях)
|
|
|
|
|
// - BLOCK_DONE: сигнатура валидна → сборка; неверная → фейловер
|
|
|
|
|
// - фейловер блока по простою (фейковое время)
|
|
|
|
|
// - исчерпание попыток → FAILED
|
|
|
|
|
// - проверка простоя md_dl_check_stall (только зависшие блоки)
|
|
|
|
|
// - OVERLOADED → следующий держатель / WAIT
|
|
|
|
|
// - RELAY_FULL → добавление держателей
|
|
|
|
|
// - пустой QUERY_RESP
|
|
|
|
|
// - назначение держателей + разброс по узлам
|
|
|
|
|
// - дедуп повторного media_download_start
|
|
|
|
|
|
|
|
|
|
#include "media_delivery.h" |
|
|
|
|
#include "media_delivery_proto.h" |
|
|
|
|
@ -36,7 +39,7 @@ 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(" %-55s", name); } while(0) |
|
|
|
|
#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) |
|
|
|
|
|
|
|
|
|
@ -57,14 +60,9 @@ static int file_size(const char* path) {
|
|
|
|
|
|
|
|
|
|
static void test_setup(void) { |
|
|
|
|
snprintf(g_temp_dir, sizeof(g_temp_dir), "%s", TEMP_DIR); |
|
|
|
|
#ifdef _WIN32 |
|
|
|
|
{ char tmp_path[512]; GetTempPathA(sizeof(tmp_path), tmp_path); |
|
|
|
|
snprintf(g_temp_dir, sizeof(g_temp_dir), "%s\\utun_test_%08x", tmp_path, (unsigned)rand()); |
|
|
|
|
_mkdir(g_temp_dir); } |
|
|
|
|
#else |
|
|
|
|
if (!mkdtemp(g_temp_dir)) { fprintf(stderr, "mkdtemp failed\n"); exit(1); } |
|
|
|
|
#endif |
|
|
|
|
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(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -73,7 +71,7 @@ static void test_cleanup(void) {
|
|
|
|
|
if (g_ua) { uasync_destroy(g_ua, 0); g_ua = NULL; } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* ── create minimal UTUN_INSTANCE with media_delivery ── */ |
|
|
|
|
/* ── минимальный UTUN_INSTANCE с media_delivery ── */ |
|
|
|
|
|
|
|
|
|
#include "../src/utun_instance.h" |
|
|
|
|
|
|
|
|
|
@ -83,413 +81,398 @@ static struct UTUN_INSTANCE* make_minimal_instance(sqlite3* db) {
|
|
|
|
|
inst->ua = g_ua; |
|
|
|
|
inst->node_id = 0xDEADBEEFDEADBEEFULL; |
|
|
|
|
inst->topo_sqlite_db = db; |
|
|
|
|
|
|
|
|
|
/* generate Ed25519 key pair — just random bytes for test */ |
|
|
|
|
for (int i = 0; i < 32; i++) inst->my_ed25519_privkey[i] = (uint8_t)(rand() & 0xFF); |
|
|
|
|
memset(inst->my_ed25519_pubkey, 0xDD, 32); |
|
|
|
|
|
|
|
|
|
/* init media_delivery */ |
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* ── test cases ── */ |
|
|
|
|
|
|
|
|
|
static void test_download_chunk(void) { |
|
|
|
|
TEST("download handle_chunk writes to temp file"); { |
|
|
|
|
sqlite3* db = NULL; sqlite3_open(":memory:", &db); |
|
|
|
|
struct UTUN_INSTANCE* inst = make_minimal_instance(db); |
|
|
|
|
|
|
|
|
|
inst->md.downloads = queue_new(inst->ua, 256, offsetof(struct media_download, media_id), 16, "md_dl_test"); |
|
|
|
|
struct media_download dl_buf; memset(&dl_buf, 0, sizeof(dl_buf)); |
|
|
|
|
make_uuid(dl_buf.media_id); |
|
|
|
|
dl_buf.num_blocks = NUM_BLOCKS; |
|
|
|
|
snprintf(dl_buf.dest_path, sizeof(dl_buf.dest_path), "%s/test_output.bin", g_temp_dir); |
|
|
|
|
dl_buf.block_ids = u_malloc(NUM_BLOCKS * 16); |
|
|
|
|
dl_buf.block_sigs = u_malloc(NUM_BLOCKS * 64); |
|
|
|
|
for (int i = 0; i < NUM_BLOCKS; i++) make_uuid(dl_buf.block_ids + i * 16); |
|
|
|
|
dl_buf.active = 1; |
|
|
|
|
memcpy(dl_buf.ll.data, dl_buf.media_id, 16); |
|
|
|
|
|
|
|
|
|
struct ll_entry* qe = queue_entry_new(sizeof(struct media_download)); |
|
|
|
|
if (qe) { memcpy(qe->data, &dl_buf, sizeof(dl_buf)); queue_data_put_with_index(inst->md.downloads, qe); } |
|
|
|
|
|
|
|
|
|
uint8_t chunk_data[256]; |
|
|
|
|
for (int i = 0; i < 256; i++) chunk_data[i] = (uint8_t)(i & 0xFF); |
|
|
|
|
|
|
|
|
|
uint8_t pkt[MEDIA_BLOCK_CHUNK_HDR_SIZE + sizeof(chunk_data)]; |
|
|
|
|
struct media_pkt_block_chunk* ch = (struct media_pkt_block_chunk*)pkt; |
|
|
|
|
ch->subcmd = MEDIA_SUBCMD_BLOCK_CHUNK; |
|
|
|
|
memcpy(ch->media_id, dl_buf.media_id, 16); |
|
|
|
|
memcpy(ch->block_id, dl_buf.block_ids, 16); |
|
|
|
|
ch->chunk = 0; ch->offset = 0; ch->data_len = (uint16_t)sizeof(chunk_data); |
|
|
|
|
memcpy(pkt + MEDIA_BLOCK_CHUNK_HDR_SIZE, chunk_data, sizeof(chunk_data)); |
|
|
|
|
/* создаёт загрузку и регистрирует в очереди; возвращает указатель на данные в очереди */ |
|
|
|
|
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"); |
|
|
|
|
|
|
|
|
|
media_download_handle_chunk(inst, pkt, sizeof(pkt)); |
|
|
|
|
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); |
|
|
|
|
|
|
|
|
|
char tmp[1024]; snprintf(tmp, sizeof(tmp), "%s.chunk_0", dl_buf.dest_path); |
|
|
|
|
int ok1 = file_exists(tmp) && file_size(tmp) == (int)sizeof(chunk_data); |
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* send second chunk with same block_id → appends */ |
|
|
|
|
ch->offset = (uint32_t)sizeof(chunk_data); |
|
|
|
|
media_download_handle_chunk(inst, pkt, sizeof(pkt)); |
|
|
|
|
int ok2 = file_size(tmp) == 2 * (int)sizeof(chunk_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); } |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (ok1 && ok2) OK(); else FAIL("chunk: ok1=%d ok2=%d size=%d", ok1, ok2, file_size(tmp)); |
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
u_free(dl_buf.block_ids); u_free(dl_buf.block_sigs); |
|
|
|
|
if (inst->md.downloads) queue_free(inst->md.downloads); |
|
|
|
|
u_free(inst); sqlite3_close(db); |
|
|
|
|
} |
|
|
|
|
/* ── 1. запись чанков по offset ── */ |
|
|
|
|
|
|
|
|
|
TEST("download handle_chunk unknown media_id"); { |
|
|
|
|
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); |
|
|
|
|
inst->md.downloads = queue_new(inst->ua, 256, offsetof(struct media_download, media_id), 16, "md_dl_test2"); |
|
|
|
|
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_BLOCK_CHUNK_HDR_SIZE]; |
|
|
|
|
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; |
|
|
|
|
make_uuid(ch->media_id); make_uuid(ch->block_id); |
|
|
|
|
ch->chunk = 0; ch->offset = 0; ch->data_len = 0; |
|
|
|
|
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)); |
|
|
|
|
OK(); /* should not crash or create files */ |
|
|
|
|
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); |
|
|
|
|
|
|
|
|
|
if (inst->md.downloads) queue_free(inst->md.downloads); |
|
|
|
|
u_free(inst); sqlite3_close(db); |
|
|
|
|
free_test_dl(inst, dl); |
|
|
|
|
sqlite3_close(db); u_free(inst); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static void test_download_done(void) { |
|
|
|
|
TEST("download handle_done sig ok + assembly"); { |
|
|
|
|
/* ── 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); |
|
|
|
|
inst->md.downloads = queue_new(inst->ua, 256, offsetof(struct media_download, media_id), 16, "md_dl_test3"); |
|
|
|
|
|
|
|
|
|
struct media_download dl; memset(&dl, 0, sizeof(dl)); |
|
|
|
|
make_uuid(dl.media_id); |
|
|
|
|
dl.num_blocks = 2; |
|
|
|
|
snprintf(dl.dest_path, sizeof(dl.dest_path), "%s/out.bin", g_temp_dir); |
|
|
|
|
dl.block_ids = u_malloc(16 * 2); dl.block_sigs = u_malloc(64 * 2); |
|
|
|
|
make_uuid(dl.block_ids); make_uuid(dl.block_ids + 16); |
|
|
|
|
memset(dl.block_sigs, 0x42, 64); memset(dl.block_sigs + 64, 0x43, 64); |
|
|
|
|
dl.block_size = CHUNK_SIZE; dl.file_size = CHUNK_SIZE * 2; |
|
|
|
|
dl.active = 1; |
|
|
|
|
memcpy(dl.ll.data, dl.media_id, 16); |
|
|
|
|
|
|
|
|
|
struct ll_entry* qe = queue_entry_new(sizeof(struct media_download)); |
|
|
|
|
if (!qe) { FAIL("queue_entry_new"); goto done3; } |
|
|
|
|
memcpy(qe->data, &dl, sizeof(dl)); queue_data_put_with_index(inst->md.downloads, qe); |
|
|
|
|
|
|
|
|
|
/* write chunk files */ |
|
|
|
|
uint8_t data0[CHUNK_SIZE]; memset(data0, 0xA0, CHUNK_SIZE); |
|
|
|
|
uint8_t data1[CHUNK_SIZE]; memset(data1, 0xB1, CHUNK_SIZE); |
|
|
|
|
{ |
|
|
|
|
char c0[1024]; snprintf(c0, sizeof(c0), "%s.chunk_0", dl.dest_path); |
|
|
|
|
char c1[1024]; snprintf(c1, sizeof(c1), "%s.chunk_1", dl.dest_path); |
|
|
|
|
write_file(c0, data0, CHUNK_SIZE); write_file(c1, data1, CHUNK_SIZE); |
|
|
|
|
} |
|
|
|
|
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); |
|
|
|
|
|
|
|
|
|
/* BLOCK_DONE for block 0 */ |
|
|
|
|
{ |
|
|
|
|
uint8_t pkt[MEDIA_BLOCK_DONE_SIZE]; |
|
|
|
|
struct media_pkt_block_done* bd = (struct media_pkt_block_done*)pkt; |
|
|
|
|
memset(pkt, 0, sizeof(pkt)); |
|
|
|
|
bd->subcmd = MEDIA_SUBCMD_BLOCK_DONE; |
|
|
|
|
memcpy(bd->media_id, dl.media_id, 16); |
|
|
|
|
memcpy(bd->block_id, dl.block_ids, 16); |
|
|
|
|
bd->chunk = 0; bd->total_size = CHUNK_SIZE; |
|
|
|
|
memcpy(bd->block_sig, dl.block_sigs, 64); /* match expected */ |
|
|
|
|
media_download_handle_done(inst, pkt, sizeof(pkt)); |
|
|
|
|
} |
|
|
|
|
/* BLOCK_DONE for block 1 */ |
|
|
|
|
{ |
|
|
|
|
uint8_t pkt[MEDIA_BLOCK_DONE_SIZE]; |
|
|
|
|
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; |
|
|
|
|
memset(pkt, 0, sizeof(pkt)); |
|
|
|
|
bd->subcmd = MEDIA_SUBCMD_BLOCK_DONE; |
|
|
|
|
memcpy(bd->media_id, dl.media_id, 16); |
|
|
|
|
memcpy(bd->block_id, dl.block_ids + 16, 16); |
|
|
|
|
bd->chunk = 1; bd->total_size = CHUNK_SIZE; |
|
|
|
|
memcpy(bd->block_sig, dl.block_sigs + 64, 64); |
|
|
|
|
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)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* verify assembly */ |
|
|
|
|
if (file_exists(dl.dest_path)) { |
|
|
|
|
int sz = file_size(dl.dest_path); |
|
|
|
|
if (sz == CHUNK_SIZE * 2) OK(); else FAIL("assembled size %d != %d", sz, CHUNK_SIZE * 2); |
|
|
|
|
} else { FAIL("assembled file not created"); } |
|
|
|
|
|
|
|
|
|
done3: |
|
|
|
|
u_free(dl.block_ids); u_free(dl.block_sigs); |
|
|
|
|
if (inst->md.downloads) queue_free(inst->md.downloads); |
|
|
|
|
u_free(inst); sqlite3_close(db); |
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
TEST("download handle_done bad sig → retry"); { |
|
|
|
|
/* ── 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); |
|
|
|
|
inst->md.downloads = queue_new(inst->ua, 256, offsetof(struct media_download, media_id), 16, "md_dl_test4"); |
|
|
|
|
|
|
|
|
|
struct media_download dl; memset(&dl, 0, sizeof(dl)); |
|
|
|
|
make_uuid(dl.media_id); |
|
|
|
|
dl.num_blocks = 1; |
|
|
|
|
snprintf(dl.dest_path, sizeof(dl.dest_path), "%s/bad_sig.bin", g_temp_dir); |
|
|
|
|
dl.block_ids = u_malloc(16); dl.block_sigs = u_malloc(64); |
|
|
|
|
make_uuid(dl.block_ids); |
|
|
|
|
memset(dl.block_sigs, 0x55, 64); /* expected sig */ |
|
|
|
|
dl.block_size = CHUNK_SIZE; dl.file_size = CHUNK_SIZE; |
|
|
|
|
dl.active = 1; |
|
|
|
|
memcpy(dl.ll.data, dl.media_id, 16); |
|
|
|
|
|
|
|
|
|
struct ll_entry* qe = queue_entry_new(sizeof(struct media_download)); |
|
|
|
|
if (!qe) { FAIL("queue_entry_new"); goto done4; } |
|
|
|
|
memcpy(qe->data, &dl, sizeof(dl)); queue_data_put_with_index(inst->md.downloads, qe); |
|
|
|
|
|
|
|
|
|
/* write chunk file */ |
|
|
|
|
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 c0[1024]; snprintf(c0, sizeof(c0), "%s.chunk_0", dl.dest_path); |
|
|
|
|
write_file(c0, data, CHUNK_SIZE); |
|
|
|
|
{ char c[1024]; snprintf(c, sizeof(c), "%s.chunk_0", dl->dest_path); write_file(c, data, CHUNK_SIZE); } |
|
|
|
|
|
|
|
|
|
/* send BLOCK_DONE with WRONG signature */ |
|
|
|
|
uint8_t pkt[MEDIA_BLOCK_DONE_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; |
|
|
|
|
memset(pkt, 0, sizeof(pkt)); |
|
|
|
|
bd->subcmd = MEDIA_SUBCMD_BLOCK_DONE; |
|
|
|
|
memcpy(bd->media_id, dl.media_id, 16); |
|
|
|
|
memcpy(bd->block_id, dl.block_ids, 16); |
|
|
|
|
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)); |
|
|
|
|
|
|
|
|
|
/* file should NOT be assembled (sig mismatch) */ |
|
|
|
|
if (!file_exists(dl.dest_path)) OK(); else FAIL("assembled despite bad sig"); |
|
|
|
|
/* файл не собран; блок в 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); |
|
|
|
|
|
|
|
|
|
done4: |
|
|
|
|
u_free(dl.block_ids); u_free(dl.block_sigs); |
|
|
|
|
if (inst->md.downloads) queue_free(inst->md.downloads); |
|
|
|
|
u_free(inst); sqlite3_close(db); |
|
|
|
|
free_test_dl(inst, dl); |
|
|
|
|
sqlite3_close(db); u_free(inst); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static void test_download_cancel(void) { |
|
|
|
|
TEST("download cancel marks inactive and calls done_cb"); { |
|
|
|
|
/* ── 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); |
|
|
|
|
inst->md.downloads = queue_new(inst->ua, 256, offsetof(struct media_download, media_id), 16, "md_dl_test5"); |
|
|
|
|
|
|
|
|
|
struct media_download dl; memset(&dl, 0, sizeof(dl)); |
|
|
|
|
make_uuid(dl.media_id); |
|
|
|
|
dl.num_blocks = 1; |
|
|
|
|
snprintf(dl.dest_path, sizeof(dl.dest_path), "%s/cancel.bin", g_temp_dir); |
|
|
|
|
dl.block_ids = u_malloc(16); make_uuid(dl.block_ids); |
|
|
|
|
dl.block_sigs = u_malloc(64); memset(dl.block_sigs, 0, 64); |
|
|
|
|
dl.active = 1; |
|
|
|
|
memcpy(dl.ll.data, dl.media_id, 16); |
|
|
|
|
|
|
|
|
|
struct ll_entry* qe = queue_entry_new(sizeof(struct media_download)); |
|
|
|
|
if (!qe) { FAIL("queue_entry_new"); goto done5; } |
|
|
|
|
memcpy(qe->data, &dl, sizeof(dl)); queue_data_put_with_index(inst->md.downloads, qe); |
|
|
|
|
|
|
|
|
|
int rc = media_download_cancel(inst, dl.media_id, NULL, 0); |
|
|
|
|
if (rc == 0) OK(); else FAIL("cancel returned %d", rc); |
|
|
|
|
|
|
|
|
|
done5: |
|
|
|
|
u_free(dl.block_ids); u_free(dl.block_sigs); |
|
|
|
|
if (inst->md.downloads) queue_free(inst->md.downloads); |
|
|
|
|
u_free(inst); sqlite3_close(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); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* ────────────────────────────────────────────────────────────────
|
|
|
|
|
Tests for block download fixes (backpressure, OOB block_id, conn_mgr close) |
|
|
|
|
──────────────────────────────────────────────────────────────── */ |
|
|
|
|
/* ── 5. исчерпание попыток → FAILED ── */ |
|
|
|
|
|
|
|
|
|
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"); |
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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->block_ids = u_calloc((size_t)num_blocks, 16); |
|
|
|
|
dl->block_sigs = u_calloc((size_t)num_blocks, 64); |
|
|
|
|
for (int i = 0; i < num_blocks; i++) { make_uuid(dl->block_ids + i * 16); memset(dl->block_sigs + i * 64, (uint8_t)(0x42 + i), 64); } |
|
|
|
|
dl->block_size = CHUNK_SIZE; dl->file_size = (int64_t)CHUNK_SIZE * num_blocks; |
|
|
|
|
dl->active = 1; dl->inst = inst; |
|
|
|
|
memcpy(dl->ll.data, dl->media_id, 16); |
|
|
|
|
/* ── 6. md_dl_check_stall: только зависшие блоки ── */ |
|
|
|
|
|
|
|
|
|
struct ll_entry* qe = queue_entry_new(sizeof(*dl)); |
|
|
|
|
if (!qe) { u_free(dl->block_ids); 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 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); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static struct media_download_peer* add_peer(struct media_download* dl, uint64_t node_id, int num_blocks) { |
|
|
|
|
if (dl->num_peers >= 10) return NULL; |
|
|
|
|
int pi = dl->num_peers++; |
|
|
|
|
struct media_download_peer* p = &dl->peers[pi]; |
|
|
|
|
p->node_id = node_id; |
|
|
|
|
p->num_blocks = num_blocks; |
|
|
|
|
for (int i = 0; i < num_blocks; i++) { make_uuid(p->blocks[i].block_id); } |
|
|
|
|
return p; |
|
|
|
|
} |
|
|
|
|
/* ── 7. OVERLOADED → следующий держатель ── */ |
|
|
|
|
|
|
|
|
|
/* ── Тест: conn_cb отправляет только первый блок (backpressure) ── */ |
|
|
|
|
static void test_conn_cb_backpressure(void) { |
|
|
|
|
TEST("conn_cb starts only first block (backpressure)"); { |
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
add_peer(dl, 0x1234, 5); /* 5 blocks on peer */ |
|
|
|
|
/* ── 8. OVERLOADED без кандидатов → WAIT ── */ |
|
|
|
|
|
|
|
|
|
md_dl_conn_cb(NULL, 0x1234, 0, CONN_EVENT_UP, dl); |
|
|
|
|
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; |
|
|
|
|
|
|
|
|
|
int started_count = 0; |
|
|
|
|
for (int i = 0; i < dl->peers[0].num_blocks; i++) |
|
|
|
|
if (dl->peers[0].blocks[i].started) started_count++; |
|
|
|
|
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)); |
|
|
|
|
|
|
|
|
|
if (dl->peers[0].connected && started_count == 1 && dl->peers[0].blocks[0].started && !dl->peers[0].blocks[1].started) |
|
|
|
|
OK(); else FAIL("connected=%d started=%d b0=%d b1=%d", dl->peers[0].connected, started_count, dl->peers[0].blocks[0].started, dl->peers[0].blocks[1].started); |
|
|
|
|
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); |
|
|
|
|
|
|
|
|
|
u_free(dl->block_ids); u_free(dl->block_sigs); |
|
|
|
|
if (inst->md.downloads) queue_free(inst->md.downloads); |
|
|
|
|
u_free(inst); sqlite3_close(db); |
|
|
|
|
free_test_dl(inst, dl); |
|
|
|
|
sqlite3_close(db); u_free(inst); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* ── Тест: conn_cb для разных пиров ── */ |
|
|
|
|
static void test_conn_cb_multi_peer(void) { |
|
|
|
|
TEST("conn_cb starts first block per peer"); { |
|
|
|
|
/* ── 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; |
|
|
|
|
|
|
|
|
|
add_peer(dl, 0x1000, 3); |
|
|
|
|
add_peer(dl, 0x2000, 2); |
|
|
|
|
uint64_t nodes[2] = { 0x3333, 0x4444 }; |
|
|
|
|
media_download_handle_relay_full(inst, dl->media_id, dl->blocks[0].block_id, 0, nodes, 2); |
|
|
|
|
|
|
|
|
|
md_dl_conn_cb(NULL, 0x1000, 0, CONN_EVENT_UP, dl); |
|
|
|
|
md_dl_conn_cb(NULL, 0x2000, 0, CONN_EVENT_UP, dl); |
|
|
|
|
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); |
|
|
|
|
|
|
|
|
|
int ok = dl->peers[0].connected && dl->peers[1].connected |
|
|
|
|
&& dl->peers[0].blocks[0].started && !dl->peers[0].blocks[1].started |
|
|
|
|
&& dl->peers[1].blocks[0].started && !dl->peers[1].blocks[1].started; |
|
|
|
|
if (ok) OK(); else FAIL("p0:conn=%d b0=%d b1=%d p1:conn=%d b0=%d b1=%d", |
|
|
|
|
dl->peers[0].connected, dl->peers[0].blocks[0].started, dl->peers[0].blocks[1].started, |
|
|
|
|
dl->peers[1].connected, dl->peers[1].blocks[0].started, dl->peers[1].blocks[1].started); |
|
|
|
|
|
|
|
|
|
u_free(dl->block_ids); u_free(dl->block_sigs); |
|
|
|
|
if (inst->md.downloads) queue_free(inst->md.downloads); |
|
|
|
|
u_free(inst); sqlite3_close(db); |
|
|
|
|
free_test_dl(inst, dl); |
|
|
|
|
sqlite3_close(db); u_free(inst); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* ── Тест: BLOCK_DONE → relay: следующий блок с того же пира ── */ |
|
|
|
|
static void test_done_block_relay(void) { |
|
|
|
|
TEST("BLOCK_DONE triggers next block from same peer"); { |
|
|
|
|
/* ── 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, 2); |
|
|
|
|
struct media_download* dl = make_test_dl(db, inst, 1); |
|
|
|
|
if (!dl) { FAIL("make_test_dl"); sqlite3_close(db); u_free(inst); return; } |
|
|
|
|
|
|
|
|
|
/* peer with 3 blocks for 2-block download */ |
|
|
|
|
add_peer(dl, 0x5555, 3); |
|
|
|
|
/* map dl->block_ids to peer blocks for matching in handle_done */ |
|
|
|
|
for (int i = 0; i < dl->num_blocks; i++) |
|
|
|
|
memcpy(dl->peers[0].blocks[i].block_id, dl->block_ids + i * 16, 16); |
|
|
|
|
/* pre-set: block 0 started and fake-received via conn_cb */ |
|
|
|
|
dl->peers[0].blocks[0].started = 1; |
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
|
/* write chunk for block 0 */ |
|
|
|
|
uint8_t data0[CHUNK_SIZE]; memset(data0, 0xBB, CHUNK_SIZE); |
|
|
|
|
{ char c[1024]; snprintf(c, sizeof(c), "%s.chunk_0", dl->dest_path); write_file(c, data0, CHUNK_SIZE); } |
|
|
|
|
|
|
|
|
|
/* BLOCK_DONE for block 0 with matching sig */ |
|
|
|
|
{ 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->block_ids, 16); |
|
|
|
|
bd->chunk = 0; bd->total_size = CHUNK_SIZE; |
|
|
|
|
memcpy(bd->block_sig, dl->block_sigs, 64); |
|
|
|
|
media_download_handle_done(inst, pkt, sizeof(pkt)); } |
|
|
|
|
|
|
|
|
|
/* block 1 should be started (relay); block 2 NOT (peer-local index unrelated to dl) */ |
|
|
|
|
int ok = dl->peers[0].blocks[1].started && !dl->peers[0].blocks[2].started; |
|
|
|
|
if (ok) OK(); else FAIL("b1_started=%d b2_started=%d", dl->peers[0].blocks[1].started, dl->peers[0].blocks[2].started); |
|
|
|
|
|
|
|
|
|
u_free(dl->block_ids); u_free(dl->block_sigs); |
|
|
|
|
if (inst->md.downloads) queue_free(inst->md.downloads); |
|
|
|
|
u_free(inst); sqlite3_close(db); |
|
|
|
|
free_test_dl(inst, dl); |
|
|
|
|
sqlite3_close(db); u_free(inst); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* ── Тест: ассемблинг при всех блоках ── */ |
|
|
|
|
static void test_done_assembly(void) { |
|
|
|
|
TEST("BLOCK_DONE all blocks → assembly"); { |
|
|
|
|
/* ── 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, 2); |
|
|
|
|
struct media_download* dl = make_test_dl(db, inst, 3); |
|
|
|
|
if (!dl) { FAIL("make_test_dl"); sqlite3_close(db); u_free(inst); return; } |
|
|
|
|
|
|
|
|
|
/* write two chunk files with different content */ |
|
|
|
|
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); } |
|
|
|
|
|
|
|
|
|
/* BLOCK_DONE for both */ |
|
|
|
|
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->block_ids + bi * 16, 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)); |
|
|
|
|
/* 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); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
int sz = file_size(dl->dest_path); |
|
|
|
|
int ok = dl->assembled && sz == CHUNK_SIZE * 2; |
|
|
|
|
if (ok) OK(); else FAIL("assembled=%d size=%d expected=%d", dl->assembled, sz, CHUNK_SIZE * 2); |
|
|
|
|
|
|
|
|
|
u_free(dl->block_ids); u_free(dl->block_sigs); |
|
|
|
|
if (inst->md.downloads) queue_free(inst->md.downloads); |
|
|
|
|
u_free(inst); sqlite3_close(db); |
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* ── Тест: пир имеет больше блоков чем dl → без OOB ── */ |
|
|
|
|
static void test_peer_more_blocks_than_dl(void) { |
|
|
|
|
TEST("conn_cb with peer blocks > dl blocks → no OOB"); { |
|
|
|
|
/* ── 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); /* 1 block */ |
|
|
|
|
struct media_download* dl = make_test_dl(db, inst, 1); |
|
|
|
|
if (!dl) { FAIL("make_test_dl"); sqlite3_close(db); u_free(inst); return; } |
|
|
|
|
|
|
|
|
|
add_peer(dl, 0x9999, 6); /* peer has 6 blocks — more than dl */ |
|
|
|
|
|
|
|
|
|
/* should NOT crash (was OOB before fix) */ |
|
|
|
|
md_dl_conn_cb(NULL, 0x9999, 0, CONN_EVENT_UP, dl); |
|
|
|
|
|
|
|
|
|
/* should have started only first peer block; no OOB access to dl->block_ids[1..5] */ |
|
|
|
|
int ok = dl->peers[0].connected && dl->peers[0].blocks[0].started |
|
|
|
|
&& !dl->peers[0].blocks[1].started && !dl->peers[0].blocks[5].started; |
|
|
|
|
if (ok) OK(); else FAIL("conn=%d b0=%d b1=%d b5=%d", dl->peers[0].connected, dl->peers[0].blocks[0].started, dl->peers[0].blocks[1].started, dl->peers[0].blocks[5].started); |
|
|
|
|
|
|
|
|
|
u_free(dl->block_ids); u_free(dl->block_sigs); |
|
|
|
|
if (inst->md.downloads) queue_free(inst->md.downloads); |
|
|
|
|
u_free(inst); sqlite3_close(db); |
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -497,16 +480,18 @@ int main(void) {
|
|
|
|
|
test_setup(); |
|
|
|
|
printf("=== test_media_delivery_download ===\n"); |
|
|
|
|
|
|
|
|
|
test_download_chunk(); |
|
|
|
|
test_download_done(); |
|
|
|
|
test_download_cancel(); |
|
|
|
|
|
|
|
|
|
printf("\n=== block download fixes (backpressure, OOB, relay) ===\n"); |
|
|
|
|
test_conn_cb_backpressure(); |
|
|
|
|
test_conn_cb_multi_peer(); |
|
|
|
|
test_done_block_relay(); |
|
|
|
|
test_chunk_offset_write(); |
|
|
|
|
test_done_assembly(); |
|
|
|
|
test_peer_more_blocks_than_dl(); |
|
|
|
|
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(); |
|
|
|
|
|