Add fault injection regressions for media files and worker ownership
This commit is contained in:
1 parent
5505afd1f3
commit
3a00caef50
2 files changed
+309
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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 <assert.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <pthread.h>
|
||||
#include <string.h>
|
||||
#ifndef _WIN32
|
||||
#include <dirent.h>
|
||||
#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;
|
||||
}
|
||||
Reference in new issue
Block a user