From 68f0c8faf2e2b9024ee24ef97df1895bb117629c Mon Sep 17 00:00:00 2001 From: Bruce Ashfield Date: Thu, 9 Apr 2026 03:35:47 +0000 Subject: incus: add runtime test suite pexpect-based tests covering: - Daemon startup via systemd - incus-admin group creation - incus admin init --minimal - Alpine container launch, exec, stop, delete Run: pytest tests/test_incus_runtime.py -v --machine qemux86-64 Signed-off-by: Bruce Ashfield --- tests/pytest.ini | 2 + tests/test_incus_runtime.py | 156 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 tests/test_incus_runtime.py diff --git a/tests/pytest.ini b/tests/pytest.ini index b0a51f7e..6d756a28 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -10,6 +10,8 @@ markers = memres: marks tests that require memory resident mode network: marks tests that require network access boot: marks tests that boot a full QEMU image (requires pexpect) + incus: marks incus runtime tests + k3s: marks k3s runtime tests # Default options - include junit xml for CI and detailed output addopts = -v --tb=short --junitxml=/tmp/pytest-results.xml diff --git a/tests/test_incus_runtime.py b/tests/test_incus_runtime.py new file mode 100644 index 00000000..6de4bd5c --- /dev/null +++ b/tests/test_incus_runtime.py @@ -0,0 +1,156 @@ +# SPDX-FileCopyrightText: Copyright (C) 2026 Bruce Ashfield +# +# SPDX-License-Identifier: MIT +""" +Incus runtime tests - boot container-image-host with incus and verify +system container management. + +Build prerequisites (in local.conf): + require conf/distro/include/meta-virt-host.conf + require conf/distro/include/container-host-incus.conf + MACHINE = "qemux86-64" # or qemuarm64 + + bitbake container-image-host + +Run: + pytest tests/test_incus_runtime.py -v --machine qemux86-64 + +Options: + --boot-timeout QEMU boot timeout (default: 120s) + --no-kvm Disable KVM acceleration +""" + +import os +import re +import time +import pytest + +try: + import pexpect + PEXPECT_AVAILABLE = True +except ImportError: + PEXPECT_AVAILABLE = False + + +pytestmark = [ + pytest.mark.skipif(not PEXPECT_AVAILABLE, reason="pexpect not installed"), + pytest.mark.incus, +] + + +@pytest.fixture(scope="module") +def incus_qemu(request): + """Boot a QEMU VM with incus and return the pexpect session.""" + machine = request.config.getoption("--machine", default="qemux86-64") + boot_timeout = int(request.config.getoption("--boot-timeout", default="120")) + no_kvm = request.config.getoption("--no-kvm", default=False) + + builddir = os.environ.get("BUILDDIR", os.path.expanduser("~/poky/build")) + + kvm_opt = "" if no_kvm else "kvm" + cmd = f"runqemu {machine} nographic slirp {kvm_opt} qemuparams=\"-m 4096\"" + + child = pexpect.spawn(f"bash -c 'source {builddir}/oe-init-build-env {builddir} >/dev/null 2>&1 && {cmd}'", + timeout=boot_timeout, encoding="utf-8", logfile=None) + + # Wait for login prompt + child.expect(r"login:", timeout=boot_timeout) + child.sendline("root") + child.expect(r"root@.*[:~#]", timeout=30) + + # Suppress shell integration escape sequences + child.sendline("export TERM=dumb") + child.expect(r"root@.*[:~#]", timeout=10) + + yield child + + # Cleanup + child.sendline("poweroff") + try: + child.expect(pexpect.EOF, timeout=30) + except pexpect.TIMEOUT: + child.terminate(force=True) + + +def run_cmd(child, cmd, timeout=60): + """Run a command and return the output.""" + marker = f"__MARKER_{time.monotonic_ns()}__" + child.sendline(f"{cmd}; echo {marker} $?") + child.expect(marker + r" (\d+)", timeout=timeout) + output = child.before.strip() + rc = int(child.match.group(1)) + # consume prompt + child.expect(r"root@.*[:~#]", timeout=10) + return output, rc + + +class TestIncusDaemon: + """Test that incusd starts and is functional.""" + + def test_incusd_running(self, incus_qemu): + """incusd should be running via systemd.""" + output, rc = run_cmd(incus_qemu, "systemctl is-active incus.service") + assert "active" in output, f"incus.service not active: {output}" + + def test_incus_admin_group(self, incus_qemu): + """incus-admin group should exist.""" + output, rc = run_cmd(incus_qemu, "getent group incus-admin") + assert rc == 0, "incus-admin group not found" + + def test_incus_version(self, incus_qemu): + """incus client should report a version.""" + output, rc = run_cmd(incus_qemu, "incus version") + assert rc == 0, f"incus version failed: {output}" + + +class TestIncusInit: + """Test incus initialization.""" + + def test_incus_init_minimal(self, incus_qemu): + """incus admin init --minimal should succeed.""" + output, rc = run_cmd(incus_qemu, "incus admin init --minimal", timeout=120) + assert rc == 0, f"incus admin init --minimal failed: {output}" + + def test_incus_network_created(self, incus_qemu): + """Default network bridge should exist after init.""" + output, rc = run_cmd(incus_qemu, "incus network list") + assert rc == 0, f"incus network list failed: {output}" + + +class TestIncusContainer: + """Test launching and managing a container.""" + + def test_launch_alpine(self, incus_qemu): + """Launch an Alpine container from the images: remote.""" + output, rc = run_cmd(incus_qemu, "incus launch images:alpine/edge incus-test1", + timeout=180) + assert rc == 0, f"incus launch failed: {output}" + + def test_container_running(self, incus_qemu): + """The launched container should be in RUNNING state.""" + output, rc = run_cmd(incus_qemu, "incus list --format csv -c n,s") + assert rc == 0 + assert "incus-test1,RUNNING" in output.replace(" ", ""), \ + f"Container not running: {output}" + + def test_exec_in_container(self, incus_qemu): + """Execute a command inside the container.""" + output, rc = run_cmd(incus_qemu, "incus exec incus-test1 -- cat /etc/os-release") + assert rc == 0 + assert "Alpine" in output, f"Unexpected os-release: {output}" + + def test_stop_container(self, incus_qemu): + """Stop the container.""" + output, rc = run_cmd(incus_qemu, "incus stop incus-test1", timeout=30) + assert rc == 0, f"incus stop failed: {output}" + + def test_delete_container(self, incus_qemu): + """Delete the stopped container.""" + output, rc = run_cmd(incus_qemu, "incus delete incus-test1", timeout=15) + assert rc == 0, f"incus delete failed: {output}" + + def test_no_containers_remain(self, incus_qemu): + """No containers should remain after cleanup.""" + output, rc = run_cmd(incus_qemu, "incus list --format csv") + assert rc == 0 + assert "incus-test1" not in output -- cgit v1.2.3-54-g00ecf