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
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Freeze a complete passing build; keep earlier version evidence separate."""
import argparse
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
import platform
import re
import subprocess
import xml.etree.ElementTree as ET
def sha(path):
with path.open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source', type=Path, default=Path(__file__).resolve().parents[2])
parser.add_argument('--build', type=Path, required=True)
parser.add_argument('--ctest', type=Path, required=True)
parser.add_argument('--installed', type=Path)
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
cases = ET.parse(args.ctest / 'tests.xml').findall('.//testcase')
assert len(cases) == 22 and all(c.find('failure') is None and c.find('skipped') is None for c in cases)
files = [args.source / name for name in ('CMakeLists.txt', 'resources.qrc')]
for folder in ('src', 'qml', 'cmake', 'resources', 'tools', 'tests'):
files += [path for path in (args.source / folder).rglob('*')
if path.is_file() and not path.is_symlink() and '__pycache__' not in path.parts
and 'results' not in path.parts and 'fixtures' not in path.parts]
names = ['docview', 'docview-pdf-worker', 'docview-archive-worker', 'test_app',
'test_core', 'test_pdf', 'test_pdf_canvas', 'test_web_qml', 'test_translations']
value = {
'recordedAtUtc': datetime.now(timezone.utc).isoformat(),
'scope': 'Full CTest after URL representation fix; Xvfb/xcb, software rendering, internal sandboxes enabled',
'os': platform.freedesktop_os_release(), 'kernel': platform.release(),
'compiler': subprocess.check_output(['c++', '--version'], text=True).splitlines()[0],
'binaries': {name: sha(args.build / name) for name in names},
'sourceSha256': {str(path.relative_to(args.source)): sha(path) for path in sorted(set(files))},
'ctest': {'groups': len(cases), 'failures': 0, 'junitSha256': sha(args.ctest / 'tests.xml'),
'logSha256': sha(args.ctest / 'LastTest.log'),
'cases': [case.attrib for case in cases]},
'qtTestTotalsIncludingSetupAndCleanup': re.findall(r'Totals: .*', (args.ctest / 'LastTest.log').read_text()),
'runnerSha256': sha(Path(__file__)),
}
if args.installed:
value['installedBinaries'] = {name: sha(args.installed / 'bin' / name) for name in names[:3]}
with args.output.open('x') as stream:
stream.write(json.dumps(value, ensure_ascii=False, indent=2) + '\n')
print(json.dumps({'ctestGroups': len(cases), 'docviewSha256': value['binaries']['docview']}))
if __name__ == '__main__':
main()
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Verify the URL-fix evidence and publish a current index with explicit history."""
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
import xml.etree.ElementTree as ET
ROOT = Path(__file__).resolve().parents[2]
RESULTS = ROOT / 'tests/results/portal'
def sha(path):
with path.open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()
def read(name):
return json.loads((RESULTS / name).read_text())
def main():
builds = {name: read(name + '-build-record.json') for name in ('arch', 'ubuntu')}
current_sources = {str(path.relative_to(ROOT)): sha(path) for directory in ('src', 'qml', 'cmake', 'resources')
for path in (ROOT / directory).rglob('*') if path.is_file() and '__pycache__' not in path.parts}
current_sources.update({name: sha(ROOT / name) for name in ('CMakeLists.txt', 'resources.qrc')})
for name, value in builds.items():
assert all(value['sourceSha256'].get(path) == digest for path, digest in current_sources.items())
for path in ('tests/test_web_qml.cpp', 'tests/webqml/tst_reader.qml'):
assert value['sourceSha256'][path] == sha(ROOT / path)
ctest = RESULTS / (name + '-ctest')
assert sha(ctest / 'tests.xml') == value['ctest']['junitSha256']
assert sha(ctest / 'LastTest.log') == value['ctest']['logSha256']
cases = ET.parse(ctest / 'tests.xml').findall('.//testcase')
assert len(cases) == 22 and all(row.find('failure') is None and row.find('skipped') is None for row in cases)
assert all(sha(ROOT / 'build' / name) == digest for name, digest in builds['arch']['binaries'].items())
package = read('ubuntu-package/report.json')
repeated = read('ubuntu-package/repeat-report.json')
verified = read('ubuntu-package/verification.json')
archive = ROOT / 'build-ubuntu-vm/packages' / package['archive']
assert package['success'] and repeated['success'] and verified['success']
assert package['archiveSha256'] == repeated['archiveSha256'] == sha(archive)
assert verified['archiveSha256'] == package['archiveSha256']
assert verified['allSizesModesAndHashesMatch'] and verified['rootOwnership']
assert verified['filesVerified'] == package['payloadFiles']
assert package['packagerSha256'] == sha(ROOT / 'tools/package_ubuntu.py')
assert package['executableSha256'] == builds['ubuntu']['installedBinaries']
assert package['manifestSha256'] == sha(RESULTS / 'ubuntu-package/package-manifest.json')
portal = {}
for platform, directory in {'x11': 'portal-x11-final', 'wayland': 'portal-wayland-fixed-keymap'}.items():
report = read(directory + '/report.json')
assert report['success'] and report['executablesUnchanged'] and not report['remainingTaggedChildren']
assert report['executables'] == package['executableSha256']
assert report['packageManifestSha256'] == package['manifestSha256']
assert report['runnerSha256'] == sha(RESULTS / directory / 'runner-at-start.py')
assert report['portalResponses'] == ['2', '0', '0', '0', '0']
assert [row['case'] for row in report['cases']] == ['cancel', 'pdf', 'html', 'htmlzip', 'epub']
assert all(row['focusRestored'] for row in report['cases'])
portal[platform] = {key: report[key] for key in ('success', 'platform', 'executables', 'portalResponses', 'cases', 'scope')}
lifecycle = {phase: read('package-vm/' + phase + '/report.json') for phase in ('install', 'smoke', 'remove')}
assert all(row['success'] and row['archiveSha256'] == package['archiveSha256'] for row in lifecycle.values())
assert lifecycle['install']['executables'] == package['executableSha256']
assert lifecycle['install']['installationMode'] == 'upgrade'
assert lifecycle['install']['runnerSha256'] == sha(ROOT / 'tests/ubuntu_vm/clean_package.py')
assert lifecycle['smoke']['smokeConditions'] == 12
assert lifecycle['remove']['payloadRemoved'] and lifecycle['remove']['kernelRestrictionUnchanged']
performance = read('performance-suite/summary.json')
assert performance['success'] and len(performance['cases']) == 10
assert all(builds['arch']['binaries'][name] == digest for name, digest in performance['buildSha256'].items())
for row in performance['cases']:
assert sha(RESULTS / 'performance-suite' / row['record']) == row['recordSha256']
assert performance['interactionRecordSha256'] == sha(RESULTS / 'performance-suite/interaction/record.json')
assert performance['stressRecordSha256'] == sha(RESULTS / 'performance-suite/stress/report.json')
prior = read('prior-validation-record.json')
record = {
'schemaVersion': 2, 'recordedAtUtc': datetime.now(timezone.utc).isoformat(),
'scope': 'Current URL-fix build: Arch/Ubuntu full CTest, Ubuntu package and XDG/GTK file chooser integration; Arch ten-condition performance, interaction and 1 GiB PDF stress',
'ctestSkipScope': 'All 22 CTest groups pass. Each App group explicitly skips its opt-in performance case; Ubuntu Python 3.12 skips 5 zstd provenance cases. These are not counted as executed acceptance tests.',
'goalComplete': False, 'sourceSha256': current_sources,
'validationToolsSha256': {name: sha(ROOT / name) for name in (
'tests/portal/record_build.py', 'tests/portal/record_validation.py', 'tests/portal/run.py',
'tests/portal/run_performance.py', 'tests/portal/summarize_performance.py', 'tests/ubuntu_vm/clean_package.py')},
'executables': {name: builds['arch']['binaries'][name] for name in package['executableSha256']},
'builds': builds, 'ubuntuPackage': package, 'portal': portal,
'packageLifecycle': lifecycle,
'performance': performance,
'historicalEvidence': {
'record': 'tests/results/portal/prior-validation-record.json',
'sha256': sha(RESULTS / 'prior-validation-record.json'),
'changedCompiledSources': [name for name, digest in current_sources.items() if prior['sourceSha256'].get(name) != digest],
'unchangedArchWorkers': [name for name in ('docview-pdf-worker', 'docview-archive-worker')
if prior['executables'][name] == builds['arch']['binaries'][name]],
'scope': 'Prior full-app performance, GPU, IME, high-density PDF and package measurements are versioned history. Current ten-condition performance and 1 GiB stress are recorded separately above.',
},
'remainingAcceptance': [
'Windows 11 native MSVC/Qt build, LPAC/pipe/PE execution and installer',
'Target-OS physical GPU/display/input, reference hardware and long-duration acceptance',
'Human G-RENDER reference-image approval',
'Complete notices/corresponding sources, public license and release distribution decision',
],
'evidenceSha256': {str(path.relative_to(ROOT)): sha(path) for path in sorted(RESULTS.rglob('*'))
if path.is_file() and path.suffix != '.pyc' and path != RESULTS / 'record.json'},
}
encoded = json.dumps(record, ensure_ascii=False, indent=2) + '\n'
(RESULTS / 'record.json').write_text(encoded)
(ROOT / 'docs/validation-record.json').write_text(encoded)
print(json.dumps({'archCtestGroups': 22, 'ubuntuCtestGroups': 22, 'portalCases': 10,
'packageSmokeConditions': 12, 'performanceConditions': 10, 'goalComplete': False}))
if __name__ == '__main__':
main()
+287
View File
@@ -0,0 +1,287 @@
#!/usr/bin/env python3
"""Exercise the packaged viewer through real XDG/GTK file chooser portals.
Only private Xvfb/Openbox or headless Sway desktops and generated files are
used. No mocked portal replies, Qt test hooks, or user desktop are involved.
"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import signal
import subprocess
import sys
import tempfile
import time
import uuid
ROOT = Path(__file__).resolve().parents[2]
def sha(path):
with Path(path).open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()
def wait_for(probe, description, seconds=15):
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
value = probe()
if value: return value
time.sleep(.1)
raise RuntimeError('Timed out: ' + description)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--platform', choices=['xcb', 'wayland'], required=True)
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--private-session', action='store_true', help=argparse.SUPPRESS)
args = parser.parse_args()
out = args.output.resolve()
if not args.private_session:
if out.exists(): parser.error('Choose a new evidence directory')
with tempfile.TemporaryDirectory(prefix='docview-portal-') as temporary:
private = Path(temporary)
env = {key: value for key, value in os.environ.items() if key in ['HOME', 'PATH', 'USER', 'LOGNAME', 'LANG']}
env.update(XDG_RUNTIME_DIR=str(private), XDG_CONFIG_HOME=str(private / 'config'),
XDG_STATE_HOME=str(private / 'state'), XDG_CACHE_HOME=str(private / 'cache'),
XDG_DATA_HOME=str(private / 'data'), XDG_CURRENT_DESKTOP='DocViewTest',
QT_QPA_PLATFORM=args.platform, QT_QPA_PLATFORMTHEME='xdgdesktopportal',
QT_USE_PORTAL='1', QT_QUICK_BACKEND='software', QTWEBENGINE_CHROMIUM_FLAGS='--disable-gpu',
QT_SCALE_FACTOR='1', QT_FONT_DPI='96', LANG='C.UTF-8', NO_AT_BRIDGE='1')
config = private / 'config/xdg-desktop-portal'; config.mkdir(parents=True)
(config / 'portals.conf').write_text('[preferred]\ndefault=gtk;\norg.freedesktop.impl.portal.FileChooser=gtk;\n')
bus = 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="*"/>'
'<allow eavesdrop="true"/></policy></busconfig>')
display = ['xvfb-run', '-a', '-s', '-screen 0 1280x960x24 -dpi 96 -nolisten tcp'] if args.platform == 'xcb' else []
return subprocess.run(['dbus-run-session', '--config-file', str(bus), '--', *display,
sys.executable, str(Path(__file__).resolve()), *sys.argv[1:], '--private-session'], env=env).returncode
out.mkdir(parents=True, exist_ok=False)
(out / 'runner-at-start.py').write_bytes(Path(__file__).read_bytes())
env = dict(os.environ, DOCVIEW_PORTAL_CHILD_ID=str(uuid.uuid4()))
tag = ('DOCVIEW_PORTAL_CHILD_ID=' + env['DOCVIEW_PORTAL_CHILD_ID']).encode()
processes, streams, commands, cases = [], [], [], []
identities = {name: sha(Path('/opt/docview/bin') / name) for name in ['docview', 'docview-pdf-worker', 'docview-archive-worker']}
report = {'success': False, 'platform': args.platform, 'executables': identities,
'launcherSha256': sha('/usr/bin/docview'), 'runnerSha256': sha(Path(__file__)),
'packageManifestSha256': sha('/opt/docview/share/doc/docview/package-manifest.json'),
'scope': 'Actual packaged app and Ubuntu XDG/GTK file chooser on a private virtual desktop; no physical input or full desktop-session certification',
'osRelease': Path('/etc/os-release').read_text(), 'cases': cases}
def run(command, checked=True, timeout=8):
result = subprocess.run(command, env=env, capture_output=True, text=True, timeout=timeout)
commands.append({'command': command, 'exitCode': result.returncode, 'stdout': result.stdout, 'stderr': result.stderr})
if checked and result.returncode:
raise RuntimeError(str(command) + ': ' + result.stderr)
return result
def start(name, command):
stream = (out / (name + '.log')).open('w'); streams.append(stream)
process = subprocess.Popen(command, env=env, stdout=stream, stderr=subprocess.STDOUT, start_new_session=True)
processes.append(process)
return process
def wayland_keyboard():
# wtype constructs its map from all arguments before sending input.
# Keep the same keysym-to-keycode mapping across the keeper and every
# invocation, including GTK's startup map. Release-only entries declare
# keys without typing them; actual input follows this prefix.
command = ['wtype']
for symbol in ['Escape', 'Return', 'Shift_L', 'Tab'] + [
'U%04X' % ord(char) for char in ''.join(chr(n) for n in range(32, 127)) + '日本語選択資料']:
command += ['-p', symbol]
return command + ['-s', '150']
def keys(*values):
if args.platform == 'xcb':
run(['xdotool', 'key', '--clearmodifiers', '--delay', '90', *values])
else:
command = wayland_keyboard()
for value in values:
parts = value.split('+'); modifiers = {'ctrl':'ctrl', 'shift':'shift', 'alt':'alt'}
for part in parts[:-1]: command += ['-M', modifiers[part.lower()]]
command += ['-s', '40', '-P', parts[-1], '-s', '40', '-p', parts[-1]]
for part in reversed(parts[:-1]): command += ['-m', modifiers[part.lower()]]
command += ['-s', '90']
run(command)
def text(value):
if args.platform == 'xcb': run(['xdotool', 'type', '--clearmodifiers', '--delay', '12', value])
else: run(wayland_keyboard() + ['-d', '12', value])
time.sleep(.3) # Wait for the other process to consume the final key.
def windows():
if args.platform == 'xcb':
found = run(['xdotool', 'search', '--onlyvisible', '--name', '.'], checked=False)
result = []
for identifier in found.stdout.split():
owner = run(['xdotool', 'getwindowpid', identifier], checked=False)
if owner.returncode: continue
title = run(['xdotool', 'getwindowname', identifier]).stdout.strip()
result.append({'id': identifier, 'pid': int(owner.stdout), 'name': title})
return result
tree = json.loads(run(['swaymsg', '-s', env['SWAYSOCK'], '-t', 'get_tree']).stdout)
result = []
def visit(node):
if node.get('pid'): result.append({key: node.get(key) for key in ['id', 'pid', 'name', 'app_id', 'focused']})
for child in node.get('nodes', []) + node.get('floating_nodes', []): visit(child)
visit(tree)
return result
def focused(pid):
if args.platform == 'xcb':
value = run(['xdotool', 'getwindowfocus', 'getwindowpid'], checked=False)
return value.returncode == 0 and value.stdout.strip() == str(pid)
return any(row['pid'] == pid and row.get('focused') for row in windows())
def capture(name):
command = ['scrot', str(out / (name + '.png'))] if args.platform == 'xcb' else ['grim', '-o', 'HEADLESS-1', str(out / (name + '.png'))]
run(command)
state_file = Path(env['XDG_STATE_HOME']) / 'docview/state.json'
def state_entry(path):
try:
value = json.loads(state_file.read_text())
return next((row for row in value['documents'] if row['identity']['canonicalPath'] == str(path) and row['location']), None)
except (OSError, ValueError): return None
try:
report['packages'] = run(['dpkg-query', '-W', 'docview', 'xdg-desktop-portal', 'xdg-desktop-portal-gtk',
'openbox', 'xdotool', 'sway', 'wtype', 'grim', 'scrot']).stdout
if args.platform == 'xcb':
env['GDK_BACKEND'] = 'x11'
start('openbox', ['openbox', '--sm-disable'])
wait_for(lambda: run(['xprop', '-root', '_NET_SUPPORTING_WM_CHECK'], checked=False).stdout.find('window id') >= 0, 'X11 window manager')
else:
runtime = Path(env['XDG_RUNTIME_DIR']); config = runtime / 'sway.conf'
config.write_text('xwayland disable\nswaybg_command -\nswaynag_command -\noutput HEADLESS-1 mode 1280x960\n'
'for_window [app_id=".*"] floating enable\ndefault_border none\ndefault_floating_border none\n'
'focus_follows_mouse no\nmouse_warping none\n')
env.update(WLR_BACKENDS='headless', WLR_RENDERER='pixman', WLR_HEADLESS_OUTPUTS='1', GDK_BACKEND='wayland', XDG_SESSION_TYPE='wayland')
start('sway', ['sway', '-c', str(config)])
wait_for(lambda: list(runtime.glob('sway-ipc.*.sock')) and any(p.is_socket() for p in runtime.glob('wayland-*')), 'Wayland compositor')
env['WAYLAND_DISPLAY'] = next(p.name for p in runtime.glob('wayland-*') if p.is_socket())
env['SWAYSOCK'] = str(next(runtime.glob('sway-ipc.*.sock')))
start('keyboard', wayland_keyboard() + ['-s', '300000', '-k', 'Shift_L'])
wait_for(lambda: any(row.get('type') == 'keyboard' for row in json.loads(run(['swaymsg', '-t', 'get_inputs']).stdout)), 'persistent virtual keyboard')
start('dconf', ['/usr/libexec/dconf-service'])
start('permission-store', ['/usr/libexec/xdg-permission-store'])
start('documents', ['/usr/libexec/xdg-document-portal'])
backend = start('gtk-portal', ['/usr/libexec/xdg-desktop-portal-gtk'])
start('desktop-portal', ['/usr/libexec/xdg-desktop-portal', '--verbose'])
query = ['gdbus', 'call', '--session', '--dest', 'org.freedesktop.portal.Desktop', '--object-path', '/org/freedesktop/portal/desktop',
'--method', 'org.freedesktop.DBus.Properties.Get', 'org.freedesktop.portal.FileChooser', 'version']
wait_for(lambda: run(query, checked=False, timeout=3).returncode == 0, 'real file chooser portal', 30)
report['portalVersion'] = run(query).stdout.strip()
monitor = start('portal-messages', ['stdbuf', '-oL', '-eL', 'dbus-monitor', '--session', "interface='org.freedesktop.portal.FileChooser'",
"interface='org.freedesktop.portal.Request'"])
documents = out / '選択 資料'; documents.mkdir()
shutil.copytree(ROOT / 'tests/fixtures/web/html', documents / 'web')
selections = []
for kind, source, destination in [
('pdf', ROOT / 'tests/fixtures/pdf/extended/japanese-layout.pdf', documents / '日本語 j k 123 #%.pdf'),
('html', documents / 'web/index.html', documents / 'web/日本語 j k 123 #%.html'),
('htmlzip', ROOT / 'tests/fixtures/web/html-book.zip', documents / '日本語 j k 123 #%.zip'),
('epub', ROOT / 'tests/fixtures/web/reflow-rtl.epub', documents / '日本語 j k 123 #%.epub')]:
shutil.copyfile(source, destination)
selections.append((kind, destination, sha(destination)))
report['documents'] = [{'format': kind, 'path': str(path), 'sha256': digest} for kind, path, digest in selections]
config = Path(env['XDG_CONFIG_HOME']) / 'docview/config.toml'; config.parent.mkdir(parents=True)
config.write_text('schema_version=1\n[privacy]\nremember_position=true\nrecent_documents=10\n')
app = start('docview', ['/usr/bin/docview', '--config', str(config)])
wait_for(lambda: any(row['pid'] == app.pid for row in windows()) and focused(app.pid), 'initial viewer focus')
time.sleep(.5) # Qt queries the portal version asynchronously at startup.
# Exercise cancellation through the real GTK window before any document opens.
keys('ctrl+o')
popup = wait_for(lambda: next((row for row in windows() if row['pid'] == backend.pid), None), 'GTK cancel dialog')
wait_for(lambda: focused(backend.pid), 'GTK dialog focus')
keys('ctrl+l'); text('j k 123 ?'); capture('cancel-dialog')
escape_count = 0
# GTK can consume the first Escape to dismiss its completion popup.
for _ in range(3):
keys('Escape'); escape_count += 1; time.sleep(.3)
if not any(row['pid'] == backend.pid for row in windows()): break
wait_for(lambda: not any(row['pid'] == backend.pid for row in windows()) and focused(app.pid), 'focus restored after cancel')
assert not state_file.exists() or not json.loads(state_file.read_text())['documents']
cases.append({'case': 'cancel', 'backendWindow': popup, 'focusRestored': True,
'documentOpened': False, 'escapeKeyCount': escape_count})
for kind, path, digest in selections:
keys('ctrl+o')
popup = wait_for(lambda: next((row for row in windows() if row['pid'] == backend.pid), None), 'GTK ' + kind + ' dialog')
wait_for(lambda: focused(backend.pid), 'GTK file entry focus')
keys('ctrl+l'); text(str(path)); capture(kind + '-dialog')
keys('Return')
wait_for(lambda: not any(row['pid'] == backend.pid for row in windows()) and focused(app.pid), 'viewer focus after ' + kind)
entry = wait_for(lambda: state_entry(path), 'presented and persisted ' + kind, 20)
title = wait_for(lambda: next((row['name'] for row in windows() if row['pid'] == app.pid and path.name in row['name']), None), kind + ' window title')
if kind == 'pdf':
assert entry['location']['kind'] == 'pdf'
text('G')
entry = wait_for(lambda: (value if (value := state_entry(path)) and value['location'].get('pageIndex') == 1 else None), 'PDF keyboard navigation after dialog', 15)
else:
assert entry['location'].get('resourcePath')
if kind == 'epub': assert entry['location'].get('spineIdref')
capture(kind + '-opened')
assert sha(path) == digest
cases.append({'case': kind, 'backendWindow': popup, 'windowTitle': title, 'savedPresentedLocation': entry['location'],
'focusRestored': True, 'originalUnchanged': True})
text(':q'); keys('Return'); app.wait(timeout=10)
assert app.returncode == 0
time.sleep(.2)
messages = (out / 'portal-messages.log').read_text()
responses = re.findall(r'interface=org.freedesktop.portal.Request; member=Response\n\s+uint32 (\d+)', messages)
assert len(re.findall(r'interface=org.freedesktop.portal.FileChooser; member=OpenFile', messages)) == 5
# GTK 1.15.1 maps Escape's GTK_RESPONSE_DELETE_EVENT to portal code 2;
# code 1 is reserved for its explicit Cancel button. Both reject in Qt.
# See upstream src/filechooser.c:file_chooser_response.
assert responses == ['2', '0', '0', '0', '0'], responses
for _, path, _ in selections: assert path.as_uri() in messages, str(path)
report['portalResponses'] = responses
report['actualGtkBackendPid'] = backend.pid
report['applicationExitCode'] = app.returncode
report['success'] = True
except Exception as error:
report['error'] = str(error)
if 'app' in locals() and app.poll() is None:
try: capture('failure')
except Exception as capture_error: report['failureCaptureError'] = str(capture_error)
finally:
report['commands'] = commands
for process in reversed(processes):
if process.poll() is None:
try: os.killpg(process.pid, signal.SIGTERM)
except ProcessLookupError: pass
for process in reversed(processes):
try: process.wait(timeout=3)
except subprocess.TimeoutExpired:
try: os.killpg(process.pid, signal.SIGKILL)
except ProcessLookupError: pass
process.wait(timeout=3)
for stream in streams: stream.close()
remaining = []
for directory in Path('/proc').glob('[0-9]*'):
try:
if int(directory.name) != os.getpid() and tag in (directory / 'environ').read_bytes().split(b'\0'):
remaining.append(int(directory.name))
except (OSError, ValueError): pass
report['remainingTaggedChildren'] = remaining
for pid in remaining:
try: os.kill(pid, signal.SIGTERM)
except ProcessLookupError: pass
report['executablesUnchanged'] = all(sha(Path('/opt/docview/bin') / name) == digest for name, digest in identities.items())
report['success'] = report['success'] and report['executablesUnchanged'] and not remaining
if state_file.exists(): shutil.copyfile(state_file, out / 'generated-document-state.json')
(out / 'report.json').write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n')
print(json.dumps({key: report.get(key) for key in ['success', 'platform', 'error', 'portalResponses']}))
return 0 if report['success'] else 1
if __name__ == '__main__':
raise SystemExit(main())
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""Repeat the existing ten-condition workload for the current production build."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import subprocess
import time
ROOT = Path(__file__).resolve().parents[2]
def sha(path):
with path.open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
out = args.output.resolve(); out.mkdir(parents=True, exist_ok=False)
(out / 'runner-at-start.py').write_bytes(Path(__file__).read_bytes())
env = dict(os.environ)
for name in ('DISPLAY', 'WAYLAND_DISPLAY', 'QT_SCREEN_SCALE_FACTORS', 'QT_STYLE_OVERRIDE', 'QT_SCALE_FACTOR_ROUNDING_POLICY'):
env.pop(name, None)
env.update(QT_QPA_PLATFORM='xcb', QT_QUICK_BACKEND='software', QTWEBENGINE_CHROMIUM_FLAGS='--disable-gpu',
QT_SCALE_FACTOR='1', QT_FONT_DPI='96', QT_AUTO_SCREEN_SCALE_FACTOR='0')
record = {'success': False, 'scope': 'Arch Xvfb 1100x760 DPR1/software; OS file cache retained; development measurements, not reference hardware acceptance',
'binaries': {name: sha(ROOT / 'build' / name) for name in ('docview', 'docview-pdf-worker', 'docview-archive-worker', 'test_app')},
'runnerSha256': sha(Path(__file__)), 'commands': [],
'displayEnvironment': {name: env[name] for name in ('QT_QPA_PLATFORM', 'QT_QUICK_BACKEND', 'QTWEBENGINE_CHROMIUM_FLAGS', 'QT_SCALE_FACTOR', 'QT_FONT_DPI', 'QT_AUTO_SCREEN_SCALE_FACTOR')}}
jobs = [(corpus, ['python3', 'tests/benchmark.py', '--binary', 'build/docview', '--corpus', corpus,
'--operations', '125', '--cold-process-runs', '30', '--output', str(out / corpus)])
for corpus in ('smoke', 'standard', 'fonts')]
jobs += [('interaction', ['python3', 'tests/measure_interaction.py', '--binary', 'build/test_app', '--output', str(out / 'interaction')]),
('stress', ['python3', 'tests/run_stress_pdf.py', '--binary', 'build/docview', '--output', str(out / 'stress')])]
try:
for name, command in jobs:
wrapper = ['dbus-run-session', '--', 'xvfb-run', '-a', '-s', '-screen 0 1100x760x24 -dpi 96 -nolisten tcp']
started = time.monotonic()
with (out / (name + '.log')).open('w') as log:
result = subprocess.run(wrapper + command, cwd=ROOT, env=env, stdout=log, stderr=subprocess.STDOUT, timeout=1500)
row = {'name': name, 'command': wrapper + command, 'exitCode': result.returncode, 'seconds': time.monotonic() - started,
'measurementRunnerSha256': sha(ROOT / command[1])}
record['commands'].append(row)
(out / 'progress.json').write_text(json.dumps(record, indent=2) + '\n')
print(json.dumps(row), flush=True)
if result.returncode: raise RuntimeError('Failed workload: ' + name)
assert all(sha(ROOT / 'build' / name) == digest for name, digest in record['binaries'].items())
record['success'] = True
finally:
(out / 'report.json').write_text(json.dumps(record, indent=2) + '\n')
if __name__ == '__main__':
main()
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Check and summarize all ten completed URL-fix measurements."""
import hashlib
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / 'tests/results/portal/performance-suite'
def sha(path):
with path.open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()
def main():
run = json.loads((OUT / 'report.json').read_text())
assert run['success'] and len(run['commands']) == 5 and all(row['exitCode'] == 0 for row in run['commands'])
assert run['runnerSha256'] == sha(OUT / 'runner-at-start.py')
binaries = {name: sha(ROOT / 'build' / name) for name in ('docview', 'docview-pdf-worker', 'docview-archive-worker')}
assert all(run['binaries'][name] == value for name, value in binaries.items())
rows, operation_count = [], 0
for corpus, formats in [('smoke', ['pdf', 'html', 'zip', 'epub']),
('standard', ['pdf', 'pdf-headings', 'html', 'zip', 'epub']), ('fonts', ['pdf'])]:
for kind in formats:
relative = corpus + '/' + kind + '.json'
value = json.loads((OUT / relative).read_text())
assert value['success'] and value['originalUnchanged'] and all(value['measurementChecks'].values())
assert value['executableSha256'] == binaries and value['runnerSha256'] == sha(ROOT / 'tests/benchmark.py')
assert len(value['openFrameMs']) == 30 and value['coldProcessOpen']['requested'] == 30 and value['coldProcessOpen']['failed'] == 0
assert value['summary']['uniform_open_close_cycles'] == 29 and value['summary']['all_closes_empty']
display = value['display']
assert display['screenLogicalWidth'] == 1100 and display['screenLogicalHeight'] == 760 and display['windowDevicePixelRatio'] == 1
cold = list((OUT / corpus / 'cold' / kind).glob('[0-9][0-9][0-9].json'))
assert len(cold) == 30
for path in cold:
sample = json.loads(path.read_text())
assert sample['success'] and sample['documentSha256'] == value['documentSha256']
assert sample['display'] == display and sample['maximumMemoryPressureLevel'] == 0
operation_count += len(value['operations'])
summary = value['summary']
exceeded = [name for name, observation in value['proposedBudgetObservations'].items()
if observation is False or isinstance(observation, dict) and observation.get('observed_met') is False]
rows.append({'corpus': corpus, 'format': kind, 'record': relative, 'recordSha256': sha(OUT / relative),
'coldOpenP95Ms': summary['fresh_process_fresh_app_cache_open']['p95_ms'],
'responseP95Ms': summary['response']['p95_ms'], 'finalQualityP95Ms': summary['final_quality']['p95_ms'],
'scrollP95Ms': summary['continuous_scroll_frames']['p95_ms'],
'processGroupPeakMiB': value['processGroupRssPeakBytes'] / 1024**2,
'budgetObservationsExceeded': exceeded, 'navigationCoverage': value['navigationCoverage']})
interaction = json.loads((OUT / 'interaction/record.json').read_text())
stress = json.loads((OUT / 'stress/report.json').read_text())
measured = {row['path']: row['sha256'] for row in interaction['binaries']}
assert interaction['success'] and measured['test_app'] == sha(ROOT / 'build/test_app')
assert all(measured[name] == digest for name, digest in binaries.items())
assert stress['success'] and stress['originalUnchanged'] and stress['sourceRemoved']
assert stress['executableSha256'] == binaries and stress['executablesUnchanged']
result = {'success': True, 'buildSha256': binaries, 'cases': rows, 'coldOpens': 300, 'sameProcessOpens': 300,
'summaryGeneratorSha256': sha(Path(__file__)),
'navigationOperations': operation_count, 'scope': run['scope'],
'interactionRecordSha256': sha(OUT / 'interaction/record.json'), 'stressRecordSha256': sha(OUT / 'stress/report.json'),
'proposedBudgetExceedances': sum(len(row['budgetObservationsExceeded']) for row in rows)}
(OUT / 'summary.json').write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n')
lines = ['# URL比較修正後の性能測定', '',
'10条件で新規プロセス・空のXDGアプリキャッシュ30回、同一プロセス30回、適用される移動系列125操作、連続scroll240入力を実施した。原本不変、全closeの状態解放、メモリ圧迫なしを確認した。', '',
'ArchのXvfb 1100×760、DPR1、software描画。OSファイルキャッシュは保持。起動後のopenを起点とし、プロセス起動時間を含めない。Qt提示通知の測定で、Webの対象画素や物理scanoutの直接測定ではない。基準機・対象OSの性能受入とは区別する。', '',
'| 資料 | 形式 | 冷open p95 ms | 応答 p95 ms | 最終品質 p95 ms | scroll p95 ms | 群RSS最大 MiB |',
'|---|---|---:|---:|---:|---:|---:|']
for row in rows:
lines.append(f"| {row['corpus']} | [{row['format']}]({row['record']}) | {row['coldOpenP95Ms']:.2f} | {row['responseP95Ms']:.2f} | {row['finalQualityP95Ms']:.2f} | {row['scrollP95Ms']:.2f} | {row['processGroupPeakMiB']:.2f} |")
lines += ['', f"新規open 300回・同一プロセスopen 300回・移動系列{operation_count}操作。暫定予算の参考超過は{result['proposedBudgetExceedances']}件。元の予算値は変更していない。未測定の系列は各JSONのnavigationCoverageで区別する。", '',
'[実行コマンド](report.json)、[機械集計](summary.json)、[操作負荷](interaction/record.json)、[約1GB・5,000ページPDF](stress/report.json)を保存した。重いPDFは3open・100操作の追加負荷であり、30回の性能受入標本ではない。', '']
(OUT / 'README.md').write_text('\n'.join(lines))
print(json.dumps({key: result[key] for key in ('success', 'coldOpens', 'sameProcessOpens', 'navigationOperations', 'proposedBudgetExceedances')}))
if __name__ == '__main__':
main()