diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-06-05 01:01:59 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-06-12 02:58:55 +0000 |
| commit | 93440a018d098f34f0c8f6c1a4ad8c66a914c225 (patch) | |
| tree | c6b90a1974f4a18aa84ffa90be36eb7e5f53c90e | |
| parent | 5e958db73270a60c6ac04b19aec8f66b6ba474e2 (diff) | |
| download | meta-virtualization-93440a018d098f34f0c8f6c1a4ad8c66a914c225.tar.gz | |
tests: make k3s, incus, and xen tests self-contained
These boot tests previously required manual local.conf changes to set
CONTAINER_PROFILE, DISTRO_FEATURES, and other variables before the
image could be built and tested. This meant complete test coverage
required editing local.conf between runs.
Add build fixtures that use bitbake -R with a temporary conf file to
inject the required variables automatically:
- k3s: builds container-image-host with CONTAINER_PROFILE=k3s-host
- incus: builds container-image-host with CONTAINER_PROFILE=incus
- xen: builds xen-image-minimal with xen/vxn DISTRO_FEATURES
Each test module now builds its own image before booting, using the
same run_bitbake + extra_vars pattern established in
test_container_cross_install.py.
Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
| -rw-r--r-- | tests/test_incus_runtime.py | 65 | ||||
| -rw-r--r-- | tests/test_k3s_runtime.py | 81 | ||||
| -rw-r--r-- | tests/test_xen_runtime.py | 62 |
3 files changed, 159 insertions, 49 deletions
diff --git a/tests/test_incus_runtime.py b/tests/test_incus_runtime.py index 6de4bd5c..ea5eb1fc 100644 --- a/tests/test_incus_runtime.py +++ b/tests/test_incus_runtime.py | |||
| @@ -5,15 +5,11 @@ | |||
| 5 | Incus runtime tests - boot container-image-host with incus and verify | 5 | Incus runtime tests - boot container-image-host with incus and verify |
| 6 | system container management. | 6 | system container management. |
| 7 | 7 | ||
| 8 | Build prerequisites (in local.conf): | 8 | The tests automatically build container-image-host with CONTAINER_PROFILE=incus |
| 9 | require conf/distro/include/meta-virt-host.conf | 9 | before booting. No local.conf changes needed. |
| 10 | require conf/distro/include/container-host-incus.conf | ||
| 11 | MACHINE = "qemux86-64" # or qemuarm64 | ||
| 12 | |||
| 13 | bitbake container-image-host | ||
| 14 | 10 | ||
| 15 | Run: | 11 | Run: |
| 16 | pytest tests/test_incus_runtime.py -v --machine qemux86-64 | 12 | pytest tests/test_incus_runtime.py -v --poky-dir /opt/bruce/poky |
| 17 | 13 | ||
| 18 | Options: | 14 | Options: |
| 19 | --boot-timeout QEMU boot timeout (default: 120s) | 15 | --boot-timeout QEMU boot timeout (default: 120s) |
| @@ -22,8 +18,11 @@ Options: | |||
| 22 | 18 | ||
| 23 | import os | 19 | import os |
| 24 | import re | 20 | import re |
| 21 | import subprocess | ||
| 22 | import tempfile | ||
| 25 | import time | 23 | import time |
| 26 | import pytest | 24 | import pytest |
| 25 | from pathlib import Path | ||
| 27 | 26 | ||
| 28 | try: | 27 | try: |
| 29 | import pexpect | 28 | import pexpect |
| @@ -38,19 +37,61 @@ pytestmark = [ | |||
| 38 | ] | 37 | ] |
| 39 | 38 | ||
| 40 | 39 | ||
| 40 | def _run_bitbake(build_dir, recipe, extra_vars=None, timeout=3600): | ||
| 41 | """Run bitbake with optional variable overrides via -R conf file.""" | ||
| 42 | bb_cmd = "bitbake" | ||
| 43 | conf_file = None | ||
| 44 | if extra_vars: | ||
| 45 | conf_file = tempfile.NamedTemporaryFile( | ||
| 46 | mode='w', suffix='.conf', prefix='pytest-incus-', | ||
| 47 | dir=str(build_dir / "conf"), delete=False) | ||
| 48 | for var, val in extra_vars.items(): | ||
| 49 | conf_file.write(f'{var} = "{val}"\n') | ||
| 50 | conf_file.close() | ||
| 51 | bb_cmd += f" -R {conf_file.name}" | ||
| 52 | bb_cmd += f" {recipe}" | ||
| 53 | poky_dir = build_dir.parent | ||
| 54 | full_cmd = f"bash -c 'cd {poky_dir} && source oe-init-build-env {build_dir} >/dev/null 2>&1 && {bb_cmd}'" | ||
| 55 | try: | ||
| 56 | return subprocess.run(full_cmd, shell=True, cwd=build_dir, | ||
| 57 | timeout=timeout, capture_output=True, text=True) | ||
| 58 | finally: | ||
| 59 | if conf_file: | ||
| 60 | os.unlink(conf_file.name) | ||
| 61 | |||
| 62 | |||
| 63 | @pytest.fixture(scope="module") | ||
| 64 | def incus_image(request): | ||
| 65 | """Build container-image-host with incus profile.""" | ||
| 66 | poky_dir = Path(request.config.getoption("--poky-dir")) | ||
| 67 | bd = request.config.getoption("--build-dir") | ||
| 68 | build_dir = Path(bd) if bd else poky_dir / "build" | ||
| 69 | result = _run_bitbake( | ||
| 70 | build_dir, "container-image-host", | ||
| 71 | extra_vars={ | ||
| 72 | "CONTAINER_PROFILE": "incus", | ||
| 73 | "DISTRO_FEATURES:append": " virtualization", | ||
| 74 | }, | ||
| 75 | ) | ||
| 76 | if result.returncode != 0: | ||
| 77 | pytest.fail(f"Incus image build failed: {result.stderr}") | ||
| 78 | |||
| 79 | |||
| 41 | @pytest.fixture(scope="module") | 80 | @pytest.fixture(scope="module") |
| 42 | def incus_qemu(request): | 81 | def incus_qemu(request, incus_image): |
| 43 | """Boot a QEMU VM with incus and return the pexpect session.""" | 82 | """Build incus image, boot a QEMU VM, and return the pexpect session.""" |
| 44 | machine = request.config.getoption("--machine", default="qemux86-64") | 83 | machine = request.config.getoption("--machine", default="qemux86-64") |
| 45 | boot_timeout = int(request.config.getoption("--boot-timeout", default="120")) | 84 | boot_timeout = int(request.config.getoption("--boot-timeout", default="120")) |
| 46 | no_kvm = request.config.getoption("--no-kvm", default=False) | 85 | no_kvm = request.config.getoption("--no-kvm", default=False) |
| 47 | 86 | ||
| 48 | builddir = os.environ.get("BUILDDIR", os.path.expanduser("~/poky/build")) | 87 | poky_dir = Path(request.config.getoption("--poky-dir")) |
| 88 | bd = request.config.getoption("--build-dir") | ||
| 89 | builddir = str(Path(bd) if bd else poky_dir / "build") | ||
| 49 | 90 | ||
| 50 | kvm_opt = "" if no_kvm else "kvm" | 91 | kvm_opt = "" if no_kvm else "kvm" |
| 51 | cmd = f"runqemu {machine} nographic slirp {kvm_opt} qemuparams=\"-m 4096\"" | 92 | cmd = f"runqemu {machine} container-image-host ext4 nographic slirp {kvm_opt} qemuparams=\"-m 4096\"" |
| 52 | 93 | ||
| 53 | child = pexpect.spawn(f"bash -c 'source {builddir}/oe-init-build-env {builddir} >/dev/null 2>&1 && {cmd}'", | 94 | child = pexpect.spawn(f"bash -c 'cd {poky_dir} && source oe-init-build-env {builddir} >/dev/null 2>&1 && {cmd}'", |
| 54 | timeout=boot_timeout, encoding="utf-8", logfile=None) | 95 | timeout=boot_timeout, encoding="utf-8", logfile=None) |
| 55 | 96 | ||
| 56 | # Wait for login prompt | 97 | # Wait for login prompt |
diff --git a/tests/test_k3s_runtime.py b/tests/test_k3s_runtime.py index aa0d443f..662bdf4c 100644 --- a/tests/test_k3s_runtime.py +++ b/tests/test_k3s_runtime.py | |||
| @@ -8,22 +8,18 @@ Single-node tests verify k3s server start, node readiness, and basic pod | |||
| 8 | deployment. Multi-node tests use QEMU socket networking to connect two VMs | 8 | deployment. Multi-node tests use QEMU socket networking to connect two VMs |
| 9 | on a shared L2 segment and verify agent join + multi-node scheduling. | 9 | on a shared L2 segment and verify agent join + multi-node scheduling. |
| 10 | 10 | ||
| 11 | Build prerequisites (in local.conf): | 11 | The tests automatically build container-image-host with CONTAINER_PROFILE=k3s-host |
| 12 | require conf/distro/include/meta-virt-host.conf | 12 | before booting. No local.conf changes needed. |
| 13 | require conf/distro/include/container-host-k3s.conf | ||
| 14 | MACHINE = "qemux86-64" # or qemuarm64 | ||
| 15 | |||
| 16 | bitbake container-image-host | ||
| 17 | 13 | ||
| 18 | Run: | 14 | Run: |
| 19 | # Single-node only | 15 | # Single-node only |
| 20 | pytest tests/test_k3s_runtime.py -v -k "not multinode" --machine qemux86-64 | 16 | pytest tests/test_k3s_runtime.py -v -k "not multinode" --poky-dir /opt/bruce/poky |
| 21 | 17 | ||
| 22 | # Multi-node only | 18 | # Multi-node only |
| 23 | pytest tests/test_k3s_runtime.py -v -k "multinode" --machine qemux86-64 | 19 | pytest tests/test_k3s_runtime.py -v -k "multinode" --poky-dir /opt/bruce/poky |
| 24 | 20 | ||
| 25 | # All tests | 21 | # All tests |
| 26 | pytest tests/test_k3s_runtime.py -v --machine qemux86-64 | 22 | pytest tests/test_k3s_runtime.py -v --poky-dir /opt/bruce/poky |
| 27 | 23 | ||
| 28 | Options: | 24 | Options: |
| 29 | --k3s-timeout Overall k3s readiness timeout (default: 300s) | 25 | --k3s-timeout Overall k3s readiness timeout (default: 300s) |
| @@ -43,6 +39,8 @@ Notes: | |||
| 43 | import os | 39 | import os |
| 44 | import re | 40 | import re |
| 45 | import shutil | 41 | import shutil |
| 42 | import subprocess | ||
| 43 | import tempfile | ||
| 46 | import time | 44 | import time |
| 47 | import pytest | 45 | import pytest |
| 48 | from pathlib import Path | 46 | from pathlib import Path |
| @@ -290,6 +288,39 @@ class K3sRunner: | |||
| 290 | # Fixtures | 288 | # Fixtures |
| 291 | # ============================================================================ | 289 | # ============================================================================ |
| 292 | 290 | ||
| 291 | def run_bitbake(build_dir, recipe, extra_vars=None, timeout=3600): | ||
| 292 | """Run a bitbake command within the Yocto environment.""" | ||
| 293 | import tempfile | ||
| 294 | |||
| 295 | bb_cmd = "bitbake" | ||
| 296 | |||
| 297 | conf_file = None | ||
| 298 | if extra_vars: | ||
| 299 | conf_file = tempfile.NamedTemporaryFile( | ||
| 300 | mode='w', suffix='.conf', prefix='pytest-k3s-', | ||
| 301 | dir=str(build_dir / "conf"), delete=False) | ||
| 302 | for var, val in extra_vars.items(): | ||
| 303 | conf_file.write(f'{var} = "{val}"\n') | ||
| 304 | conf_file.close() | ||
| 305 | bb_cmd += f" -R {conf_file.name}" | ||
| 306 | |||
| 307 | bb_cmd += f" {recipe}" | ||
| 308 | |||
| 309 | poky_dir = build_dir.parent | ||
| 310 | full_cmd = f"bash -c 'cd {poky_dir} && source oe-init-build-env {build_dir} >/dev/null 2>&1 && {bb_cmd}'" | ||
| 311 | |||
| 312 | try: | ||
| 313 | result = subprocess.run( | ||
| 314 | full_cmd, shell=True, cwd=build_dir, | ||
| 315 | timeout=timeout, capture_output=True, text=True, | ||
| 316 | ) | ||
| 317 | finally: | ||
| 318 | if conf_file: | ||
| 319 | os.unlink(conf_file.name) | ||
| 320 | |||
| 321 | return result | ||
| 322 | |||
| 323 | |||
| 293 | @pytest.fixture(scope="module") | 324 | @pytest.fixture(scope="module") |
| 294 | def poky_dir(request): | 325 | def poky_dir(request): |
| 295 | """Path to poky directory.""" | 326 | """Path to poky directory.""" |
| @@ -325,20 +356,28 @@ def k3s_timeout(request): | |||
| 325 | 356 | ||
| 326 | 357 | ||
| 327 | @pytest.fixture(scope="module") | 358 | @pytest.fixture(scope="module") |
| 328 | def k3s_session(request, poky_dir, build_dir, machine): | 359 | def k3s_image(build_dir): |
| 360 | """Build container-image-host with k3s profile.""" | ||
| 361 | result = run_bitbake( | ||
| 362 | build_dir, "container-image-host", | ||
| 363 | extra_vars={ | ||
| 364 | "CONTAINER_PROFILE": "k3s-host", | ||
| 365 | "DISTRO_FEATURES:append": " k3s virtualization", | ||
| 366 | }, | ||
| 367 | ) | ||
| 368 | if result.returncode != 0: | ||
| 369 | pytest.fail(f"K3s image build failed: {result.stderr}") | ||
| 370 | |||
| 371 | |||
| 372 | @pytest.fixture(scope="module") | ||
| 373 | def k3s_session(request, poky_dir, build_dir, machine, k3s_image): | ||
| 329 | """ | 374 | """ |
| 330 | Module-scoped fixture that boots container-image-host once for all | 375 | Module-scoped fixture that builds container-image-host with k3s profile, |
| 331 | single-node k3s tests. Uses runqemu for single-node tests. | 376 | boots it, and provides a session for all single-node k3s tests. |
| 332 | """ | 377 | """ |
| 333 | if not PEXPECT_AVAILABLE: | 378 | if not PEXPECT_AVAILABLE: |
| 334 | pytest.skip("pexpect not installed. Run: pip install pexpect") | 379 | pytest.skip("pexpect not installed. Run: pip install pexpect") |
| 335 | 380 | ||
| 336 | deploy_dir = build_dir / "tmp" / "deploy" / "images" / machine | ||
| 337 | ext4_files = list(deploy_dir.glob("container-image-host-*.rootfs.ext4")) | ||
| 338 | if not ext4_files: | ||
| 339 | pytest.skip( | ||
| 340 | f"container-image-host ext4 image not found in {deploy_dir}") | ||
| 341 | |||
| 342 | timeout = request.config.getoption("--boot-timeout") | 381 | timeout = request.config.getoption("--boot-timeout") |
| 343 | use_kvm = not request.config.getoption("--no-kvm") | 382 | use_kvm = not request.config.getoption("--no-kvm") |
| 344 | 383 | ||
| @@ -356,10 +395,10 @@ def k3s_session(request, poky_dir, build_dir, machine): | |||
| 356 | 395 | ||
| 357 | 396 | ||
| 358 | @pytest.fixture(scope="module") | 397 | @pytest.fixture(scope="module") |
| 359 | def k3s_multinode(request, poky_dir, build_dir, machine): | 398 | def k3s_multinode(request, poky_dir, build_dir, machine, k3s_image): |
| 360 | """ | 399 | """ |
| 361 | Module-scoped fixture that boots two VMs connected via QEMU socket | 400 | Module-scoped fixture that builds with k3s profile, then boots two VMs |
| 362 | networking for multi-node k3s testing. | 401 | connected via QEMU socket networking for multi-node k3s testing. |
| 363 | 402 | ||
| 364 | Uses direct QEMU launch (not runqemu) since runqemu can only run | 403 | Uses direct QEMU launch (not runqemu) since runqemu can only run |
| 365 | one VM at a time. Creates a copy of the rootfs for the agent VM. | 404 | one VM at a time. Creates a copy of the rootfs for the agent VM. |
diff --git a/tests/test_xen_runtime.py b/tests/test_xen_runtime.py index 697c57ee..790a25bf 100644 --- a/tests/test_xen_runtime.py +++ b/tests/test_xen_runtime.py | |||
| @@ -7,21 +7,11 @@ Xen runtime boot tests - boot xen-image-minimal and verify hypervisor. | |||
| 7 | These tests boot an actual Xen Dom0 image via runqemu, verify the | 7 | These tests boot an actual Xen Dom0 image via runqemu, verify the |
| 8 | hypervisor is functional, check guest bundling, and exercise vxn/containerd. | 8 | hypervisor is functional, check guest bundling, and exercise vxn/containerd. |
| 9 | 9 | ||
| 10 | Build prerequisites (minimum for Dom0 boot tests): | 10 | The tests automatically build xen-image-minimal with the required |
| 11 | DISTRO_FEATURES:append = " xen systemd" | 11 | DISTRO_FEATURES before booting. No local.conf changes needed. |
| 12 | MACHINE = "qemux86-64" # or qemuarm64 | ||
| 13 | bitbake xen-image-minimal | ||
| 14 | |||
| 15 | For guest bundling tests: | ||
| 16 | IMAGE_INSTALL:append:pn-xen-image-minimal = " alpine-xen-guest-bundle" | ||
| 17 | |||
| 18 | For vxn/containerd tests: | ||
| 19 | DISTRO_FEATURES:append = " virtualization vcontainer vxn" | ||
| 20 | IMAGE_INSTALL:append:pn-xen-image-minimal = " vxn" | ||
| 21 | BBMULTICONFIG = "vruntime-aarch64 vruntime-x86-64" | ||
| 22 | 12 | ||
| 23 | Run with: | 13 | Run with: |
| 24 | pytest tests/test_xen_runtime.py -v --machine qemux86-64 | 14 | pytest tests/test_xen_runtime.py -v --poky-dir /opt/bruce/poky |
| 25 | 15 | ||
| 26 | Skip network-dependent tests: | 16 | Skip network-dependent tests: |
| 27 | pytest tests/test_xen_runtime.py -v -m "boot and not network" | 17 | pytest tests/test_xen_runtime.py -v -m "boot and not network" |
| @@ -33,7 +23,10 @@ Custom paths and longer timeout: | |||
| 33 | --boot-timeout 180 | 23 | --boot-timeout 180 |
| 34 | """ | 24 | """ |
| 35 | 25 | ||
| 26 | import os | ||
| 36 | import re | 27 | import re |
| 28 | import subprocess | ||
| 29 | import tempfile | ||
| 37 | import time | 30 | import time |
| 38 | import pytest | 31 | import pytest |
| 39 | from pathlib import Path | 32 | from pathlib import Path |
| @@ -50,6 +43,29 @@ except ImportError: | |||
| 50 | # are defined in conftest.py to avoid conflicts with other test files. | 43 | # are defined in conftest.py to avoid conflicts with other test files. |
| 51 | 44 | ||
| 52 | 45 | ||
| 46 | def _run_bitbake(build_dir, recipe, extra_vars=None, timeout=3600): | ||
| 47 | """Run bitbake with optional variable overrides via -R conf file.""" | ||
| 48 | bb_cmd = "bitbake" | ||
| 49 | conf_file = None | ||
| 50 | if extra_vars: | ||
| 51 | conf_file = tempfile.NamedTemporaryFile( | ||
| 52 | mode='w', suffix='.conf', prefix='pytest-xen-', | ||
| 53 | dir=str(build_dir / "conf"), delete=False) | ||
| 54 | for var, val in extra_vars.items(): | ||
| 55 | conf_file.write(f'{var} = "{val}"\n') | ||
| 56 | conf_file.close() | ||
| 57 | bb_cmd += f" -R {conf_file.name}" | ||
| 58 | bb_cmd += f" {recipe}" | ||
| 59 | poky_dir = build_dir.parent | ||
| 60 | full_cmd = f"bash -c 'cd {poky_dir} && source oe-init-build-env {build_dir} >/dev/null 2>&1 && {bb_cmd}'" | ||
| 61 | try: | ||
| 62 | return subprocess.run(full_cmd, shell=True, cwd=build_dir, | ||
| 63 | timeout=timeout, capture_output=True, text=True) | ||
| 64 | finally: | ||
| 65 | if conf_file: | ||
| 66 | os.unlink(conf_file.name) | ||
| 67 | |||
| 68 | |||
| 53 | class XenRunner: | 69 | class XenRunner: |
| 54 | """ | 70 | """ |
| 55 | Manages a runqemu session for Xen boot testing. | 71 | Manages a runqemu session for Xen boot testing. |
| @@ -218,11 +234,25 @@ def machine(request): | |||
| 218 | 234 | ||
| 219 | 235 | ||
| 220 | @pytest.fixture(scope="module") | 236 | @pytest.fixture(scope="module") |
| 221 | def xen_session(request, poky_dir, build_dir, machine): | 237 | def xen_image(build_dir): |
| 238 | """Build xen-image-minimal with required distro features.""" | ||
| 239 | result = _run_bitbake( | ||
| 240 | build_dir, "xen-image-minimal", | ||
| 241 | extra_vars={ | ||
| 242 | "DISTRO_FEATURES:append": " xen vxn virtualization vcontainer systemd", | ||
| 243 | }, | ||
| 244 | ) | ||
| 245 | if result.returncode != 0: | ||
| 246 | pytest.fail(f"Xen image build failed: {result.stderr}") | ||
| 247 | |||
| 248 | |||
| 249 | @pytest.fixture(scope="module") | ||
| 250 | def xen_session(request, poky_dir, build_dir, machine, xen_image): | ||
| 222 | """ | 251 | """ |
| 223 | Module-scoped fixture that boots xen-image-minimal once for all tests. | 252 | Module-scoped fixture that builds xen-image-minimal and boots it |
| 253 | once for all tests. | ||
| 224 | 254 | ||
| 225 | Skips if pexpect is not available, image is not found, or boot fails. | 255 | Skips if pexpect is not available or boot fails. |
| 226 | """ | 256 | """ |
| 227 | if not PEXPECT_AVAILABLE: | 257 | if not PEXPECT_AVAILABLE: |
| 228 | pytest.skip("pexpect not installed. Run: pip install pexpect") | 258 | pytest.skip("pexpect not installed. Run: pip install pexpect") |
