151 lines
9.8 KiB
Python
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()
|