Files
plezy/shared/mpv/mpv_player_common_test.cpp
T
edde746 a56b9a3dfb Merge the deduplication and dead-code removal pass
Consolidates duplicated logic behind shared implementations — paginated
grid tabs, focus chrome, cached remote stores, sheet selection columns,
the server artifact store and a test fixture layer — and removes code
that had become unreachable. Net reduction of about 5,500 lines with no
behaviour change.

Where a fix had landed separately in code that moved into a shared
helper, the fix was re-applied inside the helper rather than left behind
in the copy that went away.
2026-07-26 19:41:23 +02:00

465 lines
16 KiB
C++

#include "mpv_player_common.h"
#ifdef NDEBUG
#undef NDEBUG
#endif
#include <atomic>
#include <cassert>
#include <chrono>
#include <string>
#include <thread>
#include <vector>
namespace {
using plezy::mpv_common::AudioOutputTransition;
using plezy::mpv_common::AudioRecoveryState;
using plezy::mpv_common::AudioReloadReason;
void TestRequestRegistry() {
plezy::mpv_common::AsyncRequestRegistry registry;
bool status_called = false;
bool property_called = false;
const auto status_id = registry.RegisterStatus([&](int error) { status_called = error == -7; });
const auto property_id = registry.RegisterProperty(
[&](int error, const std::string& value) { property_called = error == -8 && value == "value"; });
auto status = registry.TakeStatus(status_id);
auto property = registry.TakeProperty(property_id);
assert(status);
assert(property);
status(-7);
property(-8, "value");
assert(status_called);
assert(property_called);
assert(!registry.TakeStatus(status_id));
assert(!registry.TakeProperty(property_id));
registry.RegisterStatus([](int) {});
registry.RegisterProperty([](int, const std::string&) {});
auto cancelled = registry.CancelAll();
assert(cancelled.status.size() == 1);
assert(cancelled.properties.size() == 1);
}
void TestConcurrentRequestCompletion() {
for (int iteration = 0; iteration < 200; ++iteration) {
plezy::mpv_common::AsyncRequestRegistry registry;
std::atomic<int> completions{0};
const auto id = registry.RegisterStatus([&](int) { completions.fetch_add(1); });
std::atomic<bool> start{false};
std::thread taker([&]() {
while (!start.load(std::memory_order_acquire)) {
}
auto callback = registry.TakeStatus(id);
if (callback) callback(0);
});
std::thread canceller([&]() {
while (!start.load(std::memory_order_acquire)) {
}
auto cancelled = registry.CancelAll();
for (auto& callback : cancelled.status) {
callback(MPV_ERROR_UNINITIALIZED);
}
});
start.store(true, std::memory_order_release);
taker.join();
canceller.join();
assert(completions.load() == 1);
}
}
void TestSetPropertyResultContract() {
using namespace plezy::mpv_common;
assert(std::string(kSetPropertyFailedCode) == "SET_PROPERTY_FAILED");
assert(std::string(kSetPropertyNotInitializedCode) == "NOT_INITIALIZED");
assert(SetPropertyStatusSucceeded(MPV_ERROR_SUCCESS));
assert(SetPropertyStatusSucceeded(1));
assert(!SetPropertyStatusSucceeded(MPV_ERROR_UNINITIALIZED));
assert(std::string(SetPropertyErrorCode(MPV_ERROR_UNINITIALIZED)) == kSetPropertyNotInitializedCode);
constexpr int kRejectedStatuses[] = {
MPV_ERROR_INVALID_PARAMETER,
MPV_ERROR_PROPERTY_ERROR,
-1,
};
for (const int status : kRejectedStatuses) {
assert(!SetPropertyStatusSucceeded(status));
assert(std::string(SetPropertyErrorCode(status)) == kSetPropertyFailedCode);
}
constexpr int kDescribedStatuses[] = {
MPV_ERROR_INVALID_PARAMETER,
MPV_ERROR_PROPERTY_ERROR,
-1,
MPV_ERROR_UNINITIALIZED,
};
for (const int status : kDescribedStatuses) {
const std::string description = SetPropertyErrorDescription(status);
assert(!description.empty());
assert(description.size() <= kSetPropertyErrorDescriptionLimit);
assert(description.find("caller-secret") == std::string::npos);
}
}
void TestPropertyObservationRegistry() {
plezy::mpv_common::PropertyObservationRegistry registry;
const auto first = registry.Register("pause", "bool", 17);
const auto duplicate = registry.Register("pause", "string", 99);
const auto node = registry.Register("track-list", "node", 18);
assert(first.added);
assert(first.format == MPV_FORMAT_FLAG);
assert(!duplicate.added);
assert(node.added);
assert(node.format == MPV_FORMAT_NODE);
int id = 0;
assert(registry.LookupId("pause", &id));
assert(id == 17);
assert(!registry.LookupId("missing", &id));
registry.Clear();
assert(!registry.LookupId("pause", &id));
}
void TestConcurrentPropertyObservationRegistry() {
constexpr int kPropertyCount = 512;
constexpr int kClearRounds = 32;
plezy::mpv_common::PropertyObservationRegistry registry;
std::vector<std::string> names;
names.reserve(kPropertyCount);
for (int i = 0; i < kPropertyCount; ++i) {
names.push_back("property-" + std::to_string(i));
}
std::atomic<bool> start{false};
std::atomic<bool> writer_done{false};
std::thread writer([&]() {
while (!start.load(std::memory_order_acquire)) {
}
for (int round = 0; round < kClearRounds; ++round) {
for (int i = 0; i < kPropertyCount; ++i) {
registry.Register(names[i], "int64", 1000 + i);
}
}
writer_done.store(true, std::memory_order_release);
});
std::thread reader([&]() {
while (!start.load(std::memory_order_acquire)) {
}
while (!writer_done.load(std::memory_order_acquire)) {
for (int i = 0; i < kPropertyCount; ++i) {
int id = 0;
if (registry.LookupId(names[i], &id)) {
assert(id == 1000 + i);
}
}
}
});
std::thread clearer([&]() {
while (!start.load(std::memory_order_acquire)) {
}
for (int round = 0; round < kClearRounds; ++round) {
registry.Clear();
std::this_thread::yield();
}
});
start.store(true, std::memory_order_release);
writer.join();
reader.join();
clearer.join();
registry.Clear();
for (int i = 0; i < kPropertyCount; ++i) {
const auto request = registry.Register(names[i], "int64", 1000 + i);
assert(request.added);
}
for (int i = 0; i < kPropertyCount; ++i) {
int id = 0;
assert(registry.LookupId(names[i], &id));
assert(id == 1000 + i);
}
}
void TestResumeRecoverySchedule() {
AudioRecoveryState state;
const auto start = AudioRecoveryState::Clock::time_point{};
state.SetFileLoaded(true);
state.RequestResume();
assert(state.NextReload(start).reason == AudioReloadReason::kNone);
assert(state.HasPendingWork());
assert(state.NextReload(start + std::chrono::milliseconds(1499)).reason == AudioReloadReason::kNone);
const auto first = state.NextReload(start + std::chrono::milliseconds(1500));
assert(first.reason == AudioReloadReason::kResume);
assert(first.attempt == 1);
assert(!first.exhausted);
assert(state.CompleteReload(first.request_generation));
const auto second = state.NextReload(start + std::chrono::milliseconds(6000));
assert(second.reason == AudioReloadReason::kResume);
assert(second.attempt == 2);
assert(state.CompleteReload(second.request_generation));
assert(!state.HasPendingWork());
}
void TestConcurrentAudioRecoveryState() {
AudioRecoveryState state;
const auto start = AudioRecoveryState::Clock::time_point{};
state.SetFileLoaded(true);
std::atomic<bool> begin{false};
std::thread resume([&]() {
while (!begin.load(std::memory_order_acquire)) {
}
for (int i = 0; i < 1000; ++i) state.RequestResume();
});
std::thread device([&]() {
while (!begin.load(std::memory_order_acquire)) {
}
for (int i = 0; i < 1000; ++i) {
state.SetCurrentAudioOutputNull(true, start);
state.OnAudioDeviceListChanged(start);
}
});
std::thread timer([&]() {
while (!begin.load(std::memory_order_acquire)) {
}
for (int i = 0; i < 1000; ++i) {
const auto action = state.NextReload(start + std::chrono::hours(1));
if (action.reason != AudioReloadReason::kNone) {
state.CompleteReload(action.request_generation);
}
}
});
begin.store(true, std::memory_order_release);
resume.join();
device.join();
timer.join();
state.SetFileLoaded(false);
assert(!state.HasPendingWork());
}
void TestFileBoundaryRestartsNullRecoveryOnlyAfterLoad() {
AudioRecoveryState state;
const auto start = AudioRecoveryState::Clock::time_point{};
state.SetFileLoaded(true, start);
assert(state.SetCurrentAudioOutputNull(true, start) == AudioOutputTransition::kFellBackToNull);
assert(state.HasPendingWork());
state.SetFileLoaded(false, start + std::chrono::milliseconds(100));
assert(!state.HasPendingWork());
assert(!state.OnAudioDeviceListChanged(start + std::chrono::milliseconds(200)));
state.SetFileLoaded(true, start + std::chrono::milliseconds(300));
assert(state.HasPendingWork());
assert(state.NextReload(start + std::chrono::milliseconds(799)).reason == AudioReloadReason::kNone);
const auto retry = state.NextReload(start + std::chrono::milliseconds(800));
assert(retry.reason == AudioReloadReason::kNullFallback);
assert(retry.attempt == 1);
}
void TestNullFallbackRecoverySchedule() {
AudioRecoveryState state;
const auto start = AudioRecoveryState::Clock::time_point{};
state.SetFileLoaded(true);
assert(state.SetCurrentAudioOutputNull(true, start) == AudioOutputTransition::kFellBackToNull);
auto action = state.NextReload(start + std::chrono::milliseconds(500));
assert(action.reason == AudioReloadReason::kNullFallback);
assert(action.attempt == 1);
assert(state.CompleteReload(action.request_generation));
action = state.NextReload(start + std::chrono::milliseconds(1000));
assert(action.reason == AudioReloadReason::kNullFallback);
assert(action.attempt == 2);
assert(state.CompleteReload(action.request_generation));
action = state.NextReload(start + std::chrono::milliseconds(2000));
assert(action.reason == AudioReloadReason::kNullFallback);
assert(action.attempt == 3);
assert(state.CompleteReload(action.request_generation));
action = state.NextReload(start + std::chrono::milliseconds(4000));
assert(action.reason == AudioReloadReason::kNullFallback);
assert(action.attempt == 4);
assert(state.CompleteReload(action.request_generation));
action = state.NextReload(start + std::chrono::milliseconds(8000));
assert(action.reason == AudioReloadReason::kNullFallback);
assert(action.attempt == 5);
assert(action.exhausted);
assert(state.CompleteReload(action.request_generation));
assert(!state.HasPendingWork());
assert(state.OnAudioDeviceListChanged(start + std::chrono::milliseconds(9000)));
action = state.NextReload(start + std::chrono::milliseconds(9250));
assert(action.reason == AudioReloadReason::kNullFallback);
assert(action.attempt == 1);
assert(state.CompleteReload(action.request_generation));
assert(
state.SetCurrentAudioOutputNull(false, start + std::chrono::milliseconds(9300)) ==
AudioOutputTransition::kRecovered);
assert(!state.HasPendingWork());
}
void TestUnloadedResumeIsConsumed() {
AudioRecoveryState state;
const auto start = AudioRecoveryState::Clock::time_point{};
state.RequestResume();
assert(!state.HasPendingWork());
assert(state.NextReload(start + std::chrono::hours(1)).reason == AudioReloadReason::kNone);
state.SetFileLoaded(true, start);
assert(!state.HasPendingWork());
}
void TestStaleReloadCompletionCannotClearCurrentRequest() {
AudioRecoveryState state;
const auto start = AudioRecoveryState::Clock::time_point{};
state.SetFileLoaded(true, start);
assert(state.SetCurrentAudioOutputNull(true, start) == AudioOutputTransition::kFellBackToNull);
const auto old_request = state.NextReload(start + std::chrono::milliseconds(500));
assert(old_request.reason == AudioReloadReason::kNullFallback);
state.SetFileLoaded(false, start + std::chrono::milliseconds(600));
state.SetFileLoaded(true, start + std::chrono::milliseconds(700));
const auto current_request = state.NextReload(start + std::chrono::milliseconds(1200));
assert(current_request.reason == AudioReloadReason::kNullFallback);
assert(current_request.request_generation != old_request.request_generation);
assert(!state.CompleteReload(old_request.request_generation));
assert(state.NextReload(start + std::chrono::hours(1)).reason == AudioReloadReason::kNone);
assert(state.CompleteReload(current_request.request_generation));
}
// Renders the shared node walk into text so its bounds can be asserted on
// every platform, without a platform value type in the way.
struct TextNodeBuilder {
using Value = std::string;
using ListBuilder = std::string;
using MapBuilder = std::string;
static Value Null() { return "null"; }
static Value Bool(bool value) { return value ? "true" : "false"; }
static Value Int(int64_t value) { return std::to_string(value); }
static Value Double(double value) { return std::to_string(value); }
static Value String(const char* value, size_t length) { return "'" + std::string(value, length) + "'"; }
static ListBuilder NewList() { return std::string("["); }
static void Append(ListBuilder& list, Value value) { list += value + ","; }
static Value FinishList(ListBuilder list) { return list + "]"; }
static MapBuilder NewMap() { return std::string("{"); }
static void Insert(MapBuilder& map, const char* key, size_t key_length, Value value) {
map += std::string(key, key_length) + ":" + value + ",";
}
static Value FinishMap(MapBuilder map) { return map + "}"; }
static void AbandonMap(MapBuilder& map) { map += "<abandoned>"; }
};
void TestNodeConversionBounds() {
using plezy::mpv_common::ConvertNode;
using plezy::mpv_common::NodeConversionBudget;
char value[] = "hello";
mpv_node text{};
text.format = MPV_FORMAT_STRING;
text.u.string = value;
assert(ConvertNode<TextNodeBuilder>(&text) == "'hello'");
// Missing storage is never trusted: no node, no string, no list.
assert(ConvertNode<TextNodeBuilder>(nullptr) == "null");
text.u.string = nullptr;
assert(ConvertNode<TextNodeBuilder>(&text) == "null");
mpv_node array{};
array.format = MPV_FORMAT_NODE_ARRAY;
array.u.list = nullptr;
assert(ConvertNode<TextNodeBuilder>(&array) == "null");
// Neither a negative nor an implausible length reaches the builder.
mpv_node entry{};
entry.format = MPV_FORMAT_INT64;
entry.u.int64 = 7;
mpv_node_list negative{-1, &entry, nullptr};
array.u.list = &negative;
assert(ConvertNode<TextNodeBuilder>(&array) == "null");
mpv_node_list oversized{plezy::mpv_common::kMaxNodeEntries + 1, &entry, nullptr};
array.u.list = &oversized;
assert(ConvertNode<TextNodeBuilder>(&array) == "null");
mpv_node_list single{1, &entry, nullptr};
array.u.list = &single;
assert(ConvertNode<TextNodeBuilder>(&array) == "[7,]");
// A map with a null key is voided rather than half-converted.
char* missing_key[] = {nullptr};
mpv_node_list keyless{1, &entry, missing_key};
mpv_node map{};
map.format = MPV_FORMAT_NODE_MAP;
map.u.list = &keyless;
assert(ConvertNode<TextNodeBuilder>(&map) == "null");
// Depth, entry, and byte budgets each stop the walk.
std::vector<mpv_node> chain(plezy::mpv_common::kMaxNodeDepth + 1);
std::vector<mpv_node_list> links(chain.size());
chain.back() = entry;
for (size_t i = chain.size() - 1; i > 0; --i) {
links[i - 1] = mpv_node_list{1, &chain[i], nullptr};
chain[i - 1].format = MPV_FORMAT_NODE_ARRAY;
chain[i - 1].u.list = &links[i - 1];
}
assert(ConvertNode<TextNodeBuilder>(&chain[0]).find('7') == std::string::npos);
NodeConversionBudget entries{2, 1024};
assert(ConvertNode<TextNodeBuilder>(&array, 0, &entries) == "[7,]");
assert(entries.remaining_entries == 0);
assert(ConvertNode<TextNodeBuilder>(&array, 0, &entries) == "null");
NodeConversionBudget bytes{8, 4};
text.u.string = value;
assert(ConvertNode<TextNodeBuilder>(&text, 0, &bytes) == "null");
assert(bytes.remaining_bytes == 4);
}
void TestHdrHelpers() {
assert(plezy::mpv_common::ParseEnabledFlag("yes"));
assert(plezy::mpv_common::ParseEnabledFlag("true"));
assert(plezy::mpv_common::ParseEnabledFlag("1"));
assert(!plezy::mpv_common::ParseEnabledFlag("no"));
assert(std::string(plezy::mpv_common::TargetColorspaceHint(true)) == "auto");
assert(std::string(plezy::mpv_common::TargetColorspaceHint(false)) == "no");
}
} // namespace
int main() {
TestRequestRegistry();
TestConcurrentRequestCompletion();
TestSetPropertyResultContract();
TestPropertyObservationRegistry();
TestConcurrentPropertyObservationRegistry();
TestResumeRecoverySchedule();
TestConcurrentAudioRecoveryState();
TestNullFallbackRecoverySchedule();
TestFileBoundaryRestartsNullRecoveryOnlyAfterLoad();
TestUnloadedResumeIsConsumed();
TestStaleReloadCompletionCannotClearCurrentRequest();
TestNodeConversionBounds();
TestHdrHelpers();
return 0;
}