tests: add tests for Arch and Chroot types (MR 2252)

Add some exhaustive unit testing to validate that these types behave as
expected.

And fix a few bugs uncovered by the tests.

Signed-off-by: Caleb Connolly <caleb@postmarketos.org>
This commit is contained in:
Caleb Connolly 2024-06-13 04:35:10 +02:00 committed by Oliver Smith
parent 6087a9df8f
commit ca722b499e
No known key found for this signature in database
GPG key ID: 5AE7F5513E0885CB
8 changed files with 334 additions and 57 deletions

View file

@ -1,50 +1,55 @@
import os import os
from pathlib import Path from pathlib import Path
import pytest import pytest
from contextlib import contextmanager import shutil
@contextmanager import pmb.core
def _fixture_context(val): from pmb.types import PmbArgs
yield val from pmb.helpers.args import init as init_args
@pytest.fixture(scope="session") _testdir = Path(__file__).parent / "data/tests"
def config_file_session(tmp_path_factory):
@pytest.fixture
def config_file(tmp_path_factory):
"""Fixture to create a temporary pmbootstrap.cfg file.""" """Fixture to create a temporary pmbootstrap.cfg file."""
tmp_path = tmp_path_factory.mktemp("pmbootstrap") tmp_path = tmp_path_factory.mktemp("pmbootstrap")
file = tmp_path / "pmbootstrap.cfg" out_file = tmp_path / "pmbootstrap.cfg"
workdir = tmp_path / "work" workdir = tmp_path / "work"
workdir.mkdir() workdir.mkdir()
contents = """[pmbootstrap]
build_default_device_arch = True
ccache_size = 5G
device = qemu-amd64
extra_packages = neofetch,neovim,reboot-mode
hostname = qemu-amd64
is_default_channel = False
jobs = 8
kernel = edge
locale = C.UTF-8
ssh_keys = True
sudo_timer = True
systemd = always
timezone = Europe/Berlin
ui = gnome
work = {0}
[providers] file = _testdir / "pmbootstrap.cfg"
contents = open(file).read().format(workdir)
[mirrors] open(out_file, "w").write(contents)
""".format(workdir) return out_file
open(file, "w").write(contents)
return file
@pytest.fixture @pytest.fixture
def config_file(config_file_session): def device_package(config_file):
"""Fixture to create a temporary pmbootstrap.cfg file.""" """Fixture to create a temporary deviceinfo file."""
with _fixture_context(config_file_session) as val: MOCK_DEVICE = "qemu-amd64"
yield val pkgdir = config_file.parent / f"device-{MOCK_DEVICE}"
pkgdir.mkdir()
for file in ["APKBUILD", "deviceinfo"]:
shutil.copy(_testdir / f"{file}.{MOCK_DEVICE}",
pkgdir / file)
return pkgdir
@pytest.fixture
def mock_devices_find_path(device_package, monkeypatch):
"""Fixture to mock pmb.helpers.devices.find_path()"""
def mock_find_path(device, file=''):
print(f"mock_find_path({device}, {file})")
out = device_package / file
if not out.exists():
return None
return out
monkeypatch.setattr("pmb.helpers.devices.find_path", mock_find_path)
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -67,30 +72,44 @@ def setup_mock_ask(monkeypatch):
monkeypatch.setattr(pmb.helpers.cli, "ask", mock_ask) monkeypatch.setattr(pmb.helpers.cli, "ask", mock_ask)
# FIXME: get/set_context() is a bad hack :(
@pytest.fixture
def mock_context(monkeypatch):
"""Mock set_context() to bypass sanity checks. Ideally we would
mock get_context() as well, but since every submodule of pmb imports
it like "from pmb.core.context import get_context()", we can't
actually override it with monkeypatch.setattr(). So this is the
best we can do... set_context() is only called from one place and is
done so with the full namespace, so this works."""
def mock_set_context(ctx):
print(f"mock_set_context({ctx})")
setattr(pmb.core.context, "__context", ctx)
monkeypatch.setattr("pmb.core.context.set_context", mock_set_context)
# FIXME: get_context() at runtime somehow doesn't return the # FIXME: get_context() at runtime somehow doesn't return the
# custom context we set up here. # custom context we set up here.
# @pytest.fixture(scope="session") @pytest.fixture
# def pmb_args(config_file_session): def pmb_args(config_file, mock_context):
# """This is (still) a hack, since a bunch of the codebase still """This is (still) a hack, since a bunch of the codebase still
# expects some global state to be initialised. We do that here.""" expects some global state to be initialised. We do that here."""
# from pmb.types import PmbArgs args = PmbArgs()
# from pmb.helpers.args import init as init_args args.config = config_file
args.aports = None
args.timeout = 900
args.details_to_stdout = False
args.quiet = False
args.verbose = False
args.offline = False
args.action = "init"
args.cross = False
args.log = Path()
# args = PmbArgs() print("init_args")
# args.config = config_file_session init_args(args)
# args.aports = None
# args.timeout = 900
# args.details_to_stdout = False
# args.quiet = False
# args.verbose = False
# args.offline = False
# args.action = "init"
# args.cross = False
# args.log = Path()
# print("init_args")
# return init_args(args)
@pytest.fixture @pytest.fixture
def foreign_arch(): def foreign_arch():

View file

@ -43,7 +43,9 @@ class Arch(enum.Enum):
try: try:
return Arch(arch) return Arch(arch)
except ValueError: except ValueError:
raise ValueError(f"Invalid architecture: {arch}") raise ValueError(f"Invalid architecture: '{arch}',"
" expected something like:"
f" {', '.join([str(a) for a in Arch.supported()])}")
@staticmethod @staticmethod
@ -52,7 +54,6 @@ class Arch(enum.Enum):
"i686": Arch.x86, "i686": Arch.x86,
"x86_64": Arch.x86_64, "x86_64": Arch.x86_64,
"aarch64": Arch.aarch64, "aarch64": Arch.aarch64,
"arm64": Arch.aarch64,
"armv6l": Arch.armhf, "armv6l": Arch.armhf,
"armv7l": Arch.armv7, "armv7l": Arch.armv7,
"armv8l": Arch.armv7, "armv8l": Arch.armv7,
@ -93,9 +94,12 @@ class Arch(enum.Enum):
Arch.x86: "x86", Arch.x86: "x86",
Arch.x86_64: "x86_64", Arch.x86_64: "x86_64",
Arch.armhf: "arm", Arch.armhf: "arm",
Arch.armv7: "arm",
Arch.aarch64: "arm64", Arch.aarch64: "arm64",
Arch.riscv64: "riscv", Arch.riscv64: "riscv",
Arch.ppc64le: "powerpc", Arch.ppc64le: "powerpc",
Arch.ppc64: "powerpc",
Arch.ppc: "powerpc",
Arch.s390x: "s390", Arch.s390x: "s390",
} }
return mapping.get(self, self.value) return mapping.get(self, self.value)
@ -105,12 +109,12 @@ class Arch(enum.Enum):
Arch.x86: "i386", Arch.x86: "i386",
Arch.armhf: "arm", Arch.armhf: "arm",
Arch.armv7: "arm", Arch.armv7: "arm",
Arch.ppc64le: "ppc64",
} }
return mapping.get(self, self.value) return mapping.get(self, self.value)
def alpine_triple(self): def alpine_triple(self):
"""Get the cross compiler triple for this architecture on Alpine."""
mapping = { mapping = {
Arch.aarch64: "aarch64-alpine-linux-musl", Arch.aarch64: "aarch64-alpine-linux-musl",
Arch.armel: "armv5-alpine-linux-musleabi", Arch.armel: "armv5-alpine-linux-musleabi",

73
pmb/core/test_arch.py Normal file
View file

@ -0,0 +1,73 @@
import os
from pathlib import Path
from typing import Any
import pytest
from .arch import Arch
def test_valid_arches():
# Silly test
assert Arch.native().is_native()
# Test constructor interface
assert Arch.from_str("x86") == Arch.x86
assert Arch.from_str("x86_64") == Arch.x86_64
assert Arch.from_str("aarch64") == Arch.aarch64
assert Arch.from_str("armhf") == Arch.armhf
# Test from_machine_type
assert Arch.from_machine_type("i686") == Arch.x86
assert Arch.from_machine_type("x86_64") == Arch.x86_64
assert Arch.from_machine_type("aarch64") == Arch.aarch64
# Check supported architectures
assert Arch.x86 in Arch.supported()
assert Arch.x86_64 in Arch.supported()
assert Arch.aarch64 in Arch.supported()
assert Arch.armhf in Arch.supported()
assert Arch.armv7 in Arch.supported()
# kernel arch
assert Arch.x86.kernel() == "x86"
assert Arch.x86_64.kernel() == "x86_64"
assert Arch.aarch64.kernel() == "arm64" # The fun one
assert Arch.armhf.kernel() == "arm"
assert Arch.armv7.kernel() == "arm"
# qemu arch
assert Arch.x86.qemu() == "i386"
assert Arch.x86_64.qemu() == "x86_64"
assert Arch.aarch64.qemu() == "aarch64"
assert Arch.armhf.qemu() == "arm"
assert Arch.armv7.qemu() == "arm"
assert Arch.ppc64.qemu() == "ppc64"
assert Arch.ppc64le.qemu() == "ppc64le"
# Check that Arch.cpu_emulation_required() works
assert Arch.native() == Arch.x86_64 or Arch.x86_64.cpu_emulation_required()
assert Arch.native() == Arch.aarch64 or Arch.aarch64.cpu_emulation_required()
# Check that every arch has a target triple
for arch in Arch:
assert arch.alpine_triple() is not None
# Arch-as-path magic
assert Arch.aarch64 / Path("beep") == Path("aarch64/beep")
assert os.fspath(Arch.aarch64 / "beep") == "aarch64/beep"
assert isinstance(Arch.aarch64 / "beep", Path)
assert (Arch.aarch64 / "beep").name == "beep"
assert Path("boop") / Arch.aarch64 == Path("boop/aarch64")
def test_invalid_arches():
excinfo: Any
with pytest.raises(ValueError) as excinfo:
Arch.from_str("invalid")
assert "Invalid architecture: 'invalid'" in str(excinfo.value)
with pytest.raises(TypeError) as excinfo:
Arch.aarch64 / 5
assert "unsupported operand type(s) for /: 'Arch' and 'int'" in str(excinfo.value)
with pytest.raises(TypeError) as excinfo:
"bap" / Arch.aarch64
assert "unsupported operand type(s) for /: 'str' and 'Arch'" in str(excinfo.value)

57
pmb/core/test_chroot.py Normal file
View file

@ -0,0 +1,57 @@
import pytest
from .arch import Arch
from .context import get_context
from .chroot import Chroot, ChrootType
def test_valid_chroots(pmb_args, mock_devices_find_path):
"""Test that Chroot objects work as expected"""
work = get_context().config.work
chroot = Chroot.native()
assert chroot.type == ChrootType.NATIVE
assert chroot.name == ""
assert chroot.arch in Arch.supported()
assert not chroot.exists() # Shouldn't be created
assert chroot.path == work / "chroot_native"
assert str(chroot) == "native"
chroot = Chroot.buildroot(Arch.aarch64)
assert chroot.type == ChrootType.BUILDROOT
assert chroot.name == "aarch64"
assert chroot.arch == Arch.aarch64
assert not chroot.exists() # Shouldn't be created
assert chroot.path == work / "chroot_buildroot_aarch64"
assert str(chroot) == "buildroot_aarch64"
# FIXME: implicily assumes that we're mocking the qemu-amd64 deviceinfo
chroot = Chroot(ChrootType.ROOTFS, "qemu-amd64")
assert chroot.type == ChrootType.ROOTFS
assert chroot.name == "qemu-amd64"
assert chroot.arch == Arch.x86_64
assert not chroot.exists() # Shouldn't be created
assert chroot.path == work / "chroot_rootfs_qemu-amd64"
assert str(chroot) == "rootfs_qemu-amd64"
# mypy: ignore-errors
def test_invalid_chroots(pmb_args):
"""Test that we can't create invalid chroots."""
with pytest.raises(ValueError) as excinfo:
Chroot(ChrootType.BUILDROOT, "BAD_ARCH")
assert str(excinfo.value) == "Invalid buildroot suffix: 'BAD_ARCH'"
with pytest.raises(ValueError) as excinfo:
Chroot(ChrootType.NATIVE, "aarch64")
assert str(excinfo.value) == "The native suffix can't have a name but got: 'aarch64'"
with pytest.raises(ValueError) as excinfo:
Chroot("beep boop")
assert str(excinfo.value) == "Invalid chroot type: 'beep boop'"
with pytest.raises(ValueError) as excinfo:
Chroot(5)
assert str(excinfo.value) == "Invalid chroot type: '5'"

View file

@ -0,0 +1,77 @@
# Reference: <https://postmarketos.org/devicepkg>
# Maintainer: Minecrell <minecrell@minecrell.net>
# Co-Maintainer: Oliver Smith <ollieparanoid@postmarketos.org>
pkgname=device-qemu-amd64
pkgver=6
pkgrel=3
pkgdesc="Simulated device in QEMU (x86_64)"
url="https://postmarketos.org"
arch="x86_64"
license="MIT"
depends="postmarketos-base systemd-boot"
makedepends="devicepkg-dev"
# First kernel subpackage is default in pmbootstrap init!
subpackages="
$pkgname-kernel-lts:kernel_lts
$pkgname-kernel-virt:kernel_virt
$pkgname-kernel-edge:kernel_edge
$pkgname-kernel-none:kernel_none
$pkgname-mce
$pkgname-sway
"
source="
deviceinfo
modules-initfs
mce-display-blanking.conf
"
options="!check !archcheck"
build() {
devicepkg_build $startdir $pkgname
}
package() {
devicepkg_package $startdir $pkgname
}
mce() {
pkgdesc="Prevents screen blanking for UI's using mce (Glacier, Asteroid)"
install_if="$pkgname=$pkgver-r$pkgrel mce"
install -Dm644 "$srcdir"/mce-display-blanking.conf \
"$subpkgdir"/etc/mce/50display-blanking.conf
}
sway() {
install_if="$pkgname=$pkgver-r$pkgrel postmarketos-ui-sway"
depends="postmarketos-ui-sway-logo-key-alt"
mkdir "$subpkgdir"
}
kernel_none() {
pkgdesc="No kernel (does not boot! can be used during pmbootstrap testing to save time)"
devicepkg_subpackage_kernel $startdir $pkgname $subpkgname
}
kernel_virt() {
pkgdesc="Alpine Virt kernel (minimal, no audio/mouse/network)"
depends="linux-virt"
devicepkg_subpackage_kernel $startdir $pkgname $subpkgname
}
kernel_lts() {
pkgdesc="Alpine LTS kernel (recommended)"
depends="linux-lts linux-firmware-none"
devicepkg_subpackage_kernel $startdir $pkgname $subpkgname
}
kernel_edge() {
pkgdesc="Alpine Edge kernel"
depends="linux-edge linux-firmware-none"
devicepkg_subpackage_kernel $startdir $pkgname $subpkgname
}
sha512sums="
94f8f9ad44ba6ffe55a07a5bce11351f89a7d97e0c52931c573ea21ff7416cc3ede800da94611e04ebfed6914f8d5edb9614f8c0847b53b09ae168a8607f6195 deviceinfo
29766094e64a7ce881c8e96433203ea538057b8fd1d577fc69b9add6bc1217af04ddf60cbcf82333811c627897eda7537b0b1f862899e1fdfd93403b3f6425d7 modules-initfs
99d32eed6c5cda59e91516e982c5bd5165ff718133e2411a0dbba04e2057d1dfad49a75e5cc67140d0e0adcbe1383671bd2892335929b782a5b19f5472e635ad mce-display-blanking.conf
"

View file

@ -0,0 +1,27 @@
# Reference: <https://postmarketos.org/deviceinfo>
# Please use double quotes only. You can source this file in shell scripts.
deviceinfo_format_version="0"
deviceinfo_name="QEMU amd64"
deviceinfo_manufacturer="QEMU"
deviceinfo_codename="qemu-amd64"
deviceinfo_arch="x86_64"
# Device related
deviceinfo_gpu_accelerated="true"
deviceinfo_chassis="vm"
deviceinfo_keyboard="true"
deviceinfo_external_storage="true"
deviceinfo_screen_width="1024"
deviceinfo_screen_height="768"
deviceinfo_getty="ttyS0;115200"
deviceinfo_dev_internal_storage="/dev/vdb"
deviceinfo_dev_internal_storage_repartition="true"
# Bootloader related
deviceinfo_flash_method="none"
deviceinfo_kernel_cmdline="console=tty1 console=ttyS0 PMOS_NO_OUTPUT_REDIRECT PMOS_FORCE_PARTITION_RESIZE"
deviceinfo_disable_dhcpd="true"
deviceinfo_partition_type="gpt"
deviceinfo_boot_filesystem="fat32"
deviceinfo_generate_systemd_boot="true"

View file

@ -0,0 +1,20 @@
[pmbootstrap]
build_default_device_arch = True
ccache_size = 5G
device = qemu-amd64
extra_packages = neofetch,neovim,reboot-mode
hostname = qemu-amd64
is_default_channel = False
jobs = 8
kernel = edge
locale = C.UTF-8
ssh_keys = True
sudo_timer = True
systemd = always
timezone = Europe/Berlin
ui = gnome
work = {0}
[providers]
[mirrors]

View file

@ -254,7 +254,7 @@ class Deviceinfo:
# if key not in Deviceinfo.__annotations__.keys(): # if key not in Deviceinfo.__annotations__.keys():
# logging.warning(f"deviceinfo: {key} is not a known attribute") # logging.warning(f"deviceinfo: {key} is not a known attribute")
if key == "arch": if key == "arch":
setattr(self, key, Arch(value)) setattr(self, key, Arch.from_str(value))
else: else:
setattr(self, key, value) setattr(self, key, value)