git mirror - github.com/owenewans/holy - branch master
clone: https://src.holypkg.eu/holy/

file tests/installer-menu.py

import errno
import fcntl
import hashlib
import json
import os
import pty
import select
import signal
import struct
import subprocess
import sys
import termios
import time
from pathlib import Path


def run_menu(installer, holypkg, root, digest, config, plan, source_id):
    image = Path(config + '.img')
    with image.open('wb') as stream:
        stream.truncate(1 << 30)

    def disk_sample():
        with image.open('rb') as stream:
            head = hashlib.sha256(stream.read(1048576)).digest()
            stream.seek(-1048576, os.SEEK_END)
            tail = hashlib.sha256(stream.read(1048576)).digest()
        return head, tail

    pid, master = pty.fork()
    if pid == 0:
        os.execv(installer, [installer, '--menu', '--config', config,
                             '--plan', plan, '--holypkg', holypkg])
    output = bytearray()
    cursor = 0

    def expect(marker):
        nonlocal cursor
        needle = marker.encode()
        deadline = time.monotonic() + 30
        while time.monotonic() < deadline:
            position = output.find(needle, cursor)
            if position >= 0:
                cursor = position + len(needle)
                return
            ready, _, _ = select.select([master], [], [], 1)
            if not ready:
                continue
            try:
                chunk = os.read(master, 4096)
            except OSError as exc:
                if exc.errno == errno.EIO:
                    break
                raise
            if not chunk:
                break
            output.extend(chunk)
        raise AssertionError(f'missing {marker!r}: {output[-3000:]!r}')

    def send(value):
        os.write(master, value.encode())

    try:
        expect('Choice > ')
        send('2\n')
        expect('a add, d delete, x arch override, p setuid approval, s source, b back > ')
        send('b\n')
        expect('Choice > ')
        send('1\n')
        expect('Target root > ')
        send(root + '\n')
        expect('Choice > ')
        send('2\n')
        expect('a add, d delete, x arch override, p setuid approval, s source, b back > ')
        send('a\n')
        expect('SHA-256 > ')
        send(digest + '\n')
        expect('a add, d delete, x arch override, p setuid approval, s source, b back > ')
        send('x\n')
        expect('Number > ')
        send('1\n')
        expect('[arch override]')
        expect('a add, d delete, x arch override, p setuid approval, s source, b back > ')
        send('x\n')
        expect('Number > ')
        send('1\n')
        expect('a add, d delete, x arch override, p setuid approval, s source, b back > ')
        send('p\n')
        expect('Number > ')
        send('1\n')
        expect('[setuid approved]')
        expect('a add, d delete, x arch override, p setuid approval, s source, b back > ')
        send('p\n')
        expect('Number > ')
        send('1\n')
        expect('a add, d delete, x arch override, p setuid approval, s source, b back > ')
        send('s\n')
        expect('Number > ')
        send('1\n')
        expect('Source ID SHA-256 (empty clears) > ')
        send(source_id + '\n')
        expect('[source assigned]')
        expect('a add, d delete, x arch override, p setuid approval, s source, b back > ')
        send('b\n')
        expect('Packages: 1')
        expect('Choice > ')
        send('8\n')
        expect('Disk image (blank clears selection) > ')
        send(str(image) + '\n')
        expect('Choice > ')
        fcntl.ioctl(master, termios.TIOCSWINSZ, struct.pack('HHHH', 30, 90, 0, 0))
        os.kill(pid, signal.SIGWINCH)
        send('3\n')
        expect('plan-set generation')
        expect('Choice > ')
        send('4\n')
        expect('Config saved')
        expect('Choice > ')
        send('5\n')
        expect('holyinstall plan')
        expect('Choice > ')
        send('6\n')
        expect('Type yes to install > ')
        send('no\n')
        expect('Choice > ')
        assert not Path(root, 'usr/share/installer-fixture').exists()
        send('6\n')
        expect('Type yes to install > ')
        send('yes\n')
        expect('Package transaction complete')
        expect('Choice > ')
        before = disk_sample()
        send('9\n')
        expect('Disk plan ')
        expect('Choice > ')
        assert disk_sample() == before
        send('10\n')
        expect('Type the exact disk image path to erase > ')
        send(str(image) + '-wrong\n')
        expect('Disk apply failed (status 3)')
        expect('Choice > ')
        assert disk_sample() == before
        send('10\n')
        expect('Type the exact disk image path to erase > ')
        send(str(image) + '\n')
        expect('Disk image prepared')
        expect('Choice > ')
        send('7\n')
        _, status = os.waitpid(pid, 0)
        assert os.waitstatus_to_exitcode(status) == 0, output[-3000:]
        assert Path(root, 'usr/share/installer-fixture').read_text() == 'installed\n'
        assert f'source {digest} {source_id}\n' in Path(config).read_text()
        assert f'source-id {source_id}\n' in Path(root, 'var/lib/holypkg/installed', digest, 'state').read_text()
        assert Path(config).exists() and Path(plan).exists()
        disk_plan = Path(plan + '.disk')
        assert disk_plan.exists()
        assert Path(str(disk_plan) + '.journal').read_text().splitlines()[-1] == 'committed'
        table = json.loads(subprocess.check_output(['/usr/sbin/sfdisk', '--json', str(image)]))
        assert [(part['start'], part['size']) for part in table['partitiontable']['partitions']] == [
            (2048, 2048), (4096, 524288), (528384, 1566720)]
    finally:
        os.close(master)

    saved = Path(config).read_bytes()
    pid, master = pty.fork()
    if pid == 0:
        os.execv(installer, [installer, '--menu', '--config', config,
                             '--plan', plan, '--holypkg', holypkg])
    output = bytearray()
    deadline = time.monotonic() + 15
    while b'Packages: 1' not in output and time.monotonic() < deadline:
        ready, _, _ = select.select([master], [], [], 1)
        if ready:
            output.extend(os.read(master, 4096))
    assert b'Packages: 1' in output and root.encode() in output and str(image).encode() in output, output[-3000:]
    os.write(master, b'7\n')
    _, status = os.waitpid(pid, 0)
    os.close(master)
    assert os.waitstatus_to_exitcode(status) == 0
    assert Path(config).read_bytes() == saved


if __name__ == '__main__':
    run_menu(*sys.argv[1:])
    print('installer text menu fixtures passed')