diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-04-09 03:35:47 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-04-09 03:35:47 +0000 |
| commit | 68f0c8faf2e2b9024ee24ef97df1895bb117629c (patch) | |
| tree | bc018aa36ce25df9c2e7b16b4e6330899046a4f3 | |
| parent | a05c578640c1bef9b8704ffb060815e0f946d705 (diff) | |
| download | meta-virtualization-68f0c8faf2e2b9024ee24ef97df1895bb117629c.tar.gz | |
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 <bruce.ashfield@gmail.com>
| -rw-r--r-- | tests/pytest.ini | 2 | ||||
| -rw-r--r-- | tests/test_incus_runtime.py | 156 |
2 files changed, 158 insertions, 0 deletions
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 = | |||
| 10 | memres: marks tests that require memory resident mode | 10 | memres: marks tests that require memory resident mode |
| 11 | network: marks tests that require network access | 11 | network: marks tests that require network access |
| 12 | boot: marks tests that boot a full QEMU image (requires pexpect) | 12 | boot: marks tests that boot a full QEMU image (requires pexpect) |
| 13 | incus: marks incus runtime tests | ||
| 14 | k3s: marks k3s runtime tests | ||
| 13 | 15 | ||
| 14 | # Default options - include junit xml for CI and detailed output | 16 | # Default options - include junit xml for CI and detailed output |
| 15 | addopts = -v --tb=short --junitxml=/tmp/pytest-results.xml | 17 | 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 @@ | |||
| 1 | # SPDX-FileCopyrightText: Copyright (C) 2026 Bruce Ashfield | ||
| 2 | # | ||
| 3 | # SPDX-License-Identifier: MIT | ||
| 4 | """ | ||
| 5 | Incus runtime tests - boot container-image-host with incus and verify | ||
| 6 | system container management. | ||
| 7 | |||
| 8 | Build prerequisites (in local.conf): | ||
| 9 | require conf/distro/include/meta-virt-host.conf | ||
| 10 | require conf/distro/include/container-host-incus.conf | ||
| 11 | MACHINE = "qemux86-64" # or qemuarm64 | ||
| 12 | |||
| 13 | bitbake container-image-host | ||
| 14 | |||
| 15 | Run: | ||
| 16 | pytest tests/test_incus_runtime.py -v --machine qemux86-64 | ||
| 17 | |||
| 18 | Options: | ||
| 19 | --boot-timeout QEMU boot timeout (default: 120s) | ||
| 20 | --no-kvm Disable KVM acceleration | ||
| 21 | """ | ||
| 22 | |||
| 23 | import os | ||
| 24 | import re | ||
| 25 | import time | ||
| 26 | import pytest | ||
| 27 | |||
| 28 | try: | ||
| 29 | import pexpect | ||
| 30 | PEXPECT_AVAILABLE = True | ||
| 31 | except ImportError: | ||
| 32 | PEXPECT_AVAILABLE = False | ||
| 33 | |||
| 34 | |||
| 35 | pytestmark = [ | ||
| 36 | pytest.mark.skipif(not PEXPECT_AVAILABLE, reason="pexpect not installed"), | ||
| 37 | pytest.mark.incus, | ||
| 38 | ] | ||
| 39 | |||
| 40 | |||
| 41 | @pytest.fixture(scope="module") | ||
| 42 | def incus_qemu(request): | ||
| 43 | """Boot a QEMU VM with incus and return the pexpect session.""" | ||
| 44 | machine = request.config.getoption("--machine", default="qemux86-64") | ||
| 45 | boot_timeout = int(request.config.getoption("--boot-timeout", default="120")) | ||
| 46 | no_kvm = request.config.getoption("--no-kvm", default=False) | ||
| 47 | |||
| 48 | builddir = os.environ.get("BUILDDIR", os.path.expanduser("~/poky/build")) | ||
| 49 | |||
| 50 | kvm_opt = "" if no_kvm else "kvm" | ||
| 51 | cmd = f"runqemu {machine} nographic slirp {kvm_opt} qemuparams=\"-m 4096\"" | ||
| 52 | |||
| 53 | child = pexpect.spawn(f"bash -c 'source {builddir}/oe-init-build-env {builddir} >/dev/null 2>&1 && {cmd}'", | ||
| 54 | timeout=boot_timeout, encoding="utf-8", logfile=None) | ||
| 55 | |||
| 56 | # Wait for login prompt | ||
| 57 | child.expect(r"login:", timeout=boot_timeout) | ||
| 58 | child.sendline("root") | ||
| 59 | child.expect(r"root@.*[:~#]", timeout=30) | ||
| 60 | |||
| 61 | # Suppress shell integration escape sequences | ||
| 62 | child.sendline("export TERM=dumb") | ||
| 63 | child.expect(r"root@.*[:~#]", timeout=10) | ||
| 64 | |||
| 65 | yield child | ||
| 66 | |||
| 67 | # Cleanup | ||
| 68 | child.sendline("poweroff") | ||
| 69 | try: | ||
| 70 | child.expect(pexpect.EOF, timeout=30) | ||
| 71 | except pexpect.TIMEOUT: | ||
| 72 | child.terminate(force=True) | ||
| 73 | |||
| 74 | |||
| 75 | def run_cmd(child, cmd, timeout=60): | ||
| 76 | """Run a command and return the output.""" | ||
| 77 | marker = f"__MARKER_{time.monotonic_ns()}__" | ||
| 78 | child.sendline(f"{cmd}; echo {marker} $?") | ||
| 79 | child.expect(marker + r" (\d+)", timeout=timeout) | ||
| 80 | output = child.before.strip() | ||
| 81 | rc = int(child.match.group(1)) | ||
| 82 | # consume prompt | ||
| 83 | child.expect(r"root@.*[:~#]", timeout=10) | ||
| 84 | return output, rc | ||
| 85 | |||
| 86 | |||
| 87 | class TestIncusDaemon: | ||
| 88 | """Test that incusd starts and is functional.""" | ||
| 89 | |||
| 90 | def test_incusd_running(self, incus_qemu): | ||
| 91 | """incusd should be running via systemd.""" | ||
| 92 | output, rc = run_cmd(incus_qemu, "systemctl is-active incus.service") | ||
| 93 | assert "active" in output, f"incus.service not active: {output}" | ||
| 94 | |||
| 95 | def test_incus_admin_group(self, incus_qemu): | ||
| 96 | """incus-admin group should exist.""" | ||
| 97 | output, rc = run_cmd(incus_qemu, "getent group incus-admin") | ||
| 98 | assert rc == 0, "incus-admin group not found" | ||
| 99 | |||
| 100 | def test_incus_version(self, incus_qemu): | ||
| 101 | """incus client should report a version.""" | ||
| 102 | output, rc = run_cmd(incus_qemu, "incus version") | ||
| 103 | assert rc == 0, f"incus version failed: {output}" | ||
| 104 | |||
| 105 | |||
| 106 | class TestIncusInit: | ||
| 107 | """Test incus initialization.""" | ||
| 108 | |||
| 109 | def test_incus_init_minimal(self, incus_qemu): | ||
| 110 | """incus admin init --minimal should succeed.""" | ||
| 111 | output, rc = run_cmd(incus_qemu, "incus admin init --minimal", timeout=120) | ||
| 112 | assert rc == 0, f"incus admin init --minimal failed: {output}" | ||
| 113 | |||
| 114 | def test_incus_network_created(self, incus_qemu): | ||
| 115 | """Default network bridge should exist after init.""" | ||
| 116 | output, rc = run_cmd(incus_qemu, "incus network list") | ||
| 117 | assert rc == 0, f"incus network list failed: {output}" | ||
| 118 | |||
| 119 | |||
| 120 | class TestIncusContainer: | ||
| 121 | """Test launching and managing a container.""" | ||
| 122 | |||
| 123 | def test_launch_alpine(self, incus_qemu): | ||
| 124 | """Launch an Alpine container from the images: remote.""" | ||
| 125 | output, rc = run_cmd(incus_qemu, "incus launch images:alpine/edge incus-test1", | ||
| 126 | timeout=180) | ||
| 127 | assert rc == 0, f"incus launch failed: {output}" | ||
| 128 | |||
| 129 | def test_container_running(self, incus_qemu): | ||
| 130 | """The launched container should be in RUNNING state.""" | ||
| 131 | output, rc = run_cmd(incus_qemu, "incus list --format csv -c n,s") | ||
| 132 | assert rc == 0 | ||
| 133 | assert "incus-test1,RUNNING" in output.replace(" ", ""), \ | ||
| 134 | f"Container not running: {output}" | ||
| 135 | |||
| 136 | def test_exec_in_container(self, incus_qemu): | ||
| 137 | """Execute a command inside the container.""" | ||
| 138 | output, rc = run_cmd(incus_qemu, "incus exec incus-test1 -- cat /etc/os-release") | ||
| 139 | assert rc == 0 | ||
| 140 | assert "Alpine" in output, f"Unexpected os-release: {output}" | ||
| 141 | |||
| 142 | def test_stop_container(self, incus_qemu): | ||
| 143 | """Stop the container.""" | ||
| 144 | output, rc = run_cmd(incus_qemu, "incus stop incus-test1", timeout=30) | ||
| 145 | assert rc == 0, f"incus stop failed: {output}" | ||
| 146 | |||
| 147 | def test_delete_container(self, incus_qemu): | ||
| 148 | """Delete the stopped container.""" | ||
| 149 | output, rc = run_cmd(incus_qemu, "incus delete incus-test1", timeout=15) | ||
| 150 | assert rc == 0, f"incus delete failed: {output}" | ||
| 151 | |||
| 152 | def test_no_containers_remain(self, incus_qemu): | ||
| 153 | """No containers should remain after cleanup.""" | ||
| 154 | output, rc = run_cmd(incus_qemu, "incus list --format csv") | ||
| 155 | assert rc == 0 | ||
| 156 | assert "incus-test1" not in output | ||
