114 lines
6.9 KiB
Python
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())
|