initial commit

This commit is contained in:
2026-09-21 13:41:40 +09:00
commit 855c7328df
411 changed files with 85352 additions and 0 deletions
+199
View File
@@ -0,0 +1,199 @@
#include "app/controller.h"
#include "app/pdf_canvas.h"
#include "web/web_profile.h"
#include <QCryptographicHash>
#include <QElapsedTimer>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QPointer>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QSaveFile>
#include <QScreen>
#include <QScopeGuard>
#include <QTemporaryDir>
#include <QtTest>
#include <QtWebEngineQuick>
using namespace docview;
static QByteArray digest(const QString &path) {
QFile file(path); if (!file.open(QIODevice::ReadOnly)) return {};
QCryptographicHash hash(QCryptographicHash::Sha256);
return hash.addData(&file) ? hash.result() : QByteArray{};
}
class HeavyPdfTest : public QObject {
Q_OBJECT
private slots:
void realHeavyPagesRemainResponsiveAndCancel() {
const auto heavy = qEnvironmentVariable("DOCVIEW_HEAVY_PDF");
const auto light = qEnvironmentVariable("DOCVIEW_LIGHT_PDF");
const auto output = qEnvironmentVariable("DOCVIEW_HEAVY_OUTPUT");
QVERIFY(!heavy.isEmpty() && !light.isEmpty() && !output.isEmpty());
const auto beforeHeavy = digest(heavy), beforeLight = digest(light);
QVERIFY(!beforeHeavy.isEmpty() && !beforeLight.isEmpty());
QTemporaryDir isolated; QVERIFY(isolated.isValid());
const auto config = isolated.filePath("config.toml");
QFile configuration(config); QVERIFY(configuration.open(QIODevice::WriteOnly));
configuration.write("schema_version=1\n[privacy]\nremember_position=false\nrecent_documents=0\n[performance]\nprefetch_pages=0\n");
configuration.close();
Controller reader;
reader.initialize(config, false, {config, isolated.filePath("state.json"),
isolated.filePath("cache"), isolated.filePath("logs")});
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("reader", &reader);
engine.load(QUrl("qrc:/qml/Main.qml"));
QVERIFY(!engine.rootObjects().isEmpty());
auto *window = qobject_cast<QQuickWindow *>(engine.rootObjects().first());
QVERIFY(window); reader.attachWindow(window);
window->resize(qMin(1100, window->screen()->availableGeometry().width()),
qMin(760, window->screen()->availableGeometry().height()));
auto *canvas = window->findChild<PdfCanvas *>(); QVERIFY(canvas);
QVERIFY(QTest::qWaitForWindowExposed(window, 10000)); window->requestActivate();
QTRY_COMPARE_WITH_TIMEOUT(QGuiApplication::focusWindow(), window, 5000);
QElapsedTimer clock; clock.start(); qint64 previous = 0, longestGap = 0; int ticks = 0, peakPressure = 0;
QTimer heartbeat;
connect(&heartbeat, &QTimer::timeout, this, [&] {
const auto now = clock.elapsed(); longestGap = qMax(longestGap, now - previous);
peakPressure = qMax(peakPressure, canvas->memoryPressureLevel());
previous = now; ++ticks;
});
heartbeat.start(10);
QJsonArray phases; bool success = false;
const auto save = qScopeGuard([&] {
heartbeat.stop();
QJsonObject result{{"success", success}, {"phases", phases},
{"elapsedMs", clock.elapsed()}, {"heartbeatTicks", ticks}, {"longestHeartbeatGapMs", longestGap},
{"maximumMemoryPressureLevel", peakPressure},
{"qtPlatform", QGuiApplication::platformName()}, {"qtVersion", qVersion()},
{"quickBackend", qEnvironmentVariable("QT_QUICK_BACKEND")},
{"windowWidth", window->width()}, {"windowHeight", window->height()},
{"devicePixelRatio", window->devicePixelRatio()},
{"heavySha256", QString::fromLatin1(beforeHeavy.toHex())},
{"lightSha256", QString::fromLatin1(beforeLight.toHex())},
{"heavyUnchanged", digest(heavy) == beforeHeavy}, {"lightUnchanged", digest(light) == beforeLight},
{"scope", "Production Controller/QML/Canvas with canonical sandboxed workers; GUI timer gaps and dispatched RPCs, not physical scanout or direct instrumentation inside PDFium"}};
QSaveFile file(output + "/measurement.json");
if (file.open(QIODevice::WriteOnly)) { file.write(QJsonDocument(result).toJson()); file.commit(); }
});
const auto ready = [&] { return reader.state() == "Ready" && canvas->viewportReady(); };
const auto sessionFor = [&](const QString &path) -> Session * {
for (auto *child : reader.children()) {
auto *session = dynamic_cast<Session *>(child);
if (session && session->source == path) return session;
}
return nullptr;
};
const auto sampleMarker = [&](int page) {
const QImage capture = window->grabWindow();
const QPointF logical = canvas->mapToScene(canvas->pageToCanvas(page, {46, 796}));
const QPoint point(qRound(logical.x() * window->devicePixelRatio()),
qRound(logical.y() * window->devicePixelRatio()));
return capture.rect().contains(point) ? capture.pixelColor(point) : QColor();
};
const auto centralPixels = [&](int page) {
const QImage capture = window->grabWindow();
const auto point = [&](QPointF pdf) {
return canvas->mapToScene(canvas->pageToCanvas(page, pdf)) * window->devicePixelRatio();
};
const QRect area = QRectF(point({80, 220}), point({480, 620})).normalized()
.toAlignedRect().intersected(capture.rect());
int dark = 0, paper = 0;
for (int y = area.top(); y <= area.bottom(); y += 2)
for (int x = area.left(); x <= area.right(); x += 2) {
const auto color = capture.pixelColor(x, y);
dark += color.lightness() < 80;
paper += color.red() > 220 && color.green() > 220 && color.blue() > 220;
}
return QPair{dark, paper};
};
// Complete both expensive pages before testing interruption: a rejected
// document or empty canvas cannot count as a successful heavy workload.
auto started = clock.elapsed(); reader.openPath(heavy);
QTRY_VERIFY_WITH_TIMEOUT(ready(), 30000);
QCOMPARE(reader.pages().size(), 2); QCOMPARE(canvas->currentPage(), 0);
canvas->setPrefetchPages(0); canvas->fitPage();
QTRY_VERIFY_WITH_TIMEOUT(canvas->viewportReady(), 30000);
const auto blue = sampleMarker(0);
QVERIFY2(blue.isValid() && blue.red() < 50 && blue.green() > 50 && blue.green() < 100 && blue.blue() > 130,
"High-resolution page did not show its original blue marker");
const auto rasterPixels = centralPixels(0);
QVERIFY2(rasterPixels.first > 1000 && rasterPixels.second > 1000,
"Decoded scan pixels and paper were not both visible in the central image area");
QVERIFY(window->grabWindow().save(output + "/raster.png"));
phases.append(QJsonObject{{"name", "complete-high-resolution-raster"}, {"elapsedMs", clock.elapsed() - started},
{"pageIndex", 0}, {"originalMarkerVisible", true},
{"darkRasterSamples", rasterPixels.first}, {"paperSamples", rasterPixels.second}});
started = clock.elapsed(); reader.execute("nav.page", {{"page", 2}});
QTRY_VERIFY_WITH_TIMEOUT(canvas->currentPage() == 1 && ready(), 30000);
const auto magenta = sampleMarker(1);
QVERIFY2(magenta.isValid() && magenta.red() > 180 && magenta.green() < 50 && magenta.blue() > 120,
"Transparent-vector page did not show its original magenta marker");
const auto vectorPixels = centralPixels(1);
QVERIFY2(vectorPixels.first > 1000 && vectorPixels.second == 0,
"Dense multiplied vector paths were absent from the central page area");
QVERIFY(window->grabWindow().save(output + "/vector.png"));
phases.append(QJsonObject{{"name", "complete-transparent-vectors"}, {"elapsedMs", clock.elapsed() - started},
{"pageIndex", 1}, {"originalMarkerVisible", true},
{"darkVectorSamples", vectorPixels.first}});
// Cancel after the real protected worker has accepted an open RPC.
reader.openPath(light); QTRY_VERIFY_WITH_TIMEOUT(ready(), 15000);
const auto retainedToken = reader.webToken();
reader.openPath(heavy); QCOMPARE(reader.state(), "Opening");
QTRY_VERIFY_WITH_TIMEOUT(sessionFor(heavy) && sessionFor(heavy)->worker
&& sessionFor(heavy)->worker->activeRequestCount("open") == 1, 10000);
QPointer<Session> cancelled = sessionFor(heavy);
started = clock.elapsed(); QTest::keyClick(window, Qt::Key_Escape);
QCOMPARE(reader.webToken(), retainedToken); QCOMPARE(reader.state(), "Ready");
QTRY_VERIFY_WITH_TIMEOUT(cancelled.isNull(), 5000);
QTRY_VERIFY_WITH_TIMEOUT(canvas->viewportReady(), 15000);
phases.append(QJsonObject{{"name", "escape-during-open-rpc"}, {"elapsedMs", clock.elapsed() - started},
{"activeOpenBeforeCancel", true}, {"retainedOldDocument", true}, {"oldSessionDestroyed", true}});
// Zoom invalidates old vector tiles. Replace while at least one new
// render RPC is outstanding; no fake sleep worker is substituted.
reader.openPath(heavy); QTRY_VERIFY_WITH_TIMEOUT(ready(), 30000);
reader.execute("nav.page", {{"page", 2}});
QTRY_VERIFY_WITH_TIMEOUT(canvas->currentPage() == 1 && ready(), 30000);
QPointer<Session> replaced = sessionFor(heavy); QVERIFY(replaced);
canvas->setZoom(350);
QTRY_VERIFY_WITH_TIMEOUT(reader.benchmarkSnapshot().value("activeRenders").toInt() == 1
&& !canvas->viewportReady(), 10000);
const auto outstanding = canvas->pendingRenderCount();
const auto replacedToken = replaced->token;
started = clock.elapsed(); reader.openPath(light);
QTRY_VERIFY_WITH_TIMEOUT(reader.webToken() != replacedToken && ready(), 15000);
QTRY_VERIFY_WITH_TIMEOUT(replaced.isNull(), 5000);
const auto lightToken = reader.webToken();
QTest::qWait(300); // Allow any already-delivered obsolete Qt callbacks to run.
QCOMPARE(reader.webToken(), lightToken); QCOMPARE(canvas->currentPage(), 0);
QCOMPARE(reader.title(), QFileInfo(light).fileName()); QVERIFY(ready());
QVERIFY(window->grabWindow().save(output + "/replacement.png"));
phases.append(QJsonObject{{"name", "replace-during-vector-render"}, {"elapsedMs", clock.elapsed() - started},
{"activeRenderBeforeReplace", true}, {"pendingRendersBeforeReplace", qint64(outstanding)},
{"oldSessionDestroyed", true}, {"replacementStableAfter300ms", true}});
reader.closeDocument(); QCOMPARE(reader.state(), "Empty"); QCOMPARE(canvas->cacheCostBytes(), 0);
QVERIFY2(ticks > 0 && longestGap < 1500, qPrintable(QString("GUI heartbeat gap: %1 ms").arg(longestGap)));
QCOMPARE(peakPressure, 0);
QCOMPARE(digest(heavy), beforeHeavy); QCOMPARE(digest(light), beforeLight);
success = true;
}
};
int main(int argc, char **argv) {
QTemporaryDir isolated; if (!isolated.isValid()) return 2;
qputenv("XDG_CONFIG_HOME", isolated.filePath("config").toUtf8());
qputenv("XDG_STATE_HOME", isolated.filePath("state").toUtf8());
qputenv("XDG_CACHE_HOME", isolated.filePath("cache").toUtf8());
registerDocumentScheme(); QtWebEngineQuick::initialize();
QGuiApplication app(argc, argv); app.setQuitOnLastWindowClosed(false);
app.setApplicationName("DocView heavy PDF validation"); app.setOrganizationName("DocView");
qmlRegisterType<PdfCanvas>("DocView", 1, 0, "PdfCanvas");
HeavyPdfTest test; return QTest::qExec(&test, argc, argv);
}
#include "test_heavy_pdf.moc"