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]

Reply via email to