Files
2026-09-21 13:41:40 +09:00

114 lines
6.9 KiB
Python

#!/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())