Test video I/O failures, audio timing, cancellation and concurrent ownership

This commit is contained in:
evgeny committed 2026-10-22 00:05:30 +02:00
1 parent 5929bb4028
commit 9163025912
2 files changed
+262 -1

No files matched your search

+7
View File
@@ -786,6 +786,13 @@ test_call_video_LDADD = $(LIBUTUN) $(COMMON_LIBS)
test_video_SOURCES = test_video.c
test_video_LDADD = $(LIBUTUN) $(COMMON_LIBS)
if HAVE_FFMPEG
if OS_LINUX
test_video_LDFLAGS = -Wl,--wrap=fdopen -Wl,--wrap=fwrite -Wl,--wrap=fflush -Wl,--wrap=fclose -Wl,--wrap=rename \
-Wl,--wrap=av_write_trailer -Wl,--wrap=avcodec_send_packet -Wl,--wrap=avcodec_send_frame \
-Wl,--wrap=avcodec_receive_packet -Wl,--wrap=avcodec_find_encoder -Wl,--wrap=pthread_create -Wl,--wrap=u_calloc_impl
endif
endif
test_video_plane_SOURCES = test_video_plane.c
test_video_plane_LDADD = ../lib/libuasync.a $(COMMON_LIBS)
+255 -1
View File
@@ -13,6 +13,11 @@
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <pthread.h>
#include <time.h>
#ifndef _WIN32
#include <dirent.h>
#endif
#ifdef HAVE_FFMPEG
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
@@ -32,6 +37,96 @@ static void done(void* arg, int error, const struct video_info* info) {
#ifdef HAVE_FFMPEG
static char directory[256], source[512], destination[512];
static int audio_offset_ms;
#ifdef __linux__
enum video_fault { NO_FAULT, WRITE_FAULT, FLUSH_FAULT, CLOSE_FAULT, PUBLISH_FAULT, TRAILER_FAULT,
DECODER_DRAIN_FAULT, ENCODER_DRAIN_FAULT, BAD_DRAIN_FAULT, AAC_FAULT, THREAD_FAULT, ALLOCATION_FAULT };
static enum video_fault fault;
static int injected;
static FILE* temporary_file;
static AVCodecContext* draining_encoder;
extern FILE* __real_fdopen(int, const char*);
extern size_t __real_fwrite(const void*, size_t, size_t, FILE*);
extern int __real_fflush(FILE*);
extern int __real_fclose(FILE*);
extern int __real_rename(const char*, const char*);
extern int __real_av_write_trailer(AVFormatContext*);
extern int __real_avcodec_send_packet(AVCodecContext*, const AVPacket*);
extern int __real_avcodec_send_frame(AVCodecContext*, const AVFrame*);
extern int __real_avcodec_receive_packet(AVCodecContext*, AVPacket*);
extern const AVCodec* __real_avcodec_find_encoder(enum AVCodecID);
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*);
static int inject(enum video_fault wanted) {
if (fault != wanted || injected) return 0;
injected = 1; errno = ENOSPC; return 1;
}
FILE* __wrap_fdopen(int fd, const char* mode) {
FILE* file = __real_fdopen(fd, mode);
if (!strcmp(mode, "wb+")) temporary_file = file;
return file;
}
size_t __wrap_fwrite(const void* bytes, size_t size, size_t count, FILE* file) {
if (file == temporary_file && inject(WRITE_FAULT)) return 0;
return __real_fwrite(bytes, size, count, file);
}
int __wrap_fflush(FILE* file) {
if (file == temporary_file && inject(FLUSH_FAULT)) return EOF;
return __real_fflush(file);
}
int __wrap_fclose(FILE* file) {
int failed = file == temporary_file && inject(CLOSE_FAULT);
if (file == temporary_file) temporary_file = NULL;
int result = __real_fclose(file);
if (failed) { errno = ENOSPC; return EOF; }
return result;
}
int __wrap_rename(const char* from, const char* to) {
return inject(PUBLISH_FAULT) ? -1 : __real_rename(from, to);
}
int __wrap_av_write_trailer(AVFormatContext* format) {
return inject(TRAILER_FAULT) ? AVERROR(EIO) : __real_av_write_trailer(format);
}
int __wrap_avcodec_send_packet(AVCodecContext* codec, const AVPacket* packet) {
if (!packet && inject(DECODER_DRAIN_FAULT)) return AVERROR(EIO);
return __real_avcodec_send_packet(codec, packet);
}
int __wrap_avcodec_send_frame(AVCodecContext* codec, const AVFrame* frame) {
if (!frame && inject(ENCODER_DRAIN_FAULT)) return AVERROR(EIO);
int error = __real_avcodec_send_frame(codec, frame);
if (!error && !frame) draining_encoder = codec;
return error;
}
int __wrap_avcodec_receive_packet(AVCodecContext* codec, AVPacket* packet) {
if (codec == draining_encoder && inject(BAD_DRAIN_FAULT)) return AVERROR(EAGAIN);
return __real_avcodec_receive_packet(codec, packet);
}
const AVCodec* __wrap_avcodec_find_encoder(enum AVCodecID id) {
return id == AV_CODEC_ID_AAC && inject(AAC_FAULT) ? NULL : __real_avcodec_find_encoder(id);
}
int __wrap_pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*entry)(void*), void* arg) {
return inject(THREAD_FAULT) ? EAGAIN : __real_pthread_create(thread, attr, entry, arg);
}
void* __wrap_u_calloc_impl(uint32_t count, uint32_t size, const char* where) {
return inject(ALLOCATION_FAULT) ? NULL : __real_u_calloc_impl(count, size, where);
}
#endif
static void check_no_temporary(void) {
#ifndef _WIN32
DIR* dir = opendir(directory); assert(dir);
struct dirent* entry;
while ((entry = readdir(dir))) assert(!strstr(entry->d_name, ".tmp."));
assert(!closedir(dir));
#else
char pattern[512]; snprintf(pattern, sizeof(pattern), "%s/*.tmp.*", directory);
WIN32_FIND_DATAA entry;
HANDLE find = FindFirstFileA(pattern, &entry);
assert(find == INVALID_HANDLE_VALUE && GetLastError() == ERROR_FILE_NOT_FOUND);
#endif
}
/* Выпустить все доступные пакеты генератора; это отдельный путь, не использующий video.c. */
static void fixture_packets(AVFormatContext* mux, AVCodecContext* enc, AVStream* stream) {
@@ -94,7 +189,7 @@ static void fixture(const char* path, const char* format, enum AVCodecID video,
frame = av_frame_alloc(); assert(frame);
frame->format = aenc->sample_fmt; frame->sample_rate = aenc->sample_rate;
assert(av_channel_layout_copy(&frame->ch_layout, &aenc->ch_layout) >= 0);
frame->nb_samples = block; frame->pts = sample;
frame->nb_samples = block; frame->pts = sample + audio_offset_ms * aenc->sample_rate / 1000;
assert(av_frame_get_buffer(frame, 0) >= 0);
assert(av_samples_set_silence(frame->extended_data, 0, block, 1, aenc->sample_fmt) >= 0);
assert(avcodec_send_frame(aenc, frame) >= 0); fixture_packets(mux, aenc, astream);
@@ -169,18 +264,158 @@ static void decode_result(const char* path, unsigned expected_frames, int expect
assert(frames == expected_frames);
av_frame_free(&frame); av_packet_free(&packet); avcodec_free_context(&dec); avformat_close_input(&mux);
}
/* Независимо декодировать AAC, проверяя хвост и сохранённое начальное смещение. */
static void decode_audio(const char* path, int minimum_samples, int offset_ms) {
AVFormatContext* format = NULL; assert(avformat_open_input(&format, path, NULL, NULL) >= 0);
assert(avformat_find_stream_info(format, NULL) >= 0);
int index = av_find_best_stream(format, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0); assert(index >= 0);
AVStream* stream = format->streams[index];
AVCodecContext* decoder = avcodec_alloc_context3(avcodec_find_decoder(stream->codecpar->codec_id)); assert(decoder);
assert(avcodec_parameters_to_context(decoder, stream->codecpar) >= 0 && avcodec_open2(decoder, decoder->codec, NULL) >= 0);
AVPacket* packet = av_packet_alloc(); AVFrame* frame = av_frame_alloc(); assert(packet && frame);
int samples = 0, eof = 0; int64_t first_us = AV_NOPTS_VALUE;
while (!eof) {
int error = av_read_frame(format, packet);
if (error == AVERROR_EOF) eof = 1;
else { assert(error >= 0); if (packet->stream_index != index) { av_packet_unref(packet); continue; } }
assert(avcodec_send_packet(decoder, eof ? NULL : packet) >= 0); av_packet_unref(packet);
while ((error = avcodec_receive_frame(decoder, frame)) >= 0) {
if (first_us == AV_NOPTS_VALUE) first_us = av_rescale_q(frame->best_effort_timestamp, stream->time_base, AV_TIME_BASE_Q);
samples += frame->nb_samples;
}
assert(error == AVERROR_EOF || error == AVERROR(EAGAIN));
}
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "test_video decoded audio samples=%d first_us=%lld expected_offset_ms=%d",
samples, (long long)first_us, offset_ms);
assert(samples >= minimum_samples && samples <= minimum_samples + 2048);
assert(first_us != AV_NOPTS_VALUE && llabs(first_us - (int64_t)offset_ms * 1000) <= 33000);
av_packet_free(&packet); av_frame_free(&frame); avcodec_free_context(&decoder); avformat_close_input(&format);
}
static void test_aliases(struct UASYNC* ua, struct media_async* owner) {
uint8_t before[32], after[32]; assert(!ma_sha256_file(source, before));
char alias[512]; snprintf(alias, sizeof(alias), "%s/./source.mp4", directory);
prepare(ua, owner, source, alias); assert(result_error);
#ifndef _WIN32
snprintf(alias, sizeof(alias), "%s/alias", directory);
assert(!link(source, alias)); prepare(ua, owner, source, alias); assert(result_error); assert(!remove(alias));
assert(!symlink("source.mp4", alias)); prepare(ua, owner, source, alias); assert(result_error); assert(!remove(alias));
#endif
assert(!ma_sha256_file(source, after) && !memcmp(before, after, sizeof(before)));
char oversized[VIDEO_PATH_MAX + 1]; memset(oversized, 'x', sizeof(oversized)); oversized[VIDEO_PATH_MAX] = 0;
completed = 0;
assert(video_transcode_start(owner, ua, oversized, destination, done, NULL) == -1 && !completed);
assert(video_transcode_start(owner, ua, source, "", done, NULL) == -1 && !completed);
assert(video_extract_thumbnail(source, destination, 1) == -1);
check_no_temporary();
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "PASS video aliases, links and invalid paths");
}
static void test_cancel(struct UASYNC* ua) {
struct media_async* owner = media_async_create(); assert(owner);
write_sentinel(destination); completed = 0;
assert(!video_transcode_start(owner, ua, source, destination, done, NULL));
for (unsigned attempt = 0; attempt < 2000 && video_transcode_get_progress() != 1000; attempt++) {
#ifdef _WIN32
Sleep(1);
#else
struct timespec delay = {0, 1000000}; nanosleep(&delay, NULL);
#endif
}
assert(video_transcode_get_progress() == 1000 && video_transcode_is_active() && !completed);
check_sentinel(destination); // Worker уже завершён, но конечный файл ещё не опубликован.
media_async_destroy(owner);
assert(completed == 1 && result_error == MEDIA_ASYNC_CANCELLED && !video_transcode_is_active());
uasync_poll(ua, 0); assert(completed == 1); check_sentinel(destination); check_no_temporary();
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "PASS video cancellation before completion preserves destination");
}
#ifdef __linux__
static void test_faults(struct UASYNC* ua, struct media_async* owner) {
for (enum video_fault which = WRITE_FAULT; which <= PUBLISH_FAULT; which++) {
write_sentinel(destination); injected = 0; fault = which;
int error = video_extract_thumbnail(source, destination, 32);
fault = NO_FAULT;
assert(error == -1 && injected); check_sentinel(destination); check_no_temporary();
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "PASS thumbnail injected failure=%d", which);
}
for (enum video_fault which = WRITE_FAULT; which <= ALLOCATION_FAULT; which++) {
write_sentinel(destination); injected = 0; fault = which; completed = 0; draining_encoder = NULL;
int started = video_transcode_start(owner, ua, source, destination, done, NULL);
if (!started) {
for (unsigned attempt = 0; attempt < 2000 && !completed; attempt++) uasync_poll(ua, 5);
assert(completed == 1 && result_error == -1);
} else assert(which == ALLOCATION_FAULT && !completed);
fault = NO_FAULT;
assert(injected && !video_transcode_is_active()); check_sentinel(destination); check_no_temporary();
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "PASS preparation injected failure=%d", which);
}
}
struct race_state { pthread_barrier_t* barrier; const char* path; int started, callbacks; pthread_t thread; };
static void race_done(void* arg, int error, const struct video_info* info) {
struct race_state* state = arg;
assert(!error && info && pthread_equal(pthread_self(), state->thread)); state->callbacks++;
}
static void* race_start(void* arg) {
struct race_state* state = arg; state->thread = pthread_self();
struct UASYNC* ua = uasync_create(); struct media_async* owner = media_async_create(); assert(ua && owner);
pthread_barrier_wait(state->barrier);
state->started = video_transcode_start(owner, ua, source, state->path, race_done, state);
pthread_barrier_wait(state->barrier); // Победитель не освобождает global slot раньше второго start.
if (!state->started) {
for (unsigned attempt = 0; attempt < 2000 && !state->callbacks; attempt++) uasync_poll(ua, 5);
assert(state->callbacks == 1);
} else assert(state->started == -1 && !state->callbacks);
media_async_destroy(owner); uasync_destroy(ua, 0); return NULL;
}
static void test_concurrent_start(void) {
pthread_barrier_t barrier; assert(!pthread_barrier_init(&barrier, NULL, 2));
char paths[2][512]; struct race_state state[2]; pthread_t threads[2];
for (unsigned i = 0; i < 2; i++) {
snprintf(paths[i], sizeof(paths[i]), "%s/race-%u.mp4", directory, i);
state[i] = (struct race_state){.barrier = &barrier, .path = paths[i]};
assert(!pthread_create(&threads[i], NULL, race_start, &state[i]));
}
for (unsigned i = 0; i < 2; i++) assert(!pthread_join(threads[i], NULL));
assert((state[0].started == 0) + (state[1].started == 0) == 1);
for (unsigned i = 0; i < 2; i++) if (!state[i].started) { decode_result(paths[i], 6, 1); assert(!remove(paths[i])); }
assert(!pthread_barrier_destroy(&barrier)); check_no_temporary();
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "PASS concurrent video start: one global owner, callbacks on correct threads");
}
#endif
struct chain_state { struct UASYNC* ua; struct media_async* owner; unsigned count; };
static void chain_done(void* arg, int error, const struct video_info* info) {
struct chain_state* state = arg; assert(!error && info && info->is_video);
if (++state->count == 1) assert(!video_transcode_start(state->owner, state->ua, source, destination, chain_done, state));
}
static void test_reentrant(struct UASYNC* ua, struct media_async* owner) {
struct chain_state state = {ua, owner, 0};
assert(!video_transcode_start(owner, ua, source, destination, chain_done, &state));
for (unsigned attempt = 0; attempt < 2000 && state.count < 2; attempt++) uasync_poll(ua, 5);
assert(state.count == 2 && !video_transcode_is_active()); check_no_temporary();
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "PASS video restart from completion callback");
}
#endif
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO);
#ifdef HAVE_FFMPEG
video_ffmpeg_init();
#ifdef _WIN32
snprintf(directory, sizeof(directory), "utun-video-test-%llu", (unsigned long long)get_time_us());
#else
snprintf(directory, sizeof(directory), "/tmp/utun-video-test-%llu", (unsigned long long)get_time_us());
#endif
assert(!utun_mkdir(directory, 0700));
snprintf(source, sizeof(source), "%s/source.mp4", directory);
snprintf(destination, sizeof(destination), "%s/result.mp4", directory);
struct UASYNC* ua = uasync_create(); struct media_async* owner = media_async_create(); assert(ua && owner);
fixture(source, "mp4", AV_CODEC_ID_H264, AV_CODEC_ID_NONE);
same_file(ua, owner);
test_aliases(ua, owner);
assert(!video_extract_thumbnail(source, destination, 32)); decode_result(destination, 1, 0);
write_sentinel(destination);
assert(video_extract_thumbnail("/nonexistent/video", destination, 32) == -1); check_sentinel(destination);
@@ -188,8 +423,27 @@ int main(void) {
assert(video_extract_thumbnail(source, "/dev/full", 32) == -1);
#endif
prepare(ua, owner, source, destination); assert(!result_error); decode_result(destination, 6, 0);
fixture(source, "mov", AV_CODEC_ID_H264, AV_CODEC_ID_AAC);
prepare(ua, owner, source, destination); assert(!result_error); decode_result(destination, 6, 1);
FILE* mp4 = fopen(destination, "rb"); assert(mp4); char brand[12]; assert(fread(brand, 1, 12, mp4) == 12);
assert(!memcmp(brand + 4, "ftyp", 4) && memcmp(brand + 8, "qt ", 4)); assert(!fclose(mp4));
fixture(source, "matroska", AV_CODEC_ID_MPEG4, AV_CODEC_ID_PCM_S16LE);
prepare(ua, owner, source, destination); assert(!result_error); decode_result(destination, 6, 1);
decode_audio(destination, 19584, 0);
audio_offset_ms = 200;
fixture(source, "matroska", AV_CODEC_ID_MPEG4, AV_CODEC_ID_PCM_S16LE);
prepare(ua, owner, source, destination); assert(!result_error); decode_audio(destination, 19584, 200);
audio_offset_ms = 0;
if (avcodec_find_encoder(AV_CODEC_ID_MP3)) {
fixture(source, "matroska", AV_CODEC_ID_MPEG4, AV_CODEC_ID_MP3);
prepare(ua, owner, source, destination); assert(!result_error); decode_result(destination, 6, 1);
}
fixture(source, "matroska", AV_CODEC_ID_MPEG4, AV_CODEC_ID_PCM_S16LE);
test_cancel(ua); test_reentrant(ua, owner);
#ifdef __linux__
test_faults(ua, owner); test_concurrent_start();
#endif
check_no_temporary();
media_async_destroy(owner); uasync_destroy(ua, 0);
assert(!remove(source) && !remove(destination)); assert(!rmdir(directory));
#else