diff --git a/tests/Makefile.am b/tests/Makefile.am index ea3b3001..943d550b 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -860,6 +860,16 @@ test_media_async_SOURCES = test_media_async.c test_media_async_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/media_async -I$(top_srcdir)/lib test_media_async_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +check_PROGRAMS += test_media_primitives +test_media_primitives_SOURCES = test_media_primitives.c +test_media_primitives_LDADD = $(LIBUTUN) $(COMMON_LIBS) +if HOST_LINUX +test_media_primitives_LDFLAGS = -Wl,--wrap=fopen -Wl,--wrap=fdopen -Wl,--wrap=fread -Wl,--wrap=ferror \ + -Wl,--wrap=fwrite -Wl,--wrap=fflush -Wl,--wrap=fclose -Wl,--wrap=rename -Wl,--wrap=RAND_bytes \ + -Wl,--wrap=opendir -Wl,--wrap=readdir -Wl,--wrap=closedir -Wl,--wrap=fseeko \ + -Wl,--wrap=pthread_create -Wl,--wrap=u_calloc_impl -Wl,--wrap=uasync_post_reserved +endif + 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) diff --git a/tests/test_media_primitives.c b/tests/test_media_primitives.c new file mode 100644 index 00000000..b8181d12 --- /dev/null +++ b/tests/test_media_primitives.c @@ -0,0 +1,299 @@ +/* Регрессии сохранности файлов, больших размеров, ошибок RNG/обхода и владения async задачами. */ +#include "../lib/platform_compat.h" +#include "../lib/file_io.h" +#include "../lib/debug_config.h" +#include "../lib/mem.h" +#include "../lib/u_async.h" +#include "../src/media_async/media_async.h" +#include "../src/media_delivery/media_index.h" +#include +#include +#include +#include +#include +#ifndef _WIN32 +#include +#endif + +static char root[256], source[512], destination[512]; +static unsigned visits; + +#ifdef __linux__ +enum fault { NONE, READ, WRITE, FLUSH, SOURCE_CLOSE, OUTPUT_CLOSE, PUBLISH, OPEN_DIR, READ_DIR, CLOSE_DIR, SEEK, THREAD }; +static enum fault fault; +static int injected, read_failed, rng_after = -1, allocation_after = -1; +static FILE *input_file, *output_file; +static pthread_barrier_t *post_barrier; +extern FILE *__real_fopen(const char*, const char*); +extern FILE *__real_fdopen(int, const char*); +extern size_t __real_fread(void*, size_t, size_t, FILE*); +extern size_t __real_fwrite(const void*, size_t, size_t, FILE*); +extern int __real_ferror(FILE*), __real_fflush(FILE*), __real_fclose(FILE*); +extern int __real_rename(const char*, const char*), __real_RAND_bytes(unsigned char*, int); +extern int __real_fseeko(FILE*, off_t, int); +extern DIR *__real_opendir(const char*); +extern struct dirent *__real_readdir(DIR*); +extern int __real_closedir(DIR*); +extern int __real_pthread_create(pthread_t*, const pthread_attr_t*, void* (*)(void*), void*); +extern void *__real_u_calloc_impl(uint32_t, uint32_t, const char*); +extern void __real_uasync_post_reserved(struct UASYNC*, struct posted_task*); + +static int inject(enum fault value) { + if (fault != value || injected) return 0; + injected = 1; errno = EIO; return 1; +} +FILE *__wrap_fopen(const char *path, const char *mode) { + FILE *file = __real_fopen(path, mode); + if (!strcmp(mode, "rb")) input_file = file; + return file; +} +FILE *__wrap_fdopen(int fd, const char *mode) { + FILE *file = __real_fdopen(fd, mode); + if (!strcmp(mode, "wb+")) output_file = file; + return file; +} +size_t __wrap_fread(void *data, size_t size, size_t count, FILE *file) { + if (file == input_file && inject(READ)) { read_failed = 1; return 0; } + return __real_fread(data, size, count, file); +} +int __wrap_ferror(FILE *file) { return file == input_file && read_failed ? 1 : __real_ferror(file); } +size_t __wrap_fwrite(const void *data, size_t size, size_t count, FILE *file) { + return file == output_file && inject(WRITE) ? 0 : __real_fwrite(data, size, count, file); +} +int __wrap_fflush(FILE *file) { return file == output_file && inject(FLUSH) ? EOF : __real_fflush(file); } +int __wrap_fclose(FILE *file) { + int failed = (file == input_file && inject(SOURCE_CLOSE)) || (file == output_file && inject(OUTPUT_CLOSE)); + if (file == input_file) { input_file = NULL; read_failed = 0; } + if (file == output_file) output_file = NULL; + int result = __real_fclose(file); + if (failed) { errno = EIO; return EOF; } + return result; +} +int __wrap_rename(const char *from, const char *to) { return inject(PUBLISH) ? -1 : __real_rename(from, to); } +int __wrap_RAND_bytes(unsigned char *out, int size) { + if (rng_after == 0) { injected = 1; return 0; } + if (rng_after > 0) rng_after--; + return __real_RAND_bytes(out, size); +} +int __wrap_fseeko(FILE *file, off_t offset, int whence) { + return inject(SEEK) ? -1 : __real_fseeko(file, offset, whence); +} +DIR *__wrap_opendir(const char *path) { return inject(OPEN_DIR) ? NULL : __real_opendir(path); } +struct dirent *__wrap_readdir(DIR *dir) { + if (visits && inject(READ_DIR)) return NULL; + return __real_readdir(dir); +} +int __wrap_closedir(DIR *dir) { + int result = __real_closedir(dir); + if (inject(CLOSE_DIR)) return -1; + return result; +} +int __wrap_pthread_create(pthread_t *thread, const pthread_attr_t *attr, void* (*fn)(void*), void *arg) { + return inject(THREAD) ? EAGAIN : __real_pthread_create(thread, attr, fn, arg); +} +void *__wrap_u_calloc_impl(uint32_t count, uint32_t size, const char *where) { + if (!allocation_after) { injected = 1; return NULL; } + if (allocation_after > 0) allocation_after--; + return __real_u_calloc_impl(count, size, where); +} +void __wrap_uasync_post_reserved(struct UASYNC *ua, struct posted_task *task) { + __real_uasync_post_reserved(ua, task); + pthread_barrier_t *barrier = __atomic_load_n(&post_barrier, __ATOMIC_ACQUIRE); + if (barrier) pthread_barrier_wait(barrier); +} +#endif + +static void write_text(const char *path, const char *text) { + FILE *file = fopen(path, "wb"); assert(file); + assert(fwrite(text, 1, strlen(text), file) == strlen(text)); assert(!fclose(file)); +} +static void check_text(const char *path, const char *text) { + FILE *file = file_io_open_read(path); assert(file); + char bytes[64] = {0}; size_t size = fread(bytes, 1, sizeof(bytes), file); assert(!fclose(file)); + assert(size == strlen(text) && !memcmp(bytes, text, size)); +} +static void visit(void *arg, const char *path, int64_t size) { + (void)arg; assert(size >= 0 && !strstr(path, ".tmp.")); visits++; +} +static void remove_file(void *arg, const char *path, int64_t size) { + (void)arg; (void)size; assert(!remove(path)); +} + +static void copy_tests(void) { + write_text(source, "source-payload"); write_text(destination, "previous"); + assert(!ma_copy_file(source, source)); check_text(source, "source-payload"); + char alias[512]; snprintf(alias, sizeof(alias), "%s/./source", root); + assert(!ma_copy_file(source, alias)); check_text(source, "source-payload"); + snprintf(alias, sizeof(alias), "%s/missing", root); + assert(ma_copy_file(alias, alias) == -1); +#ifndef _WIN32 + snprintf(alias, sizeof(alias), "%s/alias", root); + assert(!link(source, alias)); assert(!ma_copy_file(source, alias)); assert(!remove(alias)); + assert(!symlink("source", alias)); assert(ma_copy_file(source, alias) == -1); assert(!remove(alias)); + assert(ma_copy_file(source, "/dev/full") == -1); +#endif + check_text(source, "source-payload"); check_text(destination, "previous"); +#ifdef __linux__ + for (enum fault test = READ; test <= PUBLISH; test++) { + fault = test; injected = 0; read_failed = 0; + int error = ma_copy_file(source, destination); fault = NONE; + assert(error == -1 && injected); + check_text(source, "source-payload"); check_text(destination, "previous"); + visits = 0; assert(!ma_dir_walk(root, visit, NULL) && visits == 2); + DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS copy injected failure=%d: both files unchanged, no temp", test); + } +#endif + assert(!ma_copy_file(source, destination)); check_text(destination, "source-payload"); + write_text(source, ""); assert(!ma_copy_file(source, destination) && ma_file_size(destination) == 0); + write_text(source, "source-payload"); + DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS copy aliases, links, missing source, successful and empty file"); +} + +static void size_and_walk_tests(void) { + char path[512]; snprintf(path, sizeof(path), "%s/large", root); + const int64_t sizes[] = {INT_MAX, (int64_t)INT_MAX + 1, INT64_C(4294967296) + 17}; + for (unsigned n = 0; n < sizeof(sizes) / sizeof(sizes[0]); n++) { + FILE *file = fopen(path, "wb"); assert(file); +#ifdef _WIN32 + assert(!_fseeki64(file, sizes[n] - 1, SEEK_SET)); +#else + assert(!fseeko(file, (off_t)(sizes[n] - 1), SEEK_SET)); +#endif + assert(fputc(0, file) != EOF && !fclose(file)); assert(ma_file_size(path) == sizes[n]); + } + visits = 0; assert(!ma_dir_walk(root, visit, NULL) && visits == 3); + assert(ma_file_size(root) == -1 && ma_dir_walk(source, visit, NULL) == -1); + snprintf(path, sizeof(path), "%s/missing", root); assert(ma_dir_walk(path, visit, NULL) == -1); +#ifndef _WIN32 + snprintf(path, sizeof(path), "%s/cycle", root); assert(!symlink(".", path)); + visits = 0; assert(!ma_dir_walk(root, visit, NULL) && visits == 3); + visits = 0; assert(!ma_dir_walk(path, visit, NULL) && !visits); assert(!remove(path)); +#endif +#ifdef __linux__ + for (enum fault test = OPEN_DIR; test <= CLOSE_DIR; test++) { + fault = test; visits = 0; injected = 0; + int error = ma_dir_walk(root, visit, NULL); fault = NONE; + assert(error == -1 && injected); + if (test == READ_DIR) assert(visits == 1); + } + char deep[1024]; snprintf(deep, sizeof(deep), "%s", root); + for (unsigned n = 0; n <= MA_WALK_MAX_DEPTH; n++) { strcat(deep, "/d"); assert(!utun_mkdir(deep, 0700)); } + assert(ma_dir_walk(root, visit, NULL) == -1); + for (unsigned n = 0; n <= MA_WALK_MAX_DEPTH; n++) { assert(!rmdir(deep)); *strrchr(deep, '/') = 0; } +#endif + char oversized[FILE_IO_PATH_MAX + 1]; memset(oversized, 'x', sizeof(oversized)); oversized[sizeof(oversized) - 1] = 0; + assert(ma_dir_walk(oversized, visit, NULL) == -1); + DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS 64-bit sparse sizes, link cycles, directory failures and depth/path bounds"); +} + +struct registration { unsigned calls; int error; }; +static void registered(void *arg, int error, const struct media_index_result *result) { + struct registration *state = arg; state->calls++; state->error = error; + assert(error ? result == NULL : result && result->file_size == 14); +} +static void wait_registration(struct UASYNC *ua, struct registration *state) { + uint64_t deadline = get_time_tb() + 50000; + while (!state->calls && get_time_tb() < deadline) uasync_poll(ua, 10); + assert(state->calls == 1); +} +static int row_count(sqlite3 *db) { + sqlite3_stmt *stmt = NULL; assert(sqlite3_prepare_v2(db, "SELECT count(*) FROM media_files", -1, &stmt, NULL) == SQLITE_OK); + assert(sqlite3_step(stmt) == SQLITE_ROW); int count = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); return count; +} +static void registration_tests(struct UASYNC *ua) { + sqlite3 *db = NULL; assert(sqlite3_open(":memory:", &db) == SQLITE_OK && !media_index_init(db)); + struct media_async *owner = media_async_create(); assert(owner); uint8_t key[32]; memset(key, 1, sizeof(key)); +#ifdef __linux__ + for (int n = 0; n < 3; n++) { + struct registration state = {0}; injected = 0; + rng_after = n < 2 ? n : -1; fault = n == 2 ? SEEK : NONE; + media_index_register_async(owner, ua, db, 1, key, "123", source, source, 0, root, registered, &state); + wait_registration(ua, &state); rng_after = -1; fault = NONE; + assert(state.error && injected && !row_count(db)); + } + uint8_t uuid[16], zero[16] = {0}; memset(uuid, 0xa5, sizeof(uuid)); rng_after = 0; + assert(media_index_generate_uuid(uuid) == -1 && !memcmp(uuid, zero, sizeof(uuid))); rng_after = -1; +#endif + struct registration state = {0}; + media_index_register_async(owner, ua, db, 1, key, "123", source, source, 1, root, registered, &state); + wait_registration(ua, &state); assert(!state.error && row_count(db) == 1); + media_async_destroy(owner); assert(sqlite3_close(db) == SQLITE_OK); + DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS RNG/seek errors reach callback without DB rows; same-path registration succeeds"); +} + +#ifdef __linux__ +struct job { + struct media_async *owner; struct UASYNC *ua; + unsigned work, done; int error, restart, cancel_actions; + pthread_t event_thread; +}; +static void job_work(void *arg) { ((struct job*)arg)->work++; } +static void job_done(void *arg, int error) { + struct job *job = arg; assert(pthread_equal(pthread_self(), job->event_thread)); + job->done++; job->error = error; + if (job->restart && job->done == 1) media_async_submit(job->owner, job->ua, job_work, job, job_done, job); + if (job->cancel_actions && job->done == 1) { + media_async_destroy(job->owner); // Отклонённый повторный вход во время destroy. + media_async_submit(job->owner, job->ua, job_work, job, job_done, job); + } +} +static void engine_tests(struct UASYNC *ua) { + size_t before = u_get_allocated_count(); + struct media_async *owner = media_async_create(); assert(owner); + struct job jobs[MEDIA_ASYNC_MAX_TASKS + 1] = {0}; + pthread_barrier_t barrier; assert(!pthread_barrier_init(&barrier, NULL, MEDIA_ASYNC_MAX_TASKS + 1)); + __atomic_store_n(&post_barrier, &barrier, __ATOMIC_RELEASE); + for (unsigned n = 0; n < MEDIA_ASYNC_MAX_TASKS; n++) { + jobs[n].event_thread = pthread_self(); media_async_submit(owner, ua, job_work, &jobs[n], job_done, &jobs[n]); + } + pthread_barrier_wait(&barrier); __atomic_store_n(&post_barrier, NULL, __ATOMIC_RELEASE); + struct job *rejected = &jobs[MEDIA_ASYNC_MAX_TASKS]; rejected->event_thread = pthread_self(); + media_async_submit(owner, ua, job_work, rejected, job_done, rejected); + assert(rejected->done == 1 && rejected->error == -1 && !rejected->work); + media_async_destroy(owner); uasync_poll(ua, 0); pthread_barrier_destroy(&barrier); + for (unsigned n = 0; n < MEDIA_ASYNC_MAX_TASKS; n++) + assert(jobs[n].work == 1 && jobs[n].done == 1 && jobs[n].error == MEDIA_ASYNC_CANCELLED); + owner = media_async_create(); assert(owner); + struct job state = {.owner = owner, .ua = ua, .event_thread = pthread_self()}; + media_async_submit(NULL, ua, job_work, &state, job_done, &state); assert(state.done == 1 && state.error == -1); + media_async_submit(owner, ua, job_work, &state, NULL, &state); assert(state.done == 1); + for (int n = 0; n < 3; n++) { + state.done = 0; injected = 0; allocation_after = n < 2 ? n : -1; fault = n == 2 ? THREAD : NONE; + media_async_submit(owner, ua, job_work, &state, job_done, &state); allocation_after = -1; fault = NONE; + assert(injected && state.done == 1 && state.error == -1 && !state.work); + } + state.done = 0; state.restart = 1; + media_async_submit(owner, ua, job_work, &state, job_done, &state); + uint64_t deadline = get_time_tb() + 50000; + while (state.done != 2 && get_time_tb() < deadline) uasync_poll(ua, 10); + assert(state.done == 2 && state.work == 2 && !state.error); media_async_destroy(owner); + owner = media_async_create(); assert(owner); + state = (struct job){.owner = owner, .ua = ua, .cancel_actions = 1, .event_thread = pthread_self()}; + media_async_submit(owner, ua, job_work, &state, job_done, &state); media_async_destroy(owner); + assert(state.work == 1 && state.done == 2 && state.error == MEDIA_ASYNC_CANCELLED); + uasync_poll(ua, 0); assert(state.done == 2 && u_get_allocated_count() == before); + DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS 16 finished jobs retain slots; failures, reentrant submit, destroy and once-only cancellation"); +} +#endif + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_MEDIA, DEBUG_LEVEL_INFO); + size_t before = u_get_allocated_count(); +#ifdef _WIN32 + snprintf(root, sizeof(root), "media-primitives-%llu", (unsigned long long)get_time_us()); +#else + snprintf(root, sizeof(root), "/tmp/media-primitives-%llu", (unsigned long long)get_time_us()); +#endif + assert(!utun_mkdir(root, 0700)); snprintf(source, sizeof(source), "%s/source", root); + snprintf(destination, sizeof(destination), "%s/destination", root); + copy_tests(); size_and_walk_tests(); + struct UASYNC *ua = uasync_create(); assert(ua); + registration_tests(ua); +#ifdef __linux__ + engine_tests(ua); +#endif + uasync_destroy(ua, 0); assert(!ma_dir_walk(root, remove_file, NULL) && !rmdir(root)); + assert(u_get_allocated_count() == before); + DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS media primitives regressions, no tracked allocations"); + return 0; +}