PR #22734 opened by i-Amogh URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22734 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22734.patch
Added comprehensive FATE tests for libavutil/executor.c. This exercises the executor allocation, task queue management, readiness filtering, local context passing, recursive execution guard, and clean shutdown paths. The test covers both single-threaded (thread_count=0) and multi-threaded execution modes, as well as multiple independent executor instances and edge cases like nested av_executor_execute() calls during task callbacks. Coverage: 93.8% Line, 100% Function from 0% in libavutil/executor.c >From 536d739dd6a294911a0c046635cdd6427ccd0097 Mon Sep 17 00:00:00 2001 From: i-Amogh <[email protected]> Date: Tue, 7 Apr 2026 00:25:51 +0530 Subject: [PATCH 1/2] Added comprehensive FATE tests for libavutil/executor.c. This exercises the executor allocation, task queue management, readiness filtering, local context passing, recursive execution guard, and clean shutdown paths. The test covers both single-threaded (thread_count=0) and multi-threaded execution modes, as well as multiple independent executor instances and edge cases like nested av_executor_execute() calls during task callbacks. Signed-off-by: i-Amogh <[email protected]> --- libavutil/Makefile | 1 + libavutil/tests/executor.c | 668 +++++++++++++++++++++++++++++++++++++ tests/fate/libavutil.mak | 5 + 3 files changed, 674 insertions(+) create mode 100644 libavutil/tests/executor.c diff --git a/libavutil/Makefile b/libavutil/Makefile index ff166cc81a..9926dc6a08 100644 --- a/libavutil/Makefile +++ b/libavutil/Makefile @@ -275,6 +275,7 @@ TESTPROGS = adler32 \ encryption_info \ error \ eval \ + executor \ file \ fifo \ film_grain_params \ diff --git a/libavutil/tests/executor.c b/libavutil/tests/executor.c new file mode 100644 index 0000000000..c3d04c31ce --- /dev/null +++ b/libavutil/tests/executor.c @@ -0,0 +1,668 @@ +/* + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include <stdio.h> +#include <stdatomic.h> +#include <string.h> + +#include "libavutil/executor.h" +#include "libavutil/thread.h" +#include "libavutil/time.h" + +typedef struct TestTask { + AVTask task; + int id; + int priority; + int executed; + int should_fail_ready; + int local_context_received; +} TestTask; + +typedef struct TestContext { + atomic_int tasks_executed; + int fail_ready; + AVExecutor *nested_executor; +} TestContext; + +static int test_priority_higher(const AVTask *a, const AVTask *b) +{ + const TestTask *ta = (const TestTask *)a; + const TestTask *tb = (const TestTask *)b; + return ta->priority > tb->priority; +} + +static int test_ready(const AVTask *t, void *user_data) +{ + TestContext *ctx = user_data; + TestTask *tt = (TestTask *)t; + + if (ctx->fail_ready) + return 0; + + if (tt->should_fail_ready) + return 0; + + return 1; +} + +static int test_run(AVTask *t, void *local_context, void *user_data) +{ + TestTask *tt = (TestTask *)t; + + tt->executed = 1; + + if (local_context) { + ((int *)local_context)[0] = tt->id; + tt->local_context_received = ((int *)local_context)[0]; + } + + atomic_fetch_add(&((TestContext *)user_data)->tasks_executed, 1); + + return 0; +} + +static TestTask recursive_task_1, recursive_task_2; + +static int recursive_run(AVTask *t, void *local_context, void *user_data) +{ + TestContext *ctx = user_data; + TestTask *tt = (TestTask *)t; + + (void)local_context; + + if (tt->id == 1) { + av_executor_execute(ctx->nested_executor, &recursive_task_2.task); + } + + tt->executed = 1; + atomic_fetch_add(&ctx->tasks_executed, 1); + + return 0; +} + +static int test_null_params(void) +{ + AVExecutor *e; + AVTaskCallbacks cb = {0}; + int ret = 0; + + printf("Test: NULL callbacks\n"); + e = av_executor_alloc(NULL, 0); + if (e) { + printf("FAIL: av_executor_alloc(NULL) should return NULL\n"); + av_executor_free(&e); + ret = 1; + } + + printf("Test: Missing user_data\n"); + cb.user_data = NULL; + cb.priority_higher = test_priority_higher; + cb.ready = test_ready; + cb.run = test_run; + e = av_executor_alloc(&cb, 0); + if (e) { + printf("FAIL: av_executor_alloc with NULL user_data should return NULL\n"); + av_executor_free(&e); + ret = 1; + } + + printf("Test: Missing priority_higher\n"); + cb.user_data = NULL; + cb.priority_higher = NULL; + e = av_executor_alloc(&cb, 0); + if (e) { + printf("FAIL: av_executor_alloc with NULL priority_higher should return NULL\n"); + av_executor_free(&e); + ret = 1; + } + + printf("Test: Missing ready\n"); + cb.user_data = NULL; + cb.priority_higher = test_priority_higher; + cb.ready = NULL; + e = av_executor_alloc(&cb, 0); + if (e) { + printf("FAIL: av_executor_alloc with NULL ready should return NULL\n"); + av_executor_free(&e); + ret = 1; + } + + printf("Test: Missing run\n"); + cb.user_data = NULL; + cb.priority_higher = test_priority_higher; + cb.ready = test_ready; + cb.run = NULL; + e = av_executor_alloc(&cb, 0); + if (e) { + printf("FAIL: av_executor_alloc with NULL run should return NULL\n"); + av_executor_free(&e); + ret = 1; + } + + printf("Test: NULL executor pointer to av_executor_free\n"); + av_executor_free(NULL); + av_executor_free((AVExecutor **)NULL); + AVExecutor *null_e = NULL; + av_executor_free(&null_e); + + if (ret) + printf("FAIL: NULL parameter tests failed\n"); + else + printf("PASS: NULL parameter tests\n"); + + return ret; +} + +static int test_single_threaded(void) +{ + AVExecutor *e; + AVTaskCallbacks cb = {0}; + TestContext ctx = {0}; + TestTask tasks[5]; + int ret = 0; + + printf("Test: Single-threaded execution (thread_count=0)\n"); + + cb.user_data = &ctx; + cb.local_context_size = sizeof(int); + cb.priority_higher = test_priority_higher; + cb.ready = test_ready; + cb.run = test_run; + + e = av_executor_alloc(&cb, 0); + if (!e) { + printf("FAIL: av_executor_alloc(0 threads) returned NULL\n"); + return 1; + } + + for (int i = 0; i < 5; i++) { + tasks[i].id = i; + tasks[i].priority = i; + tasks[i].executed = 0; + tasks[i].should_fail_ready = 0; + tasks[i].task.next = NULL; + av_executor_execute(e, &tasks[i].task); + } + + if (atomic_load(&ctx.tasks_executed) != 5) { + printf("FAIL: Expected 5 tasks executed, got %d\n", atomic_load(&ctx.tasks_executed)); + ret = 1; + } else { + printf("PASS: All 5 tasks executed in single-threaded mode\n"); + } + + for (int i = 0; i < 5; i++) { + if (!tasks[i].executed) { + printf("FAIL: Task %d was not executed\n", i); + ret = 1; + } + } + + av_executor_execute(e, NULL); + printf("PASS: NULL task submission in single-threaded mode\n"); + + av_executor_free(&e); + + return ret; +} + +static int test_readiness_filtering(void) +{ + AVExecutor *e; + AVTaskCallbacks cb = {0}; + TestContext ctx = {0}; + TestTask tasks[3]; + int ret = 0; + + printf("Test: Readiness filtering\n"); + + ctx.fail_ready = 1; + cb.user_data = &ctx; + cb.local_context_size = 0; + cb.priority_higher = test_priority_higher; + cb.ready = test_ready; + cb.run = test_run; + + e = av_executor_alloc(&cb, 0); + if (!e) { + printf("FAIL: av_executor_alloc returned NULL\n"); + return 1; + } + + for (int i = 0; i < 3; i++) { + tasks[i].id = i; + tasks[i].priority = i; + tasks[i].executed = 0; + tasks[i].should_fail_ready = 0; + tasks[i].task.next = NULL; + av_executor_execute(e, &tasks[i].task); + } + + if (atomic_load(&ctx.tasks_executed) != 0) { + printf("FAIL: Expected 0 tasks when all are not ready, got %d\n", + atomic_load(&ctx.tasks_executed)); + ret = 1; + } else { + printf("PASS: No tasks executed when all marked not ready\n"); + } + + av_executor_free(&e); + + return ret; +} + +static int test_per_task_readiness(void) +{ + AVExecutor *e; + AVTaskCallbacks cb = {0}; + TestContext ctx = {0}; + TestTask tasks[3]; + int ret = 0; + + printf("Test: Per-task readiness\n"); + + cb.user_data = &ctx; + cb.local_context_size = 0; + cb.priority_higher = test_priority_higher; + cb.ready = test_ready; + cb.run = test_run; + + e = av_executor_alloc(&cb, 0); + if (!e) { + printf("FAIL: av_executor_alloc returned NULL\n"); + return 1; + } + + tasks[0].id = 0; + tasks[0].priority = 0; + tasks[0].executed = 0; + tasks[0].should_fail_ready = 0; + tasks[0].task.next = NULL; + + tasks[1].id = 1; + tasks[1].priority = 10; + tasks[1].executed = 0; + tasks[1].should_fail_ready = 1; + tasks[1].task.next = NULL; + + tasks[2].id = 2; + tasks[2].priority = 1; + tasks[2].executed = 0; + tasks[2].should_fail_ready = 0; + tasks[2].task.next = NULL; + + av_executor_execute(e, &tasks[0].task); + av_executor_execute(e, &tasks[1].task); + av_executor_execute(e, &tasks[2].task); + + if (tasks[1].executed) { + printf("FAIL: Task 1 should not have been executed (marked not ready)\n"); + ret = 1; + } + + if (!tasks[0].executed || !tasks[2].executed) { + printf("FAIL: Ready tasks 0 and/or 2 were not executed\n"); + ret = 1; + } + + if (ret == 0) + printf("PASS: Per-task readiness filtering works correctly\n"); + + av_executor_free(&e); + + return ret; +} + +static int test_local_context(void) +{ + AVExecutor *e; + AVTaskCallbacks cb = {0}; + TestContext ctx = {0}; + TestTask task; + int ret = 0; + + printf("Test: Local context passing\n"); + + cb.user_data = &ctx; + cb.local_context_size = sizeof(int); + cb.priority_higher = test_priority_higher; + cb.ready = test_ready; + cb.run = test_run; + + e = av_executor_alloc(&cb, 0); + if (!e) { + printf("FAIL: av_executor_alloc returned NULL\n"); + return 1; + } + + task.id = 42; + task.priority = 0; + task.executed = 0; + task.should_fail_ready = 0; + task.local_context_received = 0; + task.task.next = NULL; + + av_executor_execute(e, &task.task); + + if (!task.executed) { + printf("FAIL: Task was not executed\n"); + ret = 1; + } + + if (task.local_context_received != 42) { + printf("FAIL: Local context received %d, expected 42\n", task.local_context_received); + ret = 1; + } + + if (ret == 0) + printf("PASS: Local context passing works correctly\n"); + + av_executor_free(&e); + + return ret; +} + +static int test_multithreaded(void) +{ + AVExecutor *e; + AVTaskCallbacks cb = {0}; + TestContext ctx = {0}; + TestTask tasks[10]; + int ret = 0; + + printf("Test: Multi-threaded execution (2 threads)\n"); + + cb.user_data = &ctx; + cb.local_context_size = sizeof(int); + cb.priority_higher = test_priority_higher; + cb.ready = test_ready; + cb.run = test_run; + + e = av_executor_alloc(&cb, 2); + if (!e) { + printf("SKIP: Could not allocate executor with 2 threads (no threading support)\n"); + return 0; + } + + for (int i = 0; i < 10; i++) { + tasks[i].id = i; + tasks[i].priority = i; + tasks[i].executed = 0; + tasks[i].should_fail_ready = 0; + tasks[i].task.next = NULL; + av_executor_execute(e, &tasks[i].task); + } + + for (int wait = 0; wait < 100 && atomic_load(&ctx.tasks_executed) < 10; wait++) + av_usleep(10000); + + if (atomic_load(&ctx.tasks_executed) != 10) { + printf("FAIL: Expected 10 tasks executed, got %d\n", atomic_load(&ctx.tasks_executed)); + ret = 1; + } else { + printf("PASS: All 10 tasks executed in multi-threaded mode\n"); + } + + for (int i = 0; i < 10; i++) { + if (!tasks[i].executed) { + printf("FAIL: Task %d was not executed\n", i); + ret = 1; + } + } + + av_executor_free(&e); + + return ret; +} + +static int test_recursive_guard(void) +{ + AVExecutor *e = NULL; + AVTaskCallbacks cb = {0}; + TestContext ctx = {0}; + int ret = 0; + + printf("Test: Recursive execution guard\n"); + + cb.user_data = &ctx; + cb.local_context_size = 0; + cb.priority_higher = test_priority_higher; + cb.ready = test_ready; + cb.run = recursive_run; + + e = av_executor_alloc(&cb, 0); + if (!e) { + printf("FAIL: av_executor_alloc returned NULL\n"); + return 1; + } + + ctx.nested_executor = e; + + memset(&recursive_task_1, 0, sizeof(recursive_task_1)); + memset(&recursive_task_2, 0, sizeof(recursive_task_2)); + + recursive_task_1.id = 1; + recursive_task_1.priority = 0; + recursive_task_1.executed = 0; + recursive_task_1.should_fail_ready = 0; + recursive_task_1.task.next = NULL; + + recursive_task_2.id = 2; + recursive_task_2.priority = 0; + recursive_task_2.executed = 0; + recursive_task_2.should_fail_ready = 0; + recursive_task_2.task.next = NULL; + + av_executor_execute(e, &recursive_task_1.task); + + if (!recursive_task_1.executed) { + printf("FAIL: Task 1 was not executed\n"); + ret = 1; + } + + if (!recursive_task_2.executed) { + printf("FAIL: Task 2 was not executed (should have been queued and processed)\n"); + ret = 1; + } + + if (atomic_load(&ctx.tasks_executed) != 2) { + printf("FAIL: Expected 2 tasks executed, got %d\n", atomic_load(&ctx.tasks_executed)); + ret = 1; + } + + if (ret == 0) + printf("PASS: Recursive execution guard works correctly\n"); + + av_executor_free(&e); + + return ret; +} + +static int test_multiple_executors(void) +{ + AVExecutor *e1, *e2; + AVTaskCallbacks cb = {0}; + TestContext ctx1 = {0}, ctx2 = {0}; + TestTask t1, t2; + int ret = 0; + + printf("Test: Multiple executor instances\n"); + + cb.local_context_size = 0; + cb.priority_higher = test_priority_higher; + cb.ready = test_ready; + cb.run = test_run; + + cb.user_data = &ctx1; + e1 = av_executor_alloc(&cb, 0); + if (!e1) { + printf("FAIL: e1 alloc failed\n"); + return 1; + } + + cb.user_data = &ctx2; + e2 = av_executor_alloc(&cb, 0); + if (!e2) { + printf("FAIL: e2 alloc failed\n"); + av_executor_free(&e1); + return 1; + } + + t1.id = 100; + t1.priority = 0; + t1.executed = 0; + t1.should_fail_ready = 0; + t1.task.next = NULL; + + t2.id = 200; + t2.priority = 0; + t2.executed = 0; + t2.should_fail_ready = 0; + t2.task.next = NULL; + + av_executor_execute(e1, &t1.task); + av_executor_execute(e2, &t2.task); + + if (!t1.executed || !t2.executed) { + printf("FAIL: Tasks on separate executors not executed\n"); + ret = 1; + } + + if (atomic_load(&ctx1.tasks_executed) != 1 || atomic_load(&ctx2.tasks_executed) != 1) { + printf("FAIL: Executor task counts incorrect: e1=%d, e2=%d\n", + atomic_load(&ctx1.tasks_executed), atomic_load(&ctx2.tasks_executed)); + ret = 1; + } + + printf("PASS: Multiple executor instances are independent\n"); + + av_executor_free(&e1); + av_executor_free(&e2); + + return ret; +} + +static int test_multithreaded_empty_free(void) +{ + AVExecutor *e; + AVTaskCallbacks cb = {0}; + TestContext ctx = {0}; + int ret = 0; + + printf("Test: Multi-threaded empty free\n"); + + cb.user_data = &ctx; + cb.local_context_size = sizeof(int); + cb.priority_higher = test_priority_higher; + cb.ready = test_ready; + cb.run = test_run; + + e = av_executor_alloc(&cb, 1); + if (!e) { + printf("SKIP: Could not allocate executor with 1 thread\n"); + return 0; + } + + av_executor_free(&e); + + if (e != NULL) { + printf("FAIL: Executor not NULL after free\n"); + ret = 1; + } else { + printf("PASS: Multi-threaded empty free completed\n"); + } + + return ret; +} + +static int test_mixed_readiness_with_priority(void) +{ + AVExecutor *e; + AVTaskCallbacks cb = {0}; + TestContext ctx = {0}; + TestTask tasks[5]; + int ret = 0; + + printf("Test: Mixed readiness with priority ordering\n"); + + cb.user_data = &ctx; + cb.local_context_size = 0; + cb.priority_higher = test_priority_higher; + cb.ready = test_ready; + cb.run = test_run; + + e = av_executor_alloc(&cb, 0); + if (!e) { + printf("FAIL: av_executor_alloc returned NULL\n"); + return 1; + } + + for (int i = 0; i < 5; i++) { + tasks[i].id = i; + tasks[i].priority = i; + tasks[i].executed = 0; + tasks[i].should_fail_ready = (i % 2); + tasks[i].task.next = NULL; + av_executor_execute(e, &tasks[i].task); + } + + int ready_count = 0; + for (int i = 0; i < 5; i++) { + if (tasks[i].executed) + ready_count++; + if (tasks[i].should_fail_ready && tasks[i].executed) { + printf("FAIL: Task %d executed but was marked not ready\n", i); + ret = 1; + } + } + + if (ready_count != 3) { + printf("FAIL: Expected 3 ready tasks executed, got %d\n", ready_count); + ret = 1; + } + + if (ret == 0) + printf("PASS: Mixed readiness with priority ordering works\n"); + + av_executor_free(&e); + + return ret; +} + +int main(void) +{ + int ret = 0; + + ret |= test_null_params(); + ret |= test_single_threaded(); + ret |= test_readiness_filtering(); + ret |= test_per_task_readiness(); + ret |= test_local_context(); + ret |= test_multithreaded(); + ret |= test_recursive_guard(); + ret |= test_multiple_executors(); + ret |= test_multithreaded_empty_free(); + ret |= test_mixed_readiness_with_priority(); + + if (ret) + printf("\nSome tests FAILED\n"); + else + printf("\nAll tests PASSED\n"); + + return ret; +} diff --git a/tests/fate/libavutil.mak b/tests/fate/libavutil.mak index d69a4de863..0ff5417c53 100644 --- a/tests/fate/libavutil.mak +++ b/tests/fate/libavutil.mak @@ -79,6 +79,11 @@ fate-encryption-info: libavutil/tests/encryption_info$(EXESUF) fate-encryption-info: CMD = run libavutil/tests/encryption_info$(EXESUF) fate-encryption-info: CMP = null +FATE_LIBAVUTIL += fate-executor +fate-executor: libavutil/tests/executor$(EXESUF) +fate-executor: CMD = run libavutil/tests/executor$(EXESUF) +fate-executor: CMP = null + FATE_LIBAVUTIL += fate-eval fate-eval: libavutil/tests/eval$(EXESUF) fate-eval: CMD = run libavutil/tests/eval$(EXESUF) -- 2.52.0 >From f7b5164fb144537cb5aff19bd3269f0f983cc119 Mon Sep 17 00:00:00 2001 From: i-Amogh <[email protected]> Date: Tue, 7 Apr 2026 01:50:49 +0530 Subject: [PATCH 2/2] Added comprehensive FATE tests for libavutil/executor.c. This exercises the executor allocation, task queue management, readiness filtering, local context passing, recursive execution guard, and clean shutdown paths. The test covers both single-threaded (thread_count=0) and multi-threaded execution modes, as well as multiple independent executor instances and edge cases like nested av_executor_execute() calls during task callbacks. Signed-off-by: i-Amogh <[email protected]> --- libavutil/tests/executor.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libavutil/tests/executor.c b/libavutil/tests/executor.c index c3d04c31ce..536d04a882 100644 --- a/libavutil/tests/executor.c +++ b/libavutil/tests/executor.c @@ -1,4 +1,6 @@ /* + * Copyright (c) 2026 Amogh Kumar Sinha + * * This file is part of FFmpeg. * * FFmpeg is free software; you can redistribute it and/or -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
