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

151 lines
9.8 KiB
Python

#!/usr/bin/env python3
"""Test installation or upgrade of a deb on the dedicated VM without an SDK."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import subprocess
import sys
ROOT = Path(__file__).resolve().parents[2]
APP = Path('/opt/docview')
ARCHIVE_SHA = '978904fd5986694f7b053381dcb6ca1ac07b92884ef9768c320923d179d873e5'
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('--phase', choices=['install', 'smoke', 'remove'], required=True)
parser.add_argument('--archive', type=Path, required=True)
parser.add_argument('--archive-sha256', default=ARCHIVE_SHA)
parser.add_argument('--upgrade', action='store_true', help='Require an existing DocView package during install')
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
assert os.getuid() != 0 and Path.home() == Path('/home/docview')
assert re.fullmatch('[0-9a-f]{64}', args.archive_sha256)
assert sha(args.archive) == args.archive_sha256
for name in ['/opt/docview-qt', '/opt/docview-deps', '/home/docview/docview-build', '/home/docview/docview-install']:
assert not Path(name).exists(), name
output = args.output.resolve() / args.phase
output.mkdir(parents=True, exist_ok=False)
record = {'success': False, 'phase': args.phase, 'archiveSha256': args.archive_sha256,
'installationMode': 'upgrade' if args.upgrade else 'fresh-install',
'runnerSha256': sha(Path(__file__)), 'commands': [], 'originalSdkOrBuildPresent': False,
'osRelease': Path('/etc/os-release').read_text(), 'uid': os.getuid()}
def run(name, command, environment=None, allowed=(0,)):
with (output / (name + '.log')).open('w') as stream:
result = subprocess.run(command, env=environment, stdout=stream, stderr=subprocess.STDOUT, timeout=900)
record['commands'].append({'name': name, 'command': command, 'exitCode': result.returncode,
'logSha256': sha(output / (name + '.log'))})
assert result.returncode in allowed, name
return (output / (name + '.log')).read_text()
def packages(name):
return run(name, ['dpkg-query', '-W', '-f=${Package}\t${Version}\t${db:Status-Status}\n'])
minimal = {'PATH': '/usr/bin:/bin', 'HOME': '/home/docview', 'USER': 'docview',
'LOGNAME': 'docview', 'LANG': 'C.UTF-8'}
try:
if args.phase == 'install':
before = packages('packages-before')
installed = [line for line in before.splitlines() if line.startswith('docview\t')]
if args.upgrade:
assert APP.is_dir() and Path('/usr/bin/docview').is_file()
assert len(installed) == 1 and installed[0].endswith('\tinstalled')
record['previousPackage'] = installed[0]
record['previousManifestSha256'] = sha(APP / 'share/doc/docview/package-manifest.json')
else:
assert not APP.exists() and not Path('/usr/bin/docview').exists() and not installed
run('apt-update', ['sudo', 'apt-get', 'update'])
run('apt-plan', ['sudo', 'apt-get', '-s', '--no-install-recommends', 'install', str(args.archive)])
run('apt-install', ['sudo', 'env', 'DEBIAN_FRONTEND=noninteractive', 'apt-get', '-y',
'--no-install-recommends', 'install', str(args.archive)])
after = packages('packages-after-install')
rows = json.loads((APP / 'share/doc/docview/package-manifest.json').read_text())['files']
for row in rows:
if row['path'].startswith('DEBIAN/'): continue
path = Path('/') / row['path']; info = path.stat()
assert info.st_uid == 0 and info.st_gid == 0
assert oct(info.st_mode & 0o7777) == row['mode']
assert info.st_size == row['size'] and sha(path) == row['sha256']
environment = {**minimal, 'LD_LIBRARY_PATH': '/opt/docview/lib:/opt/docview/qt/lib'}
dependencies = []
for path in sorted(APP.rglob('*')):
if not path.is_file(): continue
with path.open('rb') as stream:
if stream.read(4) != b'\x7fELF': continue
result = subprocess.run(['ldd', str(path)], env=environment, capture_output=True, text=True, timeout=30)
assert result.returncode == 0 and 'not found' not in result.stdout, str(path) + result.stdout
resolved = re.findall(r'(?:=>\s+)?(/[^\s]+)\s+\(', result.stdout)
assert resolved or result.stdout.strip() == 'statically linked'
assert all(name.startswith(('/opt/docview/', '/lib/', '/lib64/', '/usr/lib/')) for name in resolved)
dependencies.append({'path': str(path), 'sha256': sha(path), 'resolved': sorted(set(resolved))})
record['elfDependenciesBeforeTestHelperInstallStep'] = dependencies
record['installedFilesVerified'] = sum(not row['path'].startswith('DEBIAN/') for row in rows)
record['executables'] = {name: sha(APP / 'bin' / name) for name in ['docview', 'docview-pdf-worker', 'docview-archive-worker']}
profiles = run('apparmor-installed', ['sudo', 'cat', '/sys/kernel/security/apparmor/profiles'])
assert 'docview-bundled-qtwebengine ' in profiles and 'docview-qtwebengine ' not in profiles
assert run('userns-before', ['sysctl', '-n', 'kernel.apparmor_restrict_unprivileged_userns']).strip() == '1'
record['newInstalledPackages'] = sorted(set(after.splitlines()) - set(before.splitlines()))
# Dependency resolution is recorded before adding test infrastructure.
run('apt-test-helpers', ['sudo', 'env', 'DEBIAN_FRONTEND=noninteractive', 'apt-get', '-y',
'--no-install-recommends', 'install', 'xvfb', 'xauth', 'sway', 'dbus-x11'])
packages('packages-after-test-helpers')
elif args.phase == 'smoke':
installed = json.loads((args.output / 'install/report.json').read_text())
assert installed['success']
record['executables'] = installed['executables']
record['launcherSha256'] = sha(Path('/usr/bin/docview'))
record['smokeSourceSha256'] = sha(ROOT / 'tests/smoke.py')
record['waylandRunnerSha256'] = sha(ROOT / 'tests/run_wayland_validation.py')
for name, digest in installed['executables'].items(): assert sha(APP / 'bin' / name) == digest
run('x11', ['dbus-run-session', '--', 'xvfb-run', '-a', '-s', '-screen 0 1100x760x24', sys.executable,
str(ROOT / 'tests/smoke.py'), '--binary', '/usr/bin/docview', '--output', str(output / 'x11')],
{**minimal, 'QT_QPA_PLATFORM': 'xcb', 'QT_QUICK_BACKEND': 'software', 'QTWEBENGINE_CHROMIUM_FLAGS': '--disable-gpu'})
run('wayland', [sys.executable, str(ROOT / 'tests/run_wayland_validation.py'), '--build-dir', '/opt/docview/bin',
'--smoke-binary', '/usr/bin/docview', '--output', str(output / 'wayland'), '--mode', 'smoke'], minimal)
for relative in ['x11/results.json', 'wayland/smoke/results.json']:
cases = json.loads((output / relative).read_text())
assert len(cases) == 6 and all(row['state'] == 'Ready' and row['binarySha256'] == record['launcherSha256'] for row in cases)
for name, digest in installed['executables'].items(): assert sha(APP / 'bin' / name) == digest
record.update(smokeConditions=12, executableBytesUnchanged=True, callerRuntimeVariablesAbsent=True)
else:
assert json.loads((args.output / 'smoke/report.json').read_text())['success']
probes = []
try:
for directory in ['.config/docview', '.local/state/docview', '.local/share/docview', 'validation/clean-user-document']:
path = Path.home() / directory / 'clean-package-probe.txt'; path.parent.mkdir(parents=True, exist_ok=True)
with path.open('x') as stream: stream.write('保持する文書と設定\n')
probes.append({'path': str(path), 'sha256': sha(path)})
run('apt-remove', ['sudo', 'apt-get', '-y', 'remove', 'docview'])
assert not APP.exists() and not Path('/usr/bin/docview').exists()
assert not Path('/usr/share/applications/docview.desktop').exists()
assert Path('/etc/apparmor.d/docview').is_file()
assert 'docview-bundled-qtwebengine ' not in run('apparmor-removed', ['sudo', 'cat', '/sys/kernel/security/apparmor/profiles'])
assert all(sha(Path(row['path'])) == row['sha256'] for row in probes)
run('apt-purge', ['sudo', 'apt-get', '-y', 'purge', 'docview'])
assert not Path('/etc/apparmor.d/docview').exists()
assert all(sha(Path(row['path'])) == row['sha256'] for row in probes)
assert run('userns-after', ['sysctl', '-n', 'kernel.apparmor_restrict_unprivileged_userns']).strip() == '1'
record.update(payloadRemoved=True, profileUnloaded=True, conffilePurged=True,
userProbesPreserved=probes, kernelRestrictionUnchanged=True)
finally:
for row in probes:
path = Path(row['path'])
if path.is_file() and sha(path) == row['sha256']: path.unlink()
record['success'] = True
finally:
(output / 'report.json').write_text(json.dumps(record, indent=2, sort_keys=True) + '\n')
print(json.dumps({'phase': args.phase, 'success': record['success']}))
if __name__ == '__main__':
main()