diff --git a/pmb/conftest.py b/pmb/conftest.py index 96be7cb9..077bb516 100644 --- a/pmb/conftest.py +++ b/pmb/conftest.py @@ -1,50 +1,55 @@ import os from pathlib import Path import pytest -from contextlib import contextmanager +import shutil -@contextmanager -def _fixture_context(val): - yield val +import pmb.core +from pmb.types import PmbArgs +from pmb.helpers.args import init as init_args -@pytest.fixture(scope="session") -def config_file_session(tmp_path_factory): +_testdir = Path(__file__).parent / "data/tests" + +@pytest.fixture +def config_file(tmp_path_factory): """Fixture to create a temporary pmbootstrap.cfg file.""" tmp_path = tmp_path_factory.mktemp("pmbootstrap") - file = tmp_path / "pmbootstrap.cfg" + out_file = tmp_path / "pmbootstrap.cfg" workdir = tmp_path / "work" 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] -""".format(workdir) - - open(file, "w").write(contents) - return file + open(out_file, "w").write(contents) + return out_file @pytest.fixture -def config_file(config_file_session): - """Fixture to create a temporary pmbootstrap.cfg file.""" - with _fixture_context(config_file_session) as val: - yield val +def device_package(config_file): + """Fixture to create a temporary deviceinfo file.""" + MOCK_DEVICE = "qemu-amd64" + 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) @@ -67,30 +72,44 @@ def setup_mock_ask(monkeypatch): 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 # custom context we set up here. -# @pytest.fixture(scope="session") -# def pmb_args(config_file_session): -# """This is (still) a hack, since a bunch of the codebase still -# expects some global state to be initialised. We do that here.""" +@pytest.fixture +def pmb_args(config_file, mock_context): + """This is (still) a hack, since a bunch of the codebase still + expects some global state to be initialised. We do that here.""" -# from pmb.types import PmbArgs -# from pmb.helpers.args import init as init_args + args = PmbArgs() + 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() -# args.config = config_file_session -# 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) + print("init_args") + init_args(args) @pytest.fixture def foreign_arch(): diff --git a/pmb/core/arch.py b/pmb/core/arch.py index a90af631..b667bddc 100644 --- a/pmb/core/arch.py +++ b/pmb/core/arch.py @@ -43,7 +43,9 @@ class Arch(enum.Enum): try: return Arch(arch) 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 @@ -52,7 +54,6 @@ class Arch(enum.Enum): "i686": Arch.x86, "x86_64": Arch.x86_64, "aarch64": Arch.aarch64, - "arm64": Arch.aarch64, "armv6l": Arch.armhf, "armv7l": Arch.armv7, "armv8l": Arch.armv7, @@ -93,9 +94,12 @@ class Arch(enum.Enum): Arch.x86: "x86", Arch.x86_64: "x86_64", Arch.armhf: "arm", + Arch.armv7: "arm", Arch.aarch64: "arm64", Arch.riscv64: "riscv", Arch.ppc64le: "powerpc", + Arch.ppc64: "powerpc", + Arch.ppc: "powerpc", Arch.s390x: "s390", } return mapping.get(self, self.value) @@ -105,12 +109,12 @@ class Arch(enum.Enum): Arch.x86: "i386", Arch.armhf: "arm", Arch.armv7: "arm", - Arch.ppc64le: "ppc64", } return mapping.get(self, self.value) def alpine_triple(self): + """Get the cross compiler triple for this architecture on Alpine.""" mapping = { Arch.aarch64: "aarch64-alpine-linux-musl", Arch.armel: "armv5-alpine-linux-musleabi", diff --git a/pmb/core/test_arch.py b/pmb/core/test_arch.py new file mode 100644 index 00000000..786c5ab9 --- /dev/null +++ b/pmb/core/test_arch.py @@ -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) diff --git a/pmb/core/test_chroot.py b/pmb/core/test_chroot.py new file mode 100644 index 00000000..011b11a5 --- /dev/null +++ b/pmb/core/test_chroot.py @@ -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'" + diff --git a/pmb/data/tests/APKBUILD.qemu-amd64 b/pmb/data/tests/APKBUILD.qemu-amd64 new file mode 100644 index 00000000..8703094c --- /dev/null +++ b/pmb/data/tests/APKBUILD.qemu-amd64 @@ -0,0 +1,77 @@ +# Reference: +# Maintainer: Minecrell +# Co-Maintainer: Oliver Smith +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 +" diff --git a/pmb/data/tests/deviceinfo.qemu-amd64 b/pmb/data/tests/deviceinfo.qemu-amd64 new file mode 100644 index 00000000..08631fc5 --- /dev/null +++ b/pmb/data/tests/deviceinfo.qemu-amd64 @@ -0,0 +1,27 @@ +# Reference: +# 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" \ No newline at end of file diff --git a/pmb/data/tests/pmbootstrap.cfg b/pmb/data/tests/pmbootstrap.cfg new file mode 100644 index 00000000..9f484fed --- /dev/null +++ b/pmb/data/tests/pmbootstrap.cfg @@ -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] \ No newline at end of file diff --git a/pmb/parse/deviceinfo.py b/pmb/parse/deviceinfo.py index 78337e99..82b6c874 100644 --- a/pmb/parse/deviceinfo.py +++ b/pmb/parse/deviceinfo.py @@ -254,7 +254,7 @@ class Deviceinfo: # if key not in Deviceinfo.__annotations__.keys(): # logging.warning(f"deviceinfo: {key} is not a known attribute") if key == "arch": - setattr(self, key, Arch(value)) + setattr(self, key, Arch.from_str(value)) else: setattr(self, key, value)