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