#!/bin/sh set -eu export QEMU_ACCEL=tcg export FIRMWARE=bios unset KERNEL_IMAGE INITRAMFS ROOT_IMAGE ROOT_DISK BOOT_MEDIA KERNEL_VERSION python3 - <<'PY' import importlib.util import json import subprocess import tempfile from pathlib import Path spec = importlib.util.spec_from_file_location('holy_qemu', 'tests/qemu.py') runner = importlib.util.module_from_spec(spec) spec.loader.exec_module(runner) frames, valid = runner.boot_frames([ 'HOLY-BOOT-1 boot 1', 'identity', 'recovered', 'HOLY-BOOT-1 boot 2', 'recovered'], True) assert valid and {'identity', 'recovered'} <= frames[0] assert not {'identity', 'recovered'} <= frames[1] for lines in (['HOLY-BOOT-1 boot 2'], ['HOLY-BOOT-1 boot 1', 'HOLY-BOOT-1 boot 1'], ['HOLY-BOOT-1 boot 1', 'HOLY-BOOT-1 boot 2', 'HOLY-BOOT-1 boot 3']): assert not runner.boot_frames(lines, True)[1] with tempfile.TemporaryDirectory() as scratch: base = Path(scratch) / 'base.qcow2' child = Path(scratch) / 'child.qcow2' subprocess.run(['qemu-img', 'create', '-q', '-f', 'qcow2', str(base), '8M'], check=True) assert runner.disk_format('qemu-img', base) == 'qcow2' subprocess.run(['qemu-img', 'create', '-q', '-f', 'qcow2', '-F', 'qcow2', '-b', str(base), str(child)], check=True) try: runner.disk_format('qemu-img', child) except SystemExit as exc: assert exc.code == 6 else: raise AssertionError('backing chain accepted') PY tmp=$(mktemp -d "${TMPDIR:-/tmp/opencode}/holy-qemu-fixture-XXXXXX") trap 'rm -rf "$tmp"' EXIT HUP INT TERM plan=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa for case_name in missing-disk forbidden-iso; do iso= expected=6 if test "$case_name" = forbidden-iso; then iso="$tmp/unused.iso"; expected=2; fi if ARCH=x86_64 BOOT_MEDIA=disk ISO="$iso" BOOT_PLAN="$plan" \ REPORT_DIR="$tmp" sh tests/qemu.sh > "$tmp/out" 2> "$tmp/err"; then exit 1 else test "$?" -eq "$expected" fi case "$case_name" in missing-disk) grep -q 'disk boot requires ROOT_DISK' "$tmp/err" ;; forbidden-iso) grep -q 'disk boot must not provide an ISO' "$tmp/err" ;; esac done for arch in i686 x86_64; do if ARCH="$arch" ISO="$tmp/missing.iso" BOOT_PLAN="$plan" \ REPORT_DIR="$tmp" sh tests/qemu.sh > "$tmp/out" 2> "$tmp/err"; then exit 1 else test "$?" -eq 6 fi done truncate -s 1048576 "$tmp/blank.iso" if ARCH=i686 ISO="$tmp/blank.iso" BOOT_PLAN="$plan" REPORT_DIR="$tmp" \ INSTALL_FIRMWARE=both python3 tests/install-vm.py > "$tmp/out" 2> "$tmp/err"; then exit 1 else test "$?" -eq 2 fi grep -q 'i686 supports BIOS' "$tmp/err" if ARCH=x86_64 ISO="$tmp/blank.iso" BOOT_PLAN="$plan" \ LIBC_BOOT_STATE=remove-both REPORT_DIR="$tmp" sh tests/qemu.sh \ > "$tmp/out" 2> "$tmp/err"; then exit 1 else test "$?" -eq 2 fi grep -q 'remove-both requires persistent root' "$tmp/err" if ARCH=x86_64 ISO="$tmp/blank.iso" BOOT_PLAN="$plan" \ QEMU_KEEP=invalid REPORT_DIR="$tmp" sh tests/qemu.sh \ > "$tmp/out" 2> "$tmp/err"; then exit 1 else test "$?" -eq 2 fi grep -q 'QEMU_KEEP must be 0 or 1' "$tmp/err" for arch in i686 x86_64; do if ARCH="$arch" ISO="$tmp/blank.iso" BOOT_PLAN="$plan" \ QEMU_TIMEOUT=2 REPORT_DIR="$tmp" sh tests/qemu.sh \ > "$tmp/out" 2> "$tmp/err"; then exit 1 else test "$?" -eq 4 fi report=$(sed -n 's/^holy-qemu: fail report //p' "$tmp/out") test -f "$report" grep -qx 'result fail' "$report" grep -qx "arch $arch" "$report" grep -qx 'firmware bios' "$report" grep -qx 'accelerator tcg' "$report" python3 - "$report.json" "$tmp/blank.iso" <<'PY' import hashlib, json, pathlib, sys report = json.loads(pathlib.Path(sys.argv[1]).read_text()) assert report['result'] == 'fail' assert report['reason'] == 'boot-timeout' assert report['network'] == 'disabled' assert report['missing_markers'] assert set(report['checks'].values()) == {'unknown'} assert report['inputs']['iso']['sha256'] == hashlib.sha256(pathlib.Path(sys.argv[2]).read_bytes()).hexdigest() PY done qemu-img create -q -f qcow2 "$tmp/base.qcow2" 8M base_hash=$(sha256sum "$tmp/base.qcow2" | cut -d ' ' -f 1) if ARCH=x86_64 ISO="$tmp/blank.iso" ROOT_DISK="$tmp/base.qcow2" \ BOOT_PLAN="$plan" REPORT_DIR="$tmp" QEMU_TIMEOUT=2 \ QEMU_PROBE_TIMEOUT=1 sh tests/qemu.sh > "$tmp/out" 2> "$tmp/err"; then exit 1 else test "$?" -eq 4 fi report=$(sed -n 's/^holy-qemu: fail report //p' "$tmp/out") python3 - "$report.json" "$base_hash" <<'PY' import json, pathlib, sys report = json.loads(pathlib.Path(sys.argv[1]).read_text()) assert report['inputs']['root_disk']['format'] == 'qcow2' assert report['inputs']['root_disk']['sha256'] == sys.argv[2] assert report['overlay']['base_unchanged'] assert report['overlay']['backing_format'] == 'qcow2' assert report['overlay']['retained'] is False assert report['temporary_inputs_retained'] is False assert not pathlib.Path(report['overlay']['path']).exists() assert not (pathlib.Path(sys.argv[1]).parent / 'root-base.qcow2').exists() assert report['probe_timeout_seconds'] == 1 assert report['probes'] == [] PY test "$(sha256sum "$tmp/base.qcow2" | cut -d ' ' -f 1)" = "$base_hash" if ARCH=x86_64 ISO="$tmp/blank.iso" ROOT_DISK="$tmp/base.qcow2" \ BOOT_PLAN="$plan" REPORT_DIR="$tmp" QEMU_TIMEOUT=2 QEMU_KEEP=1 \ sh tests/qemu.sh > "$tmp/out" 2> "$tmp/err"; then exit 1 else test "$?" -eq 4 fi report=$(sed -n 's/^holy-qemu: fail report //p' "$tmp/out") python3 - "$report.json" <<'PY' import json, pathlib, sys report = json.loads(pathlib.Path(sys.argv[1]).read_text()) assert report['overlay']['retained'] is True assert report['temporary_inputs_retained'] is True assert pathlib.Path(report['overlay']['path']).is_file() assert (pathlib.Path(sys.argv[1]).parent / 'root-base.qcow2').is_file() PY ARCH=x86_64 ISO="$tmp/blank.iso" BOOT_PLAN="$plan" REPORT_DIR="$tmp" \ QEMU_TIMEOUT=5 QEMU_PROBE_TIMEOUT=1 python3 - <<'PY' import json, os, pathlib, subprocess, time parent = pathlib.Path(os.environ['REPORT_DIR']) existing = set(parent.glob('holy-qemu-x86_64-*')) runner = subprocess.Popen(['sh', 'tests/qemu.sh'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) try: deadline = time.monotonic() + 5 serial = None while time.monotonic() < deadline: paths = [path / 'serial.log' for path in parent.glob('holy-qemu-x86_64-*') if path not in existing and (path / 'serial.log').exists()] if paths: serial = paths[0] break assert runner.poll() is None time.sleep(0.02) assert serial is not None with serial.open('a') as stream: stream.write('HOLY-BOOT-1 stage identity\nHOLY-BOOT-1 stage identity\n') stream.flush() os.fsync(stream.fileno()) output, stderr = runner.communicate(timeout=10) assert runner.returncode == 4, (output, stderr) report_path = output.strip().split(' report ', 1)[1] report = json.loads(pathlib.Path(report_path + '.json').read_text()) assert report['reason'] == 'probe-timeout', report['reason'] assert report['timed_out_probe'] == 'identity' assert report['timed_out_boot'] == 1 assert len(report['probes']) == 1 assert report['probes'][0]['boot'] == 1 assert report['probes'][0]['stage'] == 'identity' assert report['probes'][0]['status'] == 'unknown' finally: if runner.poll() is None: runner.terminate() runner.wait(timeout=10) PY ARCH=x86_64 ISO="$tmp/blank.iso" BOOT_PLAN="$plan" REPORT_DIR="$tmp" \ python3 - <<'PY' import json, os, pathlib, signal, subprocess, time p = subprocess.Popen(['sh', 'tests/qemu.sh'], stdout=subprocess.PIPE, text=True) try: children = pathlib.Path(f'/proc/{p.pid}/task/{p.pid}/children') deadline = time.monotonic() + 10 while not children.read_text().strip(): assert p.poll() is None and time.monotonic() < deadline time.sleep(0.05) p.send_signal(signal.SIGTERM) output, _ = p.communicate(timeout=10) assert p.returncode == 4, output report_path = output.strip().split(' report ', 1)[1] report = json.loads(pathlib.Path(report_path + '.json').read_text()) assert report['result'] == 'fail' and report['reason'] == 'cancelled' assert report['cancel_signal'] == signal.SIGTERM assert not pathlib.Path(f'/proc/{report["pid"]}').exists() finally: if p.poll() is None: p.terminate() p.wait(timeout=10) PY printf 'qemu negative gate fixtures passed\n'