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
+37
View File
@@ -0,0 +1,37 @@
cmake_minimum_required(VERSION 3.24)
project(DocViewHeavyPdfValidation LANGUAGES CXX)
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
message(FATAL_ERROR "This supplemental harness currently validates Linux only")
endif()
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
get_filename_component(DOCVIEW_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
set(DOCVIEW_CANONICAL_BUILD "${DOCVIEW_ROOT}/build" CACHE PATH "Existing tested Release build")
find_package(Qt6 6.11.2 EXACT REQUIRED COMPONENTS Core Gui Network Qml Quick QuickControls2 WebEngineQuick Test)
find_package(PkgConfig REQUIRED)
pkg_check_modules(FONTCONFIG REQUIRED IMPORTED_TARGET fontconfig)
pkg_check_modules(SECCOMP REQUIRED IMPORTED_TARGET libseccomp)
pkg_check_modules(TOML REQUIRED IMPORTED_TARGET tomlplusplus)
foreach(name docview_core docview_common docview_font_broker)
if(NOT EXISTS "${DOCVIEW_CANONICAL_BUILD}/lib${name}.a")
message(FATAL_ERROR "Build the canonical DocView targets first: ${name}")
endif()
add_library(${name} STATIC IMPORTED)
set_target_properties(${name} PROPERTIES IMPORTED_LOCATION "${DOCVIEW_CANONICAL_BUILD}/lib${name}.a")
endforeach()
qt_add_executable(test_heavy_pdf test_heavy_pdf.cpp
"${DOCVIEW_ROOT}/src/app/controller.cpp"
"${DOCVIEW_ROOT}/src/app/pdf_canvas.cpp"
"${DOCVIEW_ROOT}/src/web/web_profile.cpp"
"${DOCVIEW_ROOT}/resources.qrc")
target_include_directories(test_heavy_pdf PRIVATE "${DOCVIEW_ROOT}/src")
target_link_libraries(test_heavy_pdf PRIVATE docview_core docview_font_broker docview_common
PkgConfig::FONTCONFIG PkgConfig::SECCOMP PkgConfig::TOML Qt6::Core Qt6::Gui Qt6::Network Qt6::Quick
Qt6::Qml Qt6::QuickControls2 Qt6::WebEngineQuick Qt6::Test)
# Use exactly the recorded canonical workers; do not rebuild parser binaries.
foreach(worker docview-pdf-worker docview-archive-worker)
add_custom_command(TARGET test_heavy_pdf POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E create_symlink
"${DOCVIEW_CANONICAL_BUILD}/${worker}" "$<TARGET_FILE_DIR:test_heavy_pdf>/${worker}")
endforeach()
+21
View File
@@ -0,0 +1,21 @@
# 高解像度・高密度PDFのGUI検証
本番Controller・QML・PdfCanvasと既存Releaseビルドのcore/common/font brokerライブラリを使う、Linux専用の追加試験。PDF/Archive workerは既存の実行ファイルへ接続し、模擬workerや保護無効化を使わない。通常のCTestや製品バイナリーを変更せず、別の実行ファイルとしてビルドする。
```sh
cmake -S tests/heavy_pdf -B build-heavy-pdf -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DDOCVIEW_CANONICAL_BUILD="$PWD/build"
cmake --build build-heavy-pdf --parallel 4
python3 tests/heavy_pdf/run.py --output tests/results/heavy-pdf/new-run
```
既存ビルドと同じQt 6.11.2・ツールチェーン・依存ライブラリを使う。Qt SDKを別配置している環境では`CMAKE_PREFIX_PATH`、`PATH`、`LD_LIBRARY_PATH`、`PKG_CONFIG_PATH`も既存ビルドに合わせる。別のビルド先はrunnerの`--binary`と`--canonical-build`で明示する。既存の結果フォルダーは上書きしない。
資料は2ページ、595×842 pt。1ページ目は4,960×7,016 RGB画像(展開後104,398,080 bytes、A4の600dpi相当)、2ページ目はalpha 0.16・Multiplyで重ねる60,000個のベクターパス。生成元はオリジナルの決定的なアルゴリズムで、実スキャンや書籍ではない。Helveticaの標準フォント参照は非埋込であり、OSの代替を使用する。
qpdfによる画像streamの独立復号でサイズと全画素hashを照合し、Popplerの`pdfinfo`/`pdftotext`でページと本文markerを確認する。GUIでは画像の暗いブロックと明るい紙面、多重合成したベクター領域、各ページ固有の色markerを取得画素から検査する。その後、実open RPCが未完の状態でEscを送り、実render RPCが未完の状態で別PDFへ切り替える。旧sessionの破棄、旧文書の保持、切替後の安定、原本hash不変、文書close後のcache解放、通常解像度の維持を確認する。
専用Xvfb 1100×760・DPR1・Qt Quick software・専用D-Busを使う。XDG保存先は一時領域で、利用者の履歴を使用しない。10ms heartbeatの最大間隔はCI停止検出用の1,500ms未満で判定し、50msの視覚応答p95を代用しない。50msごとに採るRSS合計は共有ページの重複を含む。
出力には原本、manifest、復号照合、QtTestログ、3画面、各操作の記録、source/library/worker hashを保存する。`docview`本体のmain関数そのものを実行する試験ではなく、同じ本番GUI部品を使う専用test executableである。GPU、実IME、物理画面、Windows、長時間リークや広範な実書籍の互換性をこの結果だけで合格にしない。[保存済み結果](../results/heavy-pdf/README.md)
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Generate finite, original high-resolution raster and transparent-vector pages."""
import argparse
import hashlib
import json
from pathlib import Path
import zlib
def generate(path):
path = Path(path)
if path.exists():
raise FileExistsError(path)
width, height, curves = 4960, 7016, 60000
compressor = zlib.compressobj(6)
chunks = []
raw_hash = hashlib.sha256()
# Bounded row buffer; 104,398,080 decoded RGB bytes, about A4 at 600 dpi.
paper = bytes((248, 246, 240)) * width
text_row = b''.join(bytes((35, 40, 48)) if 260 < x < width - 260 and x % 83 < 61
else bytes((248, 246, 240)) for x in range(width))
for y in range(height):
row = text_row if 500 < y < height - 500 and y % 123 < 35 else paper
raw_hash.update(row)
part = compressor.compress(row)
if part:
chunks.append(part)
chunks.append(compressor.flush())
image = b''.join(chunks)
vector = [b'q 20 20 555 802 re W n /GS gs\n']
for index in range(curves):
x, y = 25 + (index * 37) % 490, 30 + (index * 53) % 730
r, g, b = (index % 7) / 7, (index % 11) / 11, (index % 13) / 13
vector.append((f'{r:.3f} {g:.3f} {b:.3f} rg {x} {y} m '
f'{x+38} {y+67} {x+61} {y-19} {x+69} {y+22} c '
f'{x+25} {y+48} l h f\n').encode())
vector.append(b'Q\n0.85 0.1 0.65 rg 30 780 32 32 re f\n'
b'BT /F1 12 Tf 80 790 Td (DOCVIEW COMPLEX 60000 TRANSPARENT PATHS) Tj ET\n')
content = [b'q 555 0 0 782 20 20 cm /Scan Do Q\n'
b'0.1 0.3 0.6 rg 30 780 32 32 re f\n'
b'BT /F1 12 Tf 80 790 Td (DOCVIEW RASTER 4960 x 7016 RGB) Tj ET\n',
b''.join(vector)]
def stream(data, dictionary=b''):
return dictionary + b' /Length ' + str(len(data)).encode() + b' >>\nstream\n' + data + b'\nendstream'
objects = [b'<< /Type /Catalog /Pages 2 0 R >>',
b'<< /Type /Pages /Count 2 /Kids [7 0 R 9 0 R] >>',
b'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
stream(image, f'<< /Type /XObject /Subtype /Image /Width {width} /Height {height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode'.encode()),
b'<< /Type /ExtGState /ca 0.16 /CA 0.16 /BM /Multiply >>',
stream(zlib.compress(content[0]), b'<< /Filter /FlateDecode'),
b'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> /XObject << /Scan 4 0 R >> >> /Contents 6 0 R >>',
stream(zlib.compress(content[1]), b'<< /Filter /FlateDecode'),
b'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> /ExtGState << /GS 5 0 R >> >> /Contents 8 0 R >>']
offsets = [0]
with path.open('xb') as out:
out.write(b'%PDF-1.7\n%\xe2\xe3\xcf\xd3\n')
for index, data in enumerate(objects, 1):
offsets.append(out.tell())
out.write(f'{index} 0 obj\n'.encode() + data + b'\nendobj\n')
xref = out.tell()
out.write(f'xref\n0 {len(offsets)}\n0000000000 65535 f \n'.encode())
for offset in offsets[1:]:
out.write(f'{offset:010d} 00000 n \n'.encode())
out.write(f'trailer\n<< /Root 1 0 R /Size {len(offsets)} >>\nstartxref\n{xref}\n%%EOF\n'.encode())
return {'sha256': hashlib.sha256(path.read_bytes()).hexdigest(), 'sizeBytes': path.stat().st_size,
'pages': 2, 'rasterWidth': width, 'rasterHeight': height, 'decodedRasterBytes': width * height * 3,
'rasterSha256': raw_hash.hexdigest(), 'transparentPathCount': curves,
'rasterFilter': 'FlateDecode', 'vectorAlpha': 0.16, 'vectorBlendMode': 'Multiply',
'source': 'Original deterministic synthetic fixture; standard Helvetica is not embedded',
'scope': 'One 600-dpi-equivalent image and one dense transparent-vector page, not a full book or real scan corpus'}
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('output', type=Path)
args = parser.parse_args()
manifest = generate(args.output)
args.output.with_suffix('.json').write_text(json.dumps(manifest, indent=2) + '\n')
print(json.dumps(manifest))
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""Run the supplemental heavy-PDF GUI test on a private X11 software display."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import platform
import shlex
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / 'tests'))
from run_stress_pdf import run_observed
from generate import generate
def sha(path):
with Path(path).open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--binary', type=Path, default=ROOT / 'build-heavy-pdf/test_heavy_pdf')
parser.add_argument('--canonical-build', type=Path, default=ROOT / 'build')
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--private-session', action='store_true', help=argparse.SUPPRESS)
args = parser.parse_args()
if args.output.exists():
parser.error('Choose a new output directory to preserve previous evidence')
flags = shlex.split(os.environ.get('QTWEBENGINE_CHROMIUM_FLAGS', ''))
if os.environ.get('QTWEBENGINE_DISABLE_SANDBOX', '') not in ('', '0') or any(
flag.split('=', 1)[0] in ('--no-sandbox', '--disable-setuid-sandbox', '--disable-seccomp-filter-sandbox') for flag in flags):
parser.error('Sandbox-disabling settings are not allowed')
if not args.private_session:
env = os.environ.copy()
for name in ['DISPLAY', 'WAYLAND_DISPLAY', 'WAYLAND_SOCKET', 'DBUS_SESSION_BUS_ADDRESS',
'QT_SCREEN_SCALE_FACTORS', 'QT_AUTO_SCREEN_SCALE_FACTOR', 'QT_DEVICE_PIXEL_RATIO',
'QT_SCALE_FACTOR_ROUNDING_POLICY', 'QT_ENABLE_HIGHDPI_SCALING', 'QT_QPA_PLATFORMTHEME']:
env.pop(name, None)
env.update(QT_QPA_PLATFORM='xcb', QT_QUICK_BACKEND='software', QT_SCALE_FACTOR='1',
QT_FONT_DPI='96', QTWEBENGINE_CHROMIUM_FLAGS='--disable-gpu', QT_FORCE_STDERR_LOGGING='1')
with tempfile.TemporaryDirectory(prefix='docview-heavy-session-') as private:
bus = Path(private) / 'bus.conf'
bus.write_text('<busconfig><type>session</type><auth>EXTERNAL</auth>'
f'<listen>unix:tmpdir={private}</listen>'
'<policy context="default"><allow send_destination="*"/>'
'<allow receive_sender="*"/><allow own="*"/></policy></busconfig>')
env['XDG_RUNTIME_DIR'] = private
return subprocess.run(['dbus-run-session', '--config-file', str(bus), '--',
'xvfb-run', '-a', '-s', '-screen 0 1100x760x24 -dpi 96 -nolisten tcp',
sys.executable, str(Path(__file__).resolve()), *sys.argv[1:], '--private-session'], env=env).returncode
output = args.output.resolve(); output.mkdir(parents=True, exist_ok=False)
binary, build = args.binary.resolve(), args.canonical_build.resolve()
report = {'success': False, 'os': platform.freedesktop_os_release(), 'kernel': platform.release(),
'scope': 'Linux supplemental heavy-PDF GUI validation; not native Windows, physical display, reference hardware or long-duration acceptance'}
watched = [binary] + [build / n for n in ['docview', 'docview-pdf-worker', 'docview-archive-worker',
'libdocview_core.a', 'libdocview_common.a', 'libdocview_font_broker.a']]
before = {os.path.relpath(p, ROOT): sha(p) for p in watched}
source_files = [ROOT / 'resources.qrc']
for folder in ['src', 'qml', 'resources', 'tests/heavy_pdf']:
source_files += [p for p in (ROOT / folder).rglob('*') if p.is_file() and '__pycache__' not in p.parts]
sources = {str(p.relative_to(ROOT)): sha(p) for p in sorted(set(source_files))}
try:
# The executable resolves workers by application directory. Verify the
# linked paths refer to the canonical tested files before any launch.
for worker in ['docview-pdf-worker', 'docview-archive-worker']:
assert sha(binary.parent / worker) == sha(build / worker)
heavy = output / 'heavy.pdf'
manifest = generate(heavy)
(output / 'fixture.json').write_text(json.dumps(manifest, indent=2) + '\n')
info = subprocess.check_output(['pdfinfo', str(heavy)], text=True, timeout=15)
(output / 'pdfinfo.txt').write_text(info)
extracted = subprocess.check_output(['pdftotext', str(heavy), '-'], text=True, timeout=30)
(output / 'pdftotext.txt').write_text(extracted)
assert 'DOCVIEW RASTER 4960 x 7016 RGB' in extracted and 'DOCVIEW COMPLEX 60000 TRANSPARENT PATHS' in extracted
# Decode the actual image object with an independent parser. Store only
# its digest/size; the 100 MiB temporary decoded file is reclaimed.
with tempfile.TemporaryFile() as decoded:
subprocess.run(['qpdf', '--show-object=4', '--filtered-stream-data', str(heavy)],
stdout=decoded, stderr=subprocess.PIPE, check=True, timeout=30)
decoded.seek(0, 2); decoded_size = decoded.tell(); decoded.seek(0)
decoded_hash = hashlib.file_digest(decoded, 'sha256').hexdigest()
assert decoded_size == manifest['decodedRasterBytes'] and decoded_hash == manifest['rasterSha256']
report['independentDecodedRaster'] = {'bytes': decoded_size, 'sha256': decoded_hash,
'method': 'qpdf --show-object=4 --filtered-stream-data'}
env = os.environ.copy()
light = ROOT / 'tests/fixtures/pdf/navigation.pdf'
env.update(DOCVIEW_HEAVY_PDF=str(heavy), DOCVIEW_LIGHT_PDF=str(light), DOCVIEW_HEAVY_OUTPUT=str(output))
report['process'] = run_observed([str(binary)], env, output, 'test', timeout=180)
measurement = json.loads((output / 'measurement.json').read_text()) if (output / 'measurement.json').exists() else {}
report['success'] = report['process']['exitCode'] == 0 and measurement.get('success') is True
report['independentFixtureMarkersVerified'] = True
report['fixtureSha256'] = sha(heavy)
report['measurementSha256'] = sha(output / 'measurement.json') if measurement else None
except Exception as error:
report['error'] = str(error)
finally:
report['binaries'] = before
report['sourceSha256'] = sources
report['binariesUnchanged'] = all(sha(ROOT / name) == value for name, value in before.items())
report['sourcesUnchanged'] = all(sha(ROOT / name) == value for name, value in sources.items())
report['success'] = report['success'] and report['binariesUnchanged'] and report['sourcesUnchanged']
(output / 'report.json').write_text(json.dumps(report, indent=2) + '\n')
print(json.dumps({'success': report['success'], 'output': str(output), 'error': report.get('error')}))
return 0 if report['success'] else 1
if __name__ == '__main__':
raise SystemExit(main())
+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"