Files
dawn/tests/test-thumbnailer.cpp
p e9c60acf83
Alpine 3.24 Success
Alpine 3.24 Flatpak Success
Arch Linux AUR Success
Clean up tests
2026-10-04 04:22:27 +02:00

398 lines
11 KiB
C++

//
// test-thumbnailer.cpp: process-wide thumbnail scheduling
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#include "qt-test.hpp"
#include "test.hpp"
#include "thumbnailer.hpp"
#include <QCoreApplication>
#include <QTimer>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <vector>
using namespace std;
using namespace chrono_literals;
namespace
{
struct WorkGate {
mutex mu;
condition_variable changed;
bool released = false;
void unblock()
{
{
lock_guard lock(mu);
released = true;
}
changed.notify_all();
}
template <typename Predicate>
bool wait(unique_lock<mutex> &lock, Predicate predicate)
{
return changed.wait_for(lock, 2s, predicate);
}
};
// Work captures the gate and whatever it reports into, so those have to
// outlive the thumbnailer's workers: declare them before it, and this after.
struct WorkRelease {
WorkGate &gate;
~WorkRelease() { gate.unblock(); }
};
} // namespace
static void
test_background_reserve()
{
WorkGate gate;
int background_started = 0;
bool visible_started = false;
dn::Thumbnailer thumbnailer(nullptr, 4);
WorkRelease release{gate};
if (thumbnailer.background_limit() != 1) {
test::fail("unexpected four-worker background limit: %zu",
thumbnailer.background_limit());
return;
}
const auto client = thumbnailer.add_client(0, {});
auto background = [&] {
unique_lock lock(gate.mu);
background_started++;
gate.changed.notify_all();
gate.changed.wait(lock, [&] { return gate.released; });
return dn::Thumbnailer::Completion{};
};
for (int i = 0; i < 2; i++) {
if (!CHECK(thumbnailer.submit(client, 0,
dn::Thumbnailer::Priority::Dimensions, background, {})))
return;
}
{
unique_lock lock(gate.mu);
if (!gate.wait(lock, [&] { return background_started == 1; })) {
test::fail("background work did not start");
return;
}
}
auto visible = [&] {
lock_guard lock(gate.mu);
visible_started = true;
gate.changed.notify_all();
return dn::Thumbnailer::Completion{};
};
if (!CHECK(thumbnailer.submit(
client, 0, dn::Thumbnailer::Priority::Interactive, visible, {})))
return;
{
unique_lock lock(gate.mu);
if (!gate.wait(lock, [&] { return visible_started; }))
test::fail("visible work was starved by background work");
if (background_started != 1)
test::fail("background admission exceeded its limit");
}
thumbnailer.remove_client(client);
}
static void
test_visible_reserve()
{
WorkGate gate;
int prefetch_started = 0;
bool visible_started = false;
dn::Thumbnailer thumbnailer(nullptr, 4);
WorkRelease release{gate};
const auto client = thumbnailer.add_client(0, {});
auto prefetch = [&] {
unique_lock lock(gate.mu);
prefetch_started++;
gate.changed.notify_all();
gate.changed.wait(lock, [&] { return gate.released; });
return dn::Thumbnailer::Completion{};
};
for (int i = 0; i < 4; i++) {
if (!CHECK(thumbnailer.submit(
client, 0, dn::Thumbnailer::Priority::Prefetch, prefetch, {})))
return;
}
{
unique_lock lock(gate.mu);
if (!gate.wait(lock, [&] { return prefetch_started == 3; })) {
test::fail("prefetch did not fill the non-visible workers");
return;
}
}
auto visible = [&] {
lock_guard lock(gate.mu);
visible_started = true;
gate.changed.notify_all();
return dn::Thumbnailer::Completion{};
};
if (!CHECK(thumbnailer.submit(
client, 0, dn::Thumbnailer::Priority::Interactive, visible, {})))
return;
{
unique_lock lock(gate.mu);
if (!gate.wait(lock, [&] { return visible_started; }) ||
prefetch_started != 3)
test::fail("prefetch consumed the visible worker reserve");
}
thumbnailer.remove_client(client);
}
static void
test_reprioritization_order()
{
WorkGate gate;
bool blocker_started = false;
vector<int> order;
dn::Thumbnailer thumbnailer(nullptr, 1);
WorkRelease release{gate};
const auto client = thumbnailer.add_client(0, {});
auto blocker = [&] {
unique_lock lock(gate.mu);
blocker_started = true;
gate.changed.notify_all();
gate.changed.wait(lock, [&] { return gate.released; });
return dn::Thumbnailer::Completion{};
};
if (!CHECK(thumbnailer.submit(
client, 0, dn::Thumbnailer::Priority::Interactive, blocker, {})))
return;
{
unique_lock lock(gate.mu);
if (!CHECK(gate.wait(lock, [&] { return blocker_started; })))
return;
}
for (int id : {1, 2}) {
auto work = [&, id] {
lock_guard lock(gate.mu);
order.push_back(id);
gate.changed.notify_all();
return dn::Thumbnailer::Completion{};
};
if (!CHECK(thumbnailer.submit(client, 0,
id == 1 ? dn::Thumbnailer::Priority::Interactive
: dn::Thumbnailer::Priority::Dimensions,
work, to_string(id))))
return;
}
if (!CHECK(thumbnailer.reprioritize(
client, 0, dn::Thumbnailer::Priority::Dimensions, "1")) ||
!CHECK(thumbnailer.reprioritize(
client, 0, dn::Thumbnailer::Priority::Interactive, "2")))
return;
gate.unblock();
{
unique_lock lock(gate.mu);
if (!CHECK(gate.wait(lock, [&] { return order.size() == 2; })))
return;
}
thumbnailer.remove_client(client);
if (order != vector<int>{2, 1})
test::fail("reprioritized order was %d,%d", order[0], order[1]);
}
// The keyed slot is what stops two jobs for one path from running at once.
// A superseded job has to give it up, or its replacement can never be
// submitted -- which is what a rescan of a changed file needs.
static void
test_cancel_frees_the_key()
{
WorkGate gate;
bool blocker_started = false;
int ran = 0;
dn::Thumbnailer thumbnailer(nullptr, 1);
WorkRelease release{gate};
const auto client = thumbnailer.add_client(0, {});
auto blocker = [&] {
unique_lock lock(gate.mu);
blocker_started = true;
gate.changed.notify_all();
gate.changed.wait(lock, [&] { return gate.released; });
return dn::Thumbnailer::Completion{};
};
if (!CHECK(thumbnailer.submit(
client, 0, dn::Thumbnailer::Priority::Interactive, blocker, {})))
return;
{
unique_lock lock(gate.mu);
if (!CHECK(gate.wait(lock, [&] { return blocker_started; })))
return;
}
auto work = [&] {
lock_guard lock(gate.mu);
ran++;
gate.changed.notify_all();
return dn::Thumbnailer::Completion{};
};
if (!CHECK(thumbnailer.submit(
client, 0, dn::Thumbnailer::Priority::Interactive, work, "a")))
return;
// Same key, and the first one is still queued behind the blocker.
if (thumbnailer.submit(
client, 0, dn::Thumbnailer::Priority::Interactive, work, "a")) {
test::fail("a duplicate key was accepted");
return;
}
if (!thumbnailer.cancel(client, "a")) {
test::fail("cancelling a queued key reported nothing to cancel");
return;
}
if (!thumbnailer.submit(
client, 0, dn::Thumbnailer::Priority::Interactive, work, "a")) {
test::fail("the replacement was still refused after cancelling");
return;
}
gate.unblock();
{
unique_lock lock(gate.mu);
if (!CHECK(gate.wait(lock, [&] { return ran > 0; })))
return;
}
thumbnailer.remove_client(client);
// The cancelled one must not have run as well.
CHECK(ran == 1);
}
static void
test_bundle_reservations()
{
dn::Thumbnailer thumbnailer(nullptr, 2);
const auto client = thumbnailer.add_client(7, {});
dn::ThumbnailSource a;
a.uri = QByteArrayLiteral("file:///a");
a.mtime = 1;
a.size = 2;
dn::ThumbnailSource b = a;
b.uri = QByteArrayLiteral("file:///b");
dn::ThumbnailSource c = a;
c.uri = QByteArrayLiteral("file:///c");
const auto first = thumbnailer.reserve_bundle(client, 7, a, 2, 4096);
const auto second = thumbnailer.reserve_bundle(client, 7, b, 2, 8192);
CHECK(first && second);
CHECK(thumbnailer.pending_bundle_limit() == 2);
CHECK(thumbnailer.pending_bundle_bytes() == 12288);
CHECK(!thumbnailer.reserve_bundle(client, 7, c, 2, 4096));
thumbnailer.cancel_bundle(first);
CHECK(thumbnailer.reserve_bundle(client, 7, c, 2, 4096));
CHECK(thumbnailer.pending_bundle_bytes() == 12288);
thumbnailer.set_epoch(client, 8);
CHECK(thumbnailer.pending_bundle_bytes() == 0);
thumbnailer.remove_client(client);
}
// Reservations are process-wide: one client holding a source refuses every
// other. A client that deferred has nothing of its own to wait on, so it has
// to be told when the slot goes away, or its placeholder stays up.
static void
test_reservation_handoff(QCoreApplication &app)
{
dn::Thumbnailer thumbnailer(nullptr, 2);
int woken = 0;
const auto owner = thumbnailer.add_client(0, {});
const auto other = thumbnailer.add_client(0, [&] {
woken++;
app.quit();
});
dn::ThumbnailSource shared;
shared.uri = QByteArrayLiteral("file:///shared");
shared.mtime = 1;
shared.size = 2;
const auto held = thumbnailer.reserve_bundle(owner, 0, shared, 2, 4096);
CHECK(held != 0);
CHECK(thumbnailer.reserve_bundle(other, 0, shared, 2, 4096) == 0);
thumbnailer.cancel_bundle(held);
QTimer::singleShot(2000, &app, [&] { app.quit(); });
app.exec();
CHECK(woken > 0);
// And the slot really is free now, not merely announced.
const auto taken = thumbnailer.reserve_bundle(other, 0, shared, 2, 4096);
CHECK(taken != 0);
thumbnailer.cancel_bundle(taken);
// The same has to hold for the window simply going away, which is how
// a browser usually gives its reservations up. Drain first, so that
// this stands on its own rather than on a pump the cancel left queued.
const auto third = thumbnailer.add_client(0, {});
CHECK(thumbnailer.reserve_bundle(third, 0, shared, 2, 4096) != 0);
QCoreApplication::processEvents();
woken = 0;
thumbnailer.remove_client(third);
QTimer::singleShot(2000, &app, [&] { app.quit(); });
app.exec();
CHECK(woken > 0);
thumbnailer.remove_client(owner);
thumbnailer.remove_client(other);
}
static void
test_activity_transitions(QCoreApplication &app)
{
WorkGate gate;
bool saw_busy = false;
bool saw_idle = false;
dn::Thumbnailer::Client client = 0;
dn::Thumbnailer thumbnailer(nullptr, 0);
WorkRelease release{gate};
client = thumbnailer.add_client(0, [&] {
if (thumbnailer.busy(client)) {
saw_busy = true;
gate.unblock();
} else if (saw_busy) {
saw_idle = true;
app.quit();
}
});
if (!thumbnailer.submit(client, 0, dn::Thumbnailer::Priority::Interactive,
[&] {
unique_lock lock(gate.mu);
gate.changed.wait(lock, [&] { return gate.released; });
return dn::Thumbnailer::Completion{};
},
{})) {
test::fail("could not submit thumbnail work");
return;
}
QTimer::singleShot(2000, &app, [&] { app.quit(); });
app.exec();
thumbnailer.remove_client(client);
CHECK(saw_busy);
CHECK(saw_idle);
}
int
main(int argc, char **argv)
{
test::Application application(argc, argv, nullptr);
return test::run({
{"background worker reserve", test_background_reserve},
{"visible worker reserve", test_visible_reserve},
{"reprioritization", test_reprioritization_order},
{"cancel frees the key", test_cancel_frees_the_key},
{"bundle reservations", test_bundle_reservations},
{"reservation handoff",
[&] { test_reservation_handoff(application.app()); }},
{"activity transitions",
[&] { test_activity_transitions(application.app()); }},
});
}