diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-12 18:31:00 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-26 01:05:01 +0000 |
| commit | 0fe8c4444f3199b862a4ba52b2b62b5f9b2af85f (patch) | |
| tree | 3ed29a52302003a532f674b20dd3466f4eae8da0 /tests | |
| parent | 2f3dcd4e0a45d1b976d266f04912fe43c68586f5 (diff) | |
| download | meta-virtualization-0fe8c4444f3199b862a4ba52b2b62b5f9b2af85f.tar.gz | |
xen: document guest import system and add tests
Add 3rd-party guest import section to README-xen.md covering
import types, kernel modes, Alpine example, and how to add
custom import handlers.
Add test_xen_guest_bundle.py with 46 pytest tests covering
bbclass structure, import handlers, kernel modes, license
warning, Alpine recipe, and README content.
Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_xen_guest_bundle.py | 361 |
1 files changed, 361 insertions, 0 deletions
diff --git a/tests/test_xen_guest_bundle.py b/tests/test_xen_guest_bundle.py new file mode 100644 index 00000000..2035a5f9 --- /dev/null +++ b/tests/test_xen_guest_bundle.py | |||
| @@ -0,0 +1,361 @@ | |||
| 1 | # SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield | ||
| 2 | # | ||
| 3 | # SPDX-License-Identifier: MIT | ||
| 4 | """ | ||
| 5 | Tests for xen-guest-bundle.bbclass - Xen guest bundling system. | ||
| 6 | |||
| 7 | These tests verify: | ||
| 8 | - bbclass file structure and syntax | ||
| 9 | - Import handler definitions | ||
| 10 | - Parse-time logic (__anonymous) | ||
| 11 | - Alpine example recipe structure | ||
| 12 | - Build tests (slow, require configured build environment) | ||
| 13 | |||
| 14 | Run with: | ||
| 15 | pytest tests/test_xen_guest_bundle.py -v | ||
| 16 | |||
| 17 | Run build tests (requires configured Yocto build): | ||
| 18 | pytest tests/test_xen_guest_bundle.py -v -m slow --machine qemuarm64 | ||
| 19 | |||
| 20 | Environment variables: | ||
| 21 | POKY_DIR: Path to poky directory (default: /opt/bruce/poky) | ||
| 22 | BUILD_DIR: Path to build directory (default: $POKY_DIR/build) | ||
| 23 | MACHINE: Target machine (default: qemux86-64) | ||
| 24 | """ | ||
| 25 | |||
| 26 | import re | ||
| 27 | import pytest | ||
| 28 | from pathlib import Path | ||
| 29 | |||
| 30 | |||
| 31 | # Note: Command line options (--poky-dir, --build-dir, --machine) | ||
| 32 | # are defined in conftest.py | ||
| 33 | |||
| 34 | |||
| 35 | @pytest.fixture(scope="module") | ||
| 36 | def poky_dir(request): | ||
| 37 | """Path to poky directory.""" | ||
| 38 | path = Path(request.config.getoption("--poky-dir")) | ||
| 39 | if not path.exists(): | ||
| 40 | pytest.skip(f"Poky directory not found: {path}") | ||
| 41 | return path | ||
| 42 | |||
| 43 | |||
| 44 | @pytest.fixture(scope="module") | ||
| 45 | def meta_virt_dir(poky_dir): | ||
| 46 | """Path to meta-virtualization layer.""" | ||
| 47 | path = poky_dir / "meta-virtualization" | ||
| 48 | if not path.exists(): | ||
| 49 | pytest.skip(f"meta-virtualization not found: {path}") | ||
| 50 | return path | ||
| 51 | |||
| 52 | |||
| 53 | @pytest.fixture(scope="module") | ||
| 54 | def bbclass_content(meta_virt_dir): | ||
| 55 | """Content of xen-guest-bundle.bbclass.""" | ||
| 56 | path = meta_virt_dir / "classes" / "xen-guest-bundle.bbclass" | ||
| 57 | if not path.exists(): | ||
| 58 | pytest.skip(f"bbclass not found: {path}") | ||
| 59 | return path.read_text() | ||
| 60 | |||
| 61 | |||
| 62 | @pytest.fixture(scope="module") | ||
| 63 | def alpine_recipe_content(meta_virt_dir): | ||
| 64 | """Content of alpine-xen-guest-bundle recipe.""" | ||
| 65 | recipes = list((meta_virt_dir / "recipes-extended" / "xen-guest-bundles").glob( | ||
| 66 | "alpine-xen-guest-bundle_*.bb")) | ||
| 67 | if not recipes: | ||
| 68 | pytest.skip("Alpine guest bundle recipe not found") | ||
| 69 | return recipes[0].read_text() | ||
| 70 | |||
| 71 | |||
| 72 | # ============================================================================ | ||
| 73 | # bbclass structure tests | ||
| 74 | # ============================================================================ | ||
| 75 | |||
| 76 | class TestXenGuestBundleClass: | ||
| 77 | """Test xen-guest-bundle.bbclass structure and syntax.""" | ||
| 78 | |||
| 79 | def test_class_exists(self, meta_virt_dir): | ||
| 80 | """Test that the bbclass file exists.""" | ||
| 81 | path = meta_virt_dir / "classes" / "xen-guest-bundle.bbclass" | ||
| 82 | assert path.exists(), f"bbclass not found: {path}" | ||
| 83 | |||
| 84 | def test_spdx_header(self, bbclass_content): | ||
| 85 | """Test SPDX license header is present.""" | ||
| 86 | assert "SPDX-License-Identifier: MIT" in bbclass_content | ||
| 87 | |||
| 88 | def test_default_variables(self, bbclass_content): | ||
| 89 | """Test that expected default variables are defined.""" | ||
| 90 | defaults = [ | ||
| 91 | "XEN_GUEST_BUNDLES", | ||
| 92 | "XEN_GUEST_IMAGE_FSTYPE", | ||
| 93 | "XEN_GUEST_MEMORY_DEFAULT", | ||
| 94 | "XEN_GUEST_VCPUS_DEFAULT", | ||
| 95 | "XEN_GUEST_VIF_DEFAULT", | ||
| 96 | "XEN_GUEST_EXTRA_DEFAULT", | ||
| 97 | "XEN_GUEST_DISK_DEVICE_DEFAULT", | ||
| 98 | ] | ||
| 99 | for var in defaults: | ||
| 100 | assert var in bbclass_content, f"Default variable {var} not found" | ||
| 101 | |||
| 102 | def test_anonymous_function(self, bbclass_content): | ||
| 103 | """Test that __anonymous() is defined.""" | ||
| 104 | assert "python __anonymous()" in bbclass_content | ||
| 105 | |||
| 106 | def test_do_compile_defined(self, bbclass_content): | ||
| 107 | """Test that do_compile is defined.""" | ||
| 108 | assert "do_compile()" in bbclass_content | ||
| 109 | |||
| 110 | def test_do_install_defined(self, bbclass_content): | ||
| 111 | """Test that do_install is defined.""" | ||
| 112 | assert "do_install()" in bbclass_content | ||
| 113 | |||
| 114 | def test_resolve_bundle_rootfs(self, bbclass_content): | ||
| 115 | """Test rootfs resolver function exists.""" | ||
| 116 | assert "resolve_bundle_rootfs()" in bbclass_content | ||
| 117 | |||
| 118 | def test_resolve_bundle_kernel(self, bbclass_content): | ||
| 119 | """Test kernel resolver function exists.""" | ||
| 120 | assert "resolve_bundle_kernel()" in bbclass_content | ||
| 121 | |||
| 122 | def test_generate_bundle_config(self, bbclass_content): | ||
| 123 | """Test config generator function exists.""" | ||
| 124 | assert "generate_bundle_config()" in bbclass_content | ||
| 125 | |||
| 126 | def test_files_variable(self, bbclass_content): | ||
| 127 | """Test FILES variable is set.""" | ||
| 128 | assert "FILES:${PN}" in bbclass_content | ||
| 129 | assert "xen-guest-bundles" in bbclass_content | ||
| 130 | |||
| 131 | def test_insane_skip(self, bbclass_content): | ||
| 132 | """Test INSANE_SKIP for binary images.""" | ||
| 133 | assert "INSANE_SKIP" in bbclass_content | ||
| 134 | assert "buildpaths" in bbclass_content | ||
| 135 | |||
| 136 | |||
| 137 | # ============================================================================ | ||
| 138 | # Import system tests | ||
| 139 | # ============================================================================ | ||
| 140 | |||
| 141 | class TestImportSystem: | ||
| 142 | """Test import system for 3rd-party guests.""" | ||
| 143 | |||
| 144 | def test_import_default_variables(self, bbclass_content): | ||
| 145 | """Test import-related default variables.""" | ||
| 146 | assert "XEN_GUEST_IMAGE_SIZE_DEFAULT" in bbclass_content | ||
| 147 | assert "XEN_GUEST_IMPORT_DEPENDS_rootfs_dir" in bbclass_content | ||
| 148 | assert "XEN_GUEST_IMPORT_DEPENDS_qcow2" in bbclass_content | ||
| 149 | assert "XEN_GUEST_IMPORT_DEPENDS_ext4" in bbclass_content | ||
| 150 | assert "XEN_GUEST_IMPORT_DEPENDS_raw" in bbclass_content | ||
| 151 | |||
| 152 | def test_import_depends_rootfs_dir(self, bbclass_content): | ||
| 153 | """Test rootfs_dir depends on e2fsprogs-native.""" | ||
| 154 | match = re.search( | ||
| 155 | r'XEN_GUEST_IMPORT_DEPENDS_rootfs_dir\s*=\s*"([^"]*)"', | ||
| 156 | bbclass_content) | ||
| 157 | assert match, "rootfs_dir depends not found" | ||
| 158 | assert "e2fsprogs-native" in match.group(1) | ||
| 159 | |||
| 160 | def test_import_depends_qcow2(self, bbclass_content): | ||
| 161 | """Test qcow2 depends on qemu-system-native.""" | ||
| 162 | match = re.search( | ||
| 163 | r'XEN_GUEST_IMPORT_DEPENDS_qcow2\s*=\s*"([^"]*)"', | ||
| 164 | bbclass_content) | ||
| 165 | assert match, "qcow2 depends not found" | ||
| 166 | assert "qemu-system-native" in match.group(1) | ||
| 167 | |||
| 168 | def test_import_handler_rootfs_dir(self, bbclass_content): | ||
| 169 | """Test rootfs_dir import handler exists.""" | ||
| 170 | assert "xen_guest_import_rootfs_dir()" in bbclass_content | ||
| 171 | assert "mkfs.ext4" in bbclass_content | ||
| 172 | |||
| 173 | def test_import_handler_qcow2(self, bbclass_content): | ||
| 174 | """Test qcow2 import handler exists.""" | ||
| 175 | assert "xen_guest_import_qcow2()" in bbclass_content | ||
| 176 | assert "qemu-img convert" in bbclass_content | ||
| 177 | |||
| 178 | def test_import_handler_ext4(self, bbclass_content): | ||
| 179 | """Test ext4 import handler exists.""" | ||
| 180 | assert "xen_guest_import_ext4()" in bbclass_content | ||
| 181 | |||
| 182 | def test_import_handler_raw(self, bbclass_content): | ||
| 183 | """Test raw import handler exists.""" | ||
| 184 | assert "xen_guest_import_raw()" in bbclass_content | ||
| 185 | |||
| 186 | def test_resolve_import_source(self, bbclass_content): | ||
| 187 | """Test import source resolver exists.""" | ||
| 188 | assert "resolve_import_source()" in bbclass_content | ||
| 189 | assert "_XEN_GUEST_IMPORT_MAP" in bbclass_content | ||
| 190 | |||
| 191 | def test_static_dispatch_in_do_compile(self, bbclass_content): | ||
| 192 | """Test that import dispatch uses static case statement.""" | ||
| 193 | # BitBake needs static function references to include them | ||
| 194 | assert "case \"$import_type\" in" in bbclass_content | ||
| 195 | assert "xen_guest_import_rootfs_dir " in bbclass_content | ||
| 196 | assert "xen_guest_import_qcow2 " in bbclass_content | ||
| 197 | |||
| 198 | def test_fakeroot_for_rootfs_dir(self, bbclass_content): | ||
| 199 | """Test that rootfs_dir type triggers fakeroot.""" | ||
| 200 | assert "fakeroot" in bbclass_content | ||
| 201 | assert "rootfs_dir" in bbclass_content | ||
| 202 | |||
| 203 | |||
| 204 | # ============================================================================ | ||
| 205 | # Kernel mode tests | ||
| 206 | # ============================================================================ | ||
| 207 | |||
| 208 | class TestKernelModes: | ||
| 209 | """Test three kernel modes: shared, custom, HVM/none.""" | ||
| 210 | |||
| 211 | def test_hvm_mode_documented(self, bbclass_content): | ||
| 212 | """Test HVM mode (kernel=none) is supported.""" | ||
| 213 | assert '"none"' in bbclass_content or "'none'" in bbclass_content | ||
| 214 | assert "HVM" in bbclass_content | ||
| 215 | |||
| 216 | def test_kernel_unpackdir_check(self, bbclass_content): | ||
| 217 | """Test kernel resolver checks UNPACKDIR.""" | ||
| 218 | assert "UNPACKDIR" in bbclass_content | ||
| 219 | |||
| 220 | def test_config_omits_kernel_for_hvm(self, bbclass_content): | ||
| 221 | """Test generate_bundle_config omits kernel for HVM.""" | ||
| 222 | # Should have conditional kernel output | ||
| 223 | assert 'if [ -n "$kernel_basename" ]' in bbclass_content | ||
| 224 | |||
| 225 | def test_shared_kernel_dependency(self, bbclass_content): | ||
| 226 | """Test virtual/kernel dependency for shared kernel.""" | ||
| 227 | assert "virtual/kernel:do_deploy" in bbclass_content | ||
| 228 | |||
| 229 | |||
| 230 | # ============================================================================ | ||
| 231 | # License warning tests | ||
| 232 | # ============================================================================ | ||
| 233 | |||
| 234 | class TestLicenseWarning: | ||
| 235 | """Test external guest license warning.""" | ||
| 236 | |||
| 237 | def test_external_names_variable(self, bbclass_content): | ||
| 238 | """Test _XEN_GUEST_EXTERNAL_NAMES is set for external guests.""" | ||
| 239 | assert "_XEN_GUEST_EXTERNAL_NAMES" in bbclass_content | ||
| 240 | |||
| 241 | def test_license_warn_prefunc(self, bbclass_content): | ||
| 242 | """Test license warning is a prefunc on do_compile.""" | ||
| 243 | assert "xen_guest_external_license_warn" in bbclass_content | ||
| 244 | assert "do_compile[prefuncs]" in bbclass_content | ||
| 245 | |||
| 246 | def test_license_warn_content(self, bbclass_content): | ||
| 247 | """Test license warning message content.""" | ||
| 248 | assert "rights to redistribute" in bbclass_content | ||
| 249 | assert "license terms" in bbclass_content | ||
| 250 | |||
| 251 | def test_license_warn_is_python(self, bbclass_content): | ||
| 252 | """Test license warning is a python function (runs once at task time).""" | ||
| 253 | assert "python xen_guest_external_license_warn()" in bbclass_content | ||
| 254 | |||
| 255 | |||
| 256 | # ============================================================================ | ||
| 257 | # Alpine recipe tests | ||
| 258 | # ============================================================================ | ||
| 259 | |||
| 260 | class TestAlpineRecipe: | ||
| 261 | """Test alpine-xen-guest-bundle recipe structure.""" | ||
| 262 | |||
| 263 | def test_recipe_exists(self, meta_virt_dir): | ||
| 264 | """Test that Alpine recipe exists.""" | ||
| 265 | recipes = list((meta_virt_dir / "recipes-extended" / "xen-guest-bundles").glob( | ||
| 266 | "alpine-xen-guest-bundle_*.bb")) | ||
| 267 | assert len(recipes) > 0, "Alpine guest bundle recipe not found" | ||
| 268 | |||
| 269 | def test_inherits_xen_guest_bundle(self, alpine_recipe_content): | ||
| 270 | """Test recipe inherits xen-guest-bundle.""" | ||
| 271 | assert "inherit xen-guest-bundle" in alpine_recipe_content | ||
| 272 | |||
| 273 | def test_license(self, alpine_recipe_content): | ||
| 274 | """Test recipe has license.""" | ||
| 275 | assert 'LICENSE = "MIT"' in alpine_recipe_content | ||
| 276 | assert "LIC_FILES_CHKSUM" in alpine_recipe_content | ||
| 277 | |||
| 278 | def test_src_uri(self, alpine_recipe_content): | ||
| 279 | """Test SRC_URI fetches Alpine minirootfs.""" | ||
| 280 | assert "dl-cdn.alpinelinux.org" in alpine_recipe_content | ||
| 281 | assert "alpine-minirootfs" in alpine_recipe_content | ||
| 282 | assert "subdir=alpine-rootfs" in alpine_recipe_content | ||
| 283 | |||
| 284 | def test_sha256sum(self, alpine_recipe_content): | ||
| 285 | """Test sha256sum is set (not placeholder).""" | ||
| 286 | match = re.search(r'SRC_URI\[sha256sum\]\s*=\s*"([^"]*)"', | ||
| 287 | alpine_recipe_content) | ||
| 288 | assert match, "sha256sum not found" | ||
| 289 | sha = match.group(1) | ||
| 290 | assert len(sha) == 64, f"sha256sum wrong length: {len(sha)}" | ||
| 291 | assert sha != "x" * 64, "sha256sum is still placeholder" | ||
| 292 | |||
| 293 | def test_guest_bundles(self, alpine_recipe_content): | ||
| 294 | """Test XEN_GUEST_BUNDLES is set.""" | ||
| 295 | assert 'XEN_GUEST_BUNDLES = "alpine:autostart:external"' in alpine_recipe_content | ||
| 296 | |||
| 297 | def test_import_source_type(self, alpine_recipe_content): | ||
| 298 | """Test import source type is rootfs_dir.""" | ||
| 299 | assert 'XEN_GUEST_SOURCE_TYPE[alpine] = "rootfs_dir"' in alpine_recipe_content | ||
| 300 | |||
| 301 | def test_import_source_file(self, alpine_recipe_content): | ||
| 302 | """Test import source file matches SRC_URI subdir.""" | ||
| 303 | assert 'XEN_GUEST_SOURCE_FILE[alpine] = "alpine-rootfs"' in alpine_recipe_content | ||
| 304 | |||
| 305 | def test_image_size(self, alpine_recipe_content): | ||
| 306 | """Test image size is set.""" | ||
| 307 | assert 'XEN_GUEST_IMAGE_SIZE[alpine]' in alpine_recipe_content | ||
| 308 | |||
| 309 | def test_guest_memory(self, alpine_recipe_content): | ||
| 310 | """Test guest memory is set.""" | ||
| 311 | assert 'XEN_GUEST_MEMORY[alpine]' in alpine_recipe_content | ||
| 312 | |||
| 313 | def test_guest_extra(self, alpine_recipe_content): | ||
| 314 | """Test guest extra args include console.""" | ||
| 315 | assert 'XEN_GUEST_EXTRA[alpine]' in alpine_recipe_content | ||
| 316 | assert "console=hvc0" in alpine_recipe_content | ||
| 317 | |||
| 318 | def test_multiarch_support(self, alpine_recipe_content): | ||
| 319 | """Test recipe supports multiple architectures.""" | ||
| 320 | assert "ALPINE_ARCH" in alpine_recipe_content | ||
| 321 | assert "aarch64" in alpine_recipe_content | ||
| 322 | assert "x86_64" in alpine_recipe_content | ||
| 323 | |||
| 324 | |||
| 325 | # ============================================================================ | ||
| 326 | # README tests | ||
| 327 | # ============================================================================ | ||
| 328 | |||
| 329 | class TestReadme: | ||
| 330 | """Test README-xen.md documentation.""" | ||
| 331 | |||
| 332 | @pytest.fixture(scope="class") | ||
| 333 | def readme_content(self, meta_virt_dir): | ||
| 334 | path = meta_virt_dir / "recipes-extended" / "images" / "README-xen.md" | ||
| 335 | if not path.exists(): | ||
| 336 | pytest.skip("README-xen.md not found") | ||
| 337 | return path.read_text() | ||
| 338 | |||
| 339 | def test_import_section_exists(self, readme_content): | ||
| 340 | """Test 3rd-party import section exists.""" | ||
| 341 | assert "3rd-party guest import" in readme_content | ||
| 342 | |||
| 343 | def test_import_types_documented(self, readme_content): | ||
| 344 | """Test import types are documented.""" | ||
| 345 | assert "rootfs_dir" in readme_content | ||
| 346 | assert "qcow2" in readme_content | ||
| 347 | |||
| 348 | def test_kernel_modes_documented(self, readme_content): | ||
| 349 | """Test kernel modes are documented.""" | ||
| 350 | assert "none" in readme_content | ||
| 351 | assert "Shared host kernel" in readme_content or "shared" in readme_content.lower() | ||
| 352 | |||
| 353 | def test_alpine_example(self, readme_content): | ||
| 354 | """Test Alpine example is in README.""" | ||
| 355 | assert "alpine" in readme_content.lower() | ||
| 356 | assert "XEN_GUEST_SOURCE_TYPE" in readme_content | ||
| 357 | |||
| 358 | def test_custom_handler_docs(self, readme_content): | ||
| 359 | """Test custom handler instructions.""" | ||
| 360 | assert "xen_guest_import_" in readme_content | ||
| 361 | assert "XEN_GUEST_IMPORT_DEPENDS_" in readme_content | ||
