Exercise media index failure recovery and pending channel deletion

This commit is contained in:
evgeny committed 2026-10-22 14:52:27 +02:00
1 parent 9117813c4d
commit 87724433b1
2 files changed
+216

No files matched your search

+4
View File
@@ -874,6 +874,10 @@ test_media_index_SOURCES = test_media_index.c
test_media_index_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/media_delivery -I$(top_srcdir)/src/media_async -I$(top_srcdir)/lib
test_media_index_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
check_PROGRAMS += test_media_index_regressions
test_media_index_regressions_SOURCES = test_media_index_regressions.c
test_media_index_regressions_LDADD = $(LIBUTUN) $(COMMON_LIBS)
test_media_delivery_sql_SOURCES = test_media_delivery_sql.c
test_media_delivery_sql_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/media_delivery -I$(top_srcdir)/lib
test_media_delivery_sql_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
+212
View File
@@ -0,0 +1,212 @@
/* Пути, атомарность SQLite, повтор удаления и поздние completion исходящих медиа. */
#include "../lib/platform_compat.h"
#include "../lib/file_io.h"
#include "../lib/debug_config.h"
#include "../src/media_delivery/media_index.h"
#include "../src/media_async/media_async.h"
#include "../src/utun_instance.h"
#include "../src/chat/chat_core.h"
#include "../src/chat/chat_core_priv.h"
#include "../src/chat/chat_event.h"
#include "test_utils.h"
#include <assert.h>
#include <errno.h>
#include <limits.h>
static char root[256] = "/tmp/media_index_regressions_XXXXXX";
static uint8_t key[32] = {1}, ids[32] = {1};
static struct media_index_result result = { .media_id = {2}, .content_hash = {3},
.file_size = 14, .block_size = 7, .num_blocks = 2, .block_ids = ids };
static void sql(sqlite3* db, const char* query) { assert(sqlite3_exec(db, query, NULL, NULL, NULL) == SQLITE_OK); }
static int scalar(sqlite3* db, const char* query) {
sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(db, query, -1, &st, NULL) == SQLITE_OK);
assert(sqlite3_step(st) == SQLITE_ROW); int value = sqlite3_column_int(st, 0);
assert(sqlite3_finalize(st) == SQLITE_OK); return value;
}
static sqlite3* database(void) {
sqlite3* db = NULL; assert(sqlite3_open(":memory:", &db) == SQLITE_OK);
assert(!media_index_init(db)); return db;
}
static void write_file(const char* path) {
FILE* file = fopen(path, "wb"); assert(file && fwrite("source-payload", 1, 14, file) == 14 && !fclose(file));
}
static int commit(sqlite3* db, const char* channel, const char* path) {
return media_index_commit(db, &result, 1, key, channel, path, root);
}
static void paths(void) {
char relative[MEDIA_PATH_MAX], path[MEDIA_PATH_MAX], base[MEDIA_BASE_MAX];
assert(!media_index_location("/root///a/./file", "/root///", relative) && !strcmp(relative, "a/file"));
assert(!media_index_path("/", "a/file", path) && !strcmp(path, "/a/file"));
assert(!media_index_location("/a/file", "/", relative) && !strcmp(relative, "a/file"));
assert(!media_index_location("./a/file", ".", relative) && !strcmp(relative, "a/file"));
assert(media_index_location("/root2/file", "/root", relative) == -1);
assert(media_index_location("/root/../file", "/root", relative) == -1);
assert(media_index_location("/root", "/root", relative) == -1);
assert(media_index_path(root, "../file", path) == -1 && media_index_path(root, "/file", path) == -1);
assert(media_index_path(root, "a//file", path) == -1 && media_index_path(root, "a/./file", path) == -1);
assert(media_index_path(root, "C:/file", path) == -1 && media_index_path(root, "a\\file", path) == -1);
memset(relative, 'a', sizeof(relative)); relative[sizeof(relative) - 1] = 0;
assert(media_index_path("/", relative, path) == -1);
relative[sizeof(relative) - 2] = 0;
assert(!media_index_path("/", relative, path) && strlen(path) == MEDIA_PATH_MAX - 1);
sqlite3* db = database();
assert(!media_index_set_base(db, "/root///") && !media_index_set_base(db, "/root/./"));
assert(!media_index_get_base(db, base) && !strcmp(base, "/root"));
assert(media_index_set_base(db, "/other") == -1);
assert(!media_index_get_base(db, base) && !strcmp(base, "/root")); sqlite3_close(db);
#ifdef _WIN32
assert(!media_index_location("C:\\root\\a", "C:/root/", relative) && !strcmp(relative, "a"));
assert(!media_index_path("C:/", "a", path) && !strcmp(path, "C:/a"));
assert(!media_index_location("//server/share/a", "//server/share/", relative) && !strcmp(relative, "a"));
#endif
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media regression paths/base boundaries PASS");
}
struct denied { const char* index; int release; };
static int authorizer(void* arg, int action, const char* one, const char* two, const char* db, const char* trigger) {
(void)db; (void)trigger; struct denied* denied = arg;
if (action == SQLITE_CREATE_INDEX && denied->index && !strcmp(one, denied->index)) return SQLITE_DENY;
if (action == SQLITE_SAVEPOINT && denied->release && !strcmp(one, "RELEASE") && !strcmp(two, "media_registration")) {
denied->release = 0; return SQLITE_DENY;
}
return SQLITE_OK;
}
static void transactions(void) {
const char* indices[] = {"block_id", "media_id", "chat_id", "node_id", "content_hash"};
for (unsigned i = 0; i < sizeof(indices) / sizeof(*indices); i++) {
sqlite3* db = NULL; assert(sqlite3_open(":memory:", &db) == SQLITE_OK);
char name[80]; snprintf(name, sizeof(name), "idx_media_files_%s", indices[i]);
struct denied denied = {.index = name}; assert(!sqlite3_set_authorizer(db, authorizer, &denied));
assert(media_index_init(db) == -1);
assert(!sqlite3_set_authorizer(db, NULL, NULL) && !media_index_init(db)); sqlite3_close(db);
}
char path[MEDIA_PATH_MAX]; assert(!media_index_path(root, "atomic", path));
sqlite3* db = database();
sql(db, "CREATE TABLE marker(n); BEGIN; INSERT INTO marker VALUES(42)");
sql(db, "CREATE TRIGGER fail_second BEFORE INSERT ON media_files WHEN NEW.chunk=1 BEGIN SELECT RAISE(ABORT,'second'); END");
assert(commit(db, "1", path) == -1 && scalar(db, "SELECT count(*) FROM media_files") == 0);
assert(!sqlite3_get_autocommit(db) && scalar(db, "SELECT n FROM marker") == 42);
assert(media_index_register_downloaded_file(db, &result, 1, "1", path, root) == -1);
assert(!scalar(db, "SELECT count(*) FROM media_files"));
sql(db, "DROP TRIGGER fail_second");
assert(!commit(db, "1", path) && scalar(db, "SELECT count(*) FROM media_files") == 2);
assert(!sqlite3_get_autocommit(db)); sql(db, "ROLLBACK");
assert(!scalar(db, "SELECT count(*) FROM media_files"));
struct denied denied = {.release = 1}; assert(!sqlite3_set_authorizer(db, authorizer, &denied));
assert(commit(db, "1", path) == -1 && !scalar(db, "SELECT count(*) FROM media_files"));
assert(sqlite3_get_autocommit(db)); assert(!sqlite3_set_authorizer(db, NULL, NULL));
sql(db, "CREATE TRIGGER rollback_all BEFORE INSERT ON media_files BEGIN SELECT RAISE(ROLLBACK,'all'); END; BEGIN");
assert(commit(db, "1", path) == -1 && sqlite3_get_autocommit(db));
sql(db, "DROP TRIGGER rollback_all");
assert(!commit(db, "1", path) && !commit(db, "1", path));
assert(!media_index_register_downloaded_file(db, &result, 1, "1", path, root));
assert(scalar(db, "SELECT count(*) FROM media_files WHERE node_sign!=zeroblob(64)") == 2);
assert(media_index_register_downloaded(db, result.media_id, ids, result.content_hash, "conflict", "wrong", 1, 14, 7, 0, 0) == -1);
assert(scalar(db, "SELECT count(*) FROM media_files WHERE chat_id='1'") == 2);
result.num_blocks = INT_MAX;
assert(commit(db, "1", path) == -1); result.num_blocks = 2;
sqlite3_close(db);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media regression schema/atomicity/savepoint/duplicates PASS");
}
static void deletion(void) {
sqlite3* db = database(); char path[MEDIA_PATH_MAX]; assert(!media_index_path(root, "delete", path));
write_file(path); assert(!commit(db, "1", path));
sql(db, "BEGIN"); assert(media_index_delete_channel(db, "1", root) == -1 && ma_file_size(path) == 14); sql(db, "ROLLBACK");
sql(db, "CREATE TRIGGER fail_delete BEFORE DELETE ON media_files BEGIN SELECT RAISE(ABORT,'delete failure'); END");
assert(media_index_delete_channel(db, "1", root) == -1 && ma_file_size(path) == -1);
assert(scalar(db, "SELECT count(*) FROM media_files") == 2);
sql(db, "DROP TRIGGER fail_delete");
assert(!media_index_delete_channel(db, "1", root) && !scalar(db, "SELECT count(*) FROM media_files"));
assert(!media_index_delete_channel(db, "1", root));
write_file(path); assert(!commit(db, "1", path));
uint8_t other[16] = {44};
assert(!media_index_register_downloaded(db, result.media_id, other, result.content_hash, "2", "delete", 1, 14, 7, 0, 0));
assert(!media_index_delete_channel(db, "1", root) && ma_file_size(path) == 14);
assert(!media_index_delete_channel(db, "2", root) && ma_file_size(path) == -1);
assert(!utun_mkdir(path, 0700)); assert(!commit(db, "1", path));
assert(media_index_delete_channel(db, "1", root) == -1 && scalar(db, "SELECT count(*) FROM media_files") == 2);
assert(!test_rmdir(path)); assert(!media_index_delete_channel(db, "1", root));
/* Подготовка могла создать файл и упасть до первого INSERT блока. */
write_file(path); assert(!media_index_track_preparation(db, "1", path, root));
assert(!media_index_delete_channel(db, "1", root) && ma_file_size(path) == -1);
assert(!scalar(db, "SELECT count(*) FROM media_preparations"));
sql(db, "DROP TABLE media_files"); assert(media_index_delete_channel(db, "1", root) == -1);
sqlite3_close(db);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media regression deletion retry/shared/unprepared files PASS");
}
static void long_path(void) {
#ifndef _WIN32
char path[MEDIA_PATH_MAX]; snprintf(path, sizeof(path), "%s", root);
for (int i = 0; i < 3; i++) {
size_t n = strlen(path); path[n++] = '/'; memset(path + n, 'a' + i, 180); path[n + 180] = 0;
assert(!utun_mkdir(path, 0700));
}
strcat(path, "/payload"); write_file(path); sqlite3* db = database();
assert(!commit(db, "1", path));
sqlite3_stmt* st = NULL; assert(!sqlite3_prepare_v2(db, "SELECT location FROM media_files LIMIT 1", -1, &st, NULL));
assert(sqlite3_step(st) == SQLITE_ROW); const char* location = (const char*)sqlite3_column_text(st, 0);
char restored[MEDIA_PATH_MAX];
assert(strlen(location) == 550 && !media_index_path(root, location, restored) && !strcmp(restored, path));
assert(ma_file_size(restored) == 14); sqlite3_finalize(st);
assert(!media_index_delete_channel(db, "1", root) && ma_file_size(path) == -1); sqlite3_close(db);
#endif
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media regression long location PASS");
}
static int deleted_events, failed_submissions, successful_submissions;
static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int length) {
(void)inst;
if (type == CHAT_EVT_CHANNEL_DELETED) deleted_events++;
if (type == CHAT_EVT_MSG_SUBMITTED) {
assert(length == 9);
if (data[8]) successful_submissions++; else failed_submissions++;
}
}
static void pending_deletion(void) {
char config[1024]; snprintf(config, sizeof(config), "[global]\ndb_path=%s\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n", root);
struct UASYNC* ua = uasync_create(); assert(ua);
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config); assert(inst);
assert(!utun_core_start(inst) && !chat_service_start(inst)); chat_event_set_handler(inst, event);
chat_core_create_channel_auto(inst, "pending");
sqlite3_stmt* st = NULL; sqlite3* db = inst->topo_sqlite_db;
assert(!sqlite3_prepare_v2(db, "SELECT channel_id FROM channels WHERE name='pending'", -1, &st, NULL));
assert(sqlite3_step(st) == SQLITE_ROW); char channel[64]; snprintf(channel, sizeof(channel), "%s", sqlite3_column_text(st, 0));
sqlite3_finalize(st);
char source[MEDIA_PATH_MAX], destinations[2][MEDIA_PATH_MAX];
assert(!media_index_path(root, "source", source)); write_file(source);
struct chat_msg_submit req = {.inst = inst, .media_copy = 1, .data = (uint8_t*)"payload", .data_len = 7};
snprintf(req.channel_id, sizeof(req.channel_id), "%s", channel);
for (int i = 0; i < 2; i++) {
char relative[32]; snprintf(relative, sizeof(relative), "outgoing%d", i);
assert(!media_index_path(root, relative, destinations[i]));
snprintf(req.media_src, sizeof(req.media_src), "%s", source);
snprintf(req.media_dest, sizeof(req.media_dest), "%s", destinations[i]);
req.request_id = i + 1; chat_core_submit_media_message(inst, &req);
}
assert(CC(inst)->media_submissions && CC(inst)->media_submissions->next); /* completions не обработаны */
chat_core_delete_channel(inst, channel); chat_core_delete_channel(inst, channel);
assert(!deleted_events && scalar(db, "SELECT count(*) FROM channels WHERE name='pending'") == 1);
req.request_id = 3; chat_core_submit_media_message(inst, &req); assert(failed_submissions == 1);
for (int i = 0; i < 2000 && !deleted_events; i++) uasync_poll(ua, 100);
assert(deleted_events == 1 && failed_submissions == 3 && !successful_submissions && !CC(inst)->media_submissions);
assert(!scalar(db, "SELECT count(*) FROM channels WHERE name='pending'"));
assert(!scalar(db, "SELECT count(*) FROM media_files") && !scalar(db, "SELECT count(*) FROM media_preparations"));
assert(ma_file_size(source) == 14 && ma_file_size(destinations[0]) == -1 && ma_file_size(destinations[1]) == -1);
chat_core_ensure_channel_ready(inst, channel); uasync_poll(ua, 0);
assert(!scalar(db, "SELECT count(*) FROM media_files"));
utun_instance_destroy(inst); uasync_destroy(ua, 0);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media regression pending deletion/two completions/recreation PASS");
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO);
assert(!test_mkdtemp(root)); ids[16] = 2;
paths(); transactions(); deletion(); long_path(); pending_deletion();
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "test_media_index_regressions PASS"); return 0;
}