Files
plezy/linux/runner/mpv/plane_render_executor_test.cc

228 lines
6.9 KiB
C++

#include "plane_render_executor.h"
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>
namespace {
int failures = 0;
void Expect(bool condition, const char* expression, int line) {
if (condition) return;
std::cerr << "line " << line << ": check failed: " << expression << '\n';
++failures;
}
#define EXPECT(condition) Expect(static_cast<bool>(condition), #condition, __LINE__)
// Each test runs against its own GLib context installed as the thread default,
// which is what the executor captures for completion delivery - exactly how
// the plugin's main context reaches it in production.
class ScopedMainContext {
public:
ScopedMainContext() : context_(g_main_context_new()) { g_main_context_push_thread_default(context_); }
~ScopedMainContext() {
g_main_context_pop_thread_default(context_);
g_main_context_unref(context_);
}
GMainContext* get() const { return context_; }
// Iterates the context until |done| holds or the deadline passes. Completions
// are GSources on this context, so this is what "the main thread ran" means.
bool IterateUntil(const std::function<bool()>& done, int timeout_ms = 5000) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);
while (!done()) {
if (std::chrono::steady_clock::now() > deadline) return false;
g_main_context_iteration(context_, FALSE);
}
return true;
}
private:
GMainContext* context_;
};
// Jobs must run off the constructing thread; completions must run on the
// constructing thread's context, carrying the job's result.
void TestJobRunsOffThreadAndCompletesOnContext() {
ScopedMainContext context;
mpv::PlaneRenderExecutor executor;
std::atomic<bool> job_ran{false};
std::thread::id job_thread;
std::atomic<bool> completed{false};
bool completion_result = false;
std::thread::id completion_thread;
EXPECT(executor.Post(
[&]() {
job_thread = std::this_thread::get_id();
job_ran = true;
return true;
},
[&](bool result) {
completion_result = result;
completion_thread = std::this_thread::get_id();
completed = true;
}));
EXPECT(context.IterateUntil([&] { return completed.load(); }));
EXPECT(job_ran.load());
EXPECT(job_thread != std::this_thread::get_id());
EXPECT(completion_thread == std::this_thread::get_id());
EXPECT(completion_result);
EXPECT(executor.ShutdownAndJoin(5000));
}
// A job's false lands in its completion untouched: the plugin's completion
// distinguishes a presented frame from a failed render/swap by exactly this.
void TestFailedJobReportsFalse() {
ScopedMainContext context;
mpv::PlaneRenderExecutor executor;
std::atomic<bool> completed{false};
bool completion_result = true;
EXPECT(executor.Post(
[] { return false; },
[&](bool result) {
completion_result = result;
completed = true;
}));
EXPECT(context.IterateUntil([&] { return completed.load(); }));
EXPECT(!completion_result);
EXPECT(executor.ShutdownAndJoin(5000));
}
// Jobs and their completions keep their queue order. The plugin only ever has
// one job in flight, but the shutdown-time EGL unbind queues behind a live
// render, and it must not overtake it.
void TestJobsRunInOrder() {
ScopedMainContext context;
mpv::PlaneRenderExecutor executor;
std::vector<int> job_order;
std::mutex order_mutex;
std::vector<int> completion_order;
std::atomic<int> completions{0};
for (int i = 0; i < 3; ++i) {
EXPECT(executor.Post(
[&, i] {
std::lock_guard<std::mutex> lock(order_mutex);
job_order.push_back(i);
return true;
},
[&, i](bool) {
completion_order.push_back(i);
++completions;
}));
}
EXPECT(context.IterateUntil([&] { return completions.load() == 3; }));
EXPECT((job_order == std::vector<int>{0, 1, 2}));
EXPECT((completion_order == std::vector<int>{0, 1, 2}));
EXPECT(executor.ShutdownAndJoin(5000));
}
// Shutdown drains what was already queued - that is what makes posting the
// EGL unbind ahead of ShutdownAndJoin sufficient - and refuses anything after.
void TestShutdownDrainsQueuedJobsAndRefusesNewOnes() {
ScopedMainContext context;
mpv::PlaneRenderExecutor executor;
// Hold the worker inside the first job so the second is provably still
// queued when shutdown begins.
std::mutex gate_mutex;
std::condition_variable gate_cv;
bool gate_open = false;
std::atomic<bool> second_ran{false};
EXPECT(executor.Post(
[&] {
std::unique_lock<std::mutex> lock(gate_mutex);
gate_cv.wait(lock, [&] { return gate_open; });
return true;
},
nullptr));
EXPECT(executor.Post(
[&] {
second_ran = true;
return true;
},
nullptr));
std::thread opener([&] {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
{
std::lock_guard<std::mutex> lock(gate_mutex);
gate_open = true;
}
gate_cv.notify_all();
});
EXPECT(executor.ShutdownAndJoin(5000));
opener.join();
EXPECT(second_ran.load());
EXPECT(!executor.Post([] { return true; }, nullptr));
}
// A worker wedged inside a driver call cannot be joined; the executor must
// abandon it within the timeout and say so, because the caller's next move -
// leak instead of free - depends on the answer. The late completion must still
// be deliverable without touching freed executor state.
void TestWedgedJobIsAbandonedNotJoined() {
ScopedMainContext context;
std::mutex gate_mutex;
std::condition_variable gate_cv;
bool gate_open = false;
std::atomic<bool> completed{false};
auto executor = std::make_unique<mpv::PlaneRenderExecutor>();
EXPECT(executor->Post(
[&] {
std::unique_lock<std::mutex> lock(gate_mutex);
gate_cv.wait(lock, [&] { return gate_open; });
return true;
},
[&](bool) { completed = true; }));
const auto start = std::chrono::steady_clock::now();
EXPECT(!executor->ShutdownAndJoin(100));
EXPECT(std::chrono::steady_clock::now() - start < std::chrono::seconds(4));
// Destruction after abandonment must not hang or double-join.
executor.reset();
// Un-wedge the abandoned thread; its completion should still arrive on the
// context, proving the thread's own context reference outlived the executor.
{
std::lock_guard<std::mutex> lock(gate_mutex);
gate_open = true;
}
gate_cv.notify_all();
EXPECT(context.IterateUntil([&] { return completed.load(); }));
}
} // namespace
int main() {
TestJobRunsOffThreadAndCompletesOnContext();
TestFailedJobReportsFalse();
TestJobsRunInOrder();
TestShutdownDrainsQueuedJobsAndRefusesNewOnes();
TestWedgedJobIsAbandonedNotJoined();
if (failures != 0) {
std::cerr << "plane_render_executor_test: " << failures << " failure(s)\n";
return 1;
}
std::cout << "plane_render_executor_test: PASS\n";
return 0;
}