Files
utun2/tests/test_media_delivery_download.c
T

501 lines
22 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 <sqlite3.h>
#include <openssl/evp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#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;
}