Continues the build order past Options/Scheduler/Speed Limiter/Batch/ Grabber/tray. - DropTargetWidget: frameless always-on-top drop target (docs/03-gui-spec.md §5), position persisted, accepts a dropped http(s) URL or link text and opens FileInfoDialog directly (skipping Add URL, since the URL is already known). Shown/hidden from general.showDropTarget, live via event.settings.changed, same pattern MainWindow already used for general.minimizeToTray. - Clipboard, explicit path #2 (docs/06-risks-and-spikes.md R2): GlobalShortcut wraps org.freedesktop.portal.GlobalShortcuts (CreateSession -> BindShortcuts -> Activated), triggering the same Add URL flow. Guarded end-to-end on `if(TARGET Qt6::DBus)` / VELOX_GUI_HAVE_DBUS so a build without the component degrades to "feature skipped," not broken (gui/docs/pkg-qa-requests-m1.md R4). Best-effort by design per the risk doc: fails silent, never advertised. Verified live against the real portal (a real Wayland session, not just offscreen): `CreateSession` refuses every caller with "An app id is required" — reproduced identically via a bare `busctl` call with no Qt involved at all, so this is the portal requiring a sandboxed caller identity, not something fixable from an unconfined process. Recorded as a partial Spike S2 answer in docs/06-risks-and-spikes.md: this explicit path likely doesn't work for Velox as a traditionally-packaged app on stock GNOME, only if/when it ships confined. Also fixed a real leak this verification caught: QDBusInterface's introspection cache reads as a LeakSanitizer leak the first time anything touches D-Bus (tst_rtl went red under ASan) — switched to QDBusMessage::createMethodCall, which needs no introspection. - Theming (docs/03-gui-spec.md §7): gui/resources/qss/{idm-like,dark}.qss, each with a documented palette block up top (QSS itself has no variable syntax), applied by ThemeManager and kept live via QStyleHints::colorSchemeChanged. util/Theme.hpp gives the handful of inline C++ styles (status dot, offline banner, the eleven identical error-label styles across dialogs) named constants instead of a twelfth copy of the same hex. - UiThreadWatchdog: the M1 DoD's 200 ms debug-build watchdog. A background std::thread pings the UI thread every 50 ms via a queued invokeMethod and warns once (not per-poll) if a ping goes unanswered past 200 ms; no QThread, no Qt event loop of its own, so the watchdog itself can never be what blocks the thread it watches. No-op in a release build. Proven both ways in tst_uithreadwatchdog: fires on a genuinely blocked UI thread (synchronous sleep, no processEvents) and stays silent on a responsive one. Full non-conformance suite (55 tests across every lane, `ctest -LE conformance`) passes clean at this point, including the whole gui label under ASan+UBSan. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
90 lines
2.6 KiB
C++
90 lines
2.6 KiB
C++
// UiThreadWatchdog unit tests. Lane GUI.
|
|
//
|
|
// Red when a genuinely blocked UI thread (a synchronous sleep with no processEvents in
|
|
// between — the exact shape of the bug this exists to catch) stops producing a warning,
|
|
// or a responsive one starts producing a false-positive one.
|
|
|
|
#include <QMutex>
|
|
#include <QMutexLocker>
|
|
#include <QThread>
|
|
#include <QtTest>
|
|
|
|
#include "util/UiThreadWatchdog.hpp"
|
|
|
|
using velox::gui::UiThreadWatchdog;
|
|
|
|
namespace {
|
|
|
|
QMutex g_mutex;
|
|
QString g_lastWarning;
|
|
QtMessageHandler g_prevHandler = nullptr;
|
|
|
|
// Qt's own message handler is process-global and can run on any thread — the watchdog's
|
|
// warning comes from its background thread while this test's main thread is deliberately
|
|
// blocked, so this needs real synchronization, not just a plain global (this test runs
|
|
// under the `tsan` preset too).
|
|
void captureHandler(QtMsgType type, const QMessageLogContext &ctx, const QString &msg) {
|
|
if (type == QtWarningMsg) {
|
|
QMutexLocker locker(&g_mutex);
|
|
g_lastWarning = msg;
|
|
}
|
|
if (g_prevHandler) {
|
|
g_prevHandler(type, ctx, msg);
|
|
}
|
|
}
|
|
|
|
QString lastWarning() {
|
|
QMutexLocker locker(&g_mutex);
|
|
return g_lastWarning;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
class TstUiThreadWatchdog : public QObject {
|
|
Q_OBJECT
|
|
|
|
private slots:
|
|
void init();
|
|
void cleanup();
|
|
|
|
void firesOnABlockedUiThread();
|
|
void staysQuietWhenResponsive();
|
|
};
|
|
|
|
void TstUiThreadWatchdog::init() {
|
|
QMutexLocker locker(&g_mutex);
|
|
g_lastWarning.clear();
|
|
g_prevHandler = qInstallMessageHandler(captureHandler);
|
|
}
|
|
|
|
void TstUiThreadWatchdog::cleanup() {
|
|
qInstallMessageHandler(g_prevHandler);
|
|
}
|
|
|
|
void TstUiThreadWatchdog::firesOnABlockedUiThread() {
|
|
UiThreadWatchdog wd;
|
|
wd.start();
|
|
|
|
// Block this thread (the watchdog's "UI thread" here) synchronously and well past
|
|
// the 200 ms budget — no processEvents at all, exactly what a real stall looks like
|
|
// and exactly what this exists to catch.
|
|
QThread::msleep(500);
|
|
|
|
// Let the event loop run so the queued ack the watchdog sent before the sleep started
|
|
// finally lands (harmless — the warning it's checking for already fired mid-sleep,
|
|
// from the watchdog's own background thread).
|
|
QTest::qWait(150);
|
|
|
|
QVERIFY2(lastWarning().contains(QStringLiteral("blocking")), qUtf8Printable(lastWarning()));
|
|
}
|
|
|
|
void TstUiThreadWatchdog::staysQuietWhenResponsive() {
|
|
UiThreadWatchdog wd;
|
|
wd.start();
|
|
QTest::qWait(400); // event loop stays responsive throughout — well past the budget
|
|
QVERIFY(lastWarning().isEmpty());
|
|
}
|
|
|
|
QTEST_MAIN(TstUiThreadWatchdog)
|
|
#include "tst_uithreadwatchdog.moc"
|