diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_multilayer_oci.py | 466 |
1 files changed, 466 insertions, 0 deletions
diff --git a/tests/test_multilayer_oci.py b/tests/test_multilayer_oci.py new file mode 100644 index 00000000..eac3d23e --- /dev/null +++ b/tests/test_multilayer_oci.py | |||
| @@ -0,0 +1,466 @@ | |||
| 1 | # SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield | ||
| 2 | # | ||
| 3 | # SPDX-License-Identifier: MIT | ||
| 4 | """ | ||
| 5 | Tests for multi-layer OCI container image support. | ||
| 6 | |||
| 7 | These tests verify that OCI_LAYER_MODE = "multi" creates proper multi-layer | ||
| 8 | OCI images and that layer caching works correctly. | ||
| 9 | |||
| 10 | Run with: | ||
| 11 | pytest tests/test_multilayer_oci.py -v --poky-dir /opt/bruce/poky | ||
| 12 | |||
| 13 | Environment variables: | ||
| 14 | POKY_DIR: Path to poky directory (default: /opt/bruce/poky) | ||
| 15 | BUILD_DIR: Path to build directory (default: $POKY_DIR/build) | ||
| 16 | MACHINE: Target machine (default: qemux86-64) | ||
| 17 | |||
| 18 | Note: These tests require a configured Yocto build environment. | ||
| 19 | """ | ||
| 20 | |||
| 21 | import os | ||
| 22 | import json | ||
| 23 | import subprocess | ||
| 24 | import shutil | ||
| 25 | import pytest | ||
| 26 | from pathlib import Path | ||
| 27 | |||
| 28 | |||
| 29 | # Note: Command line options are defined in conftest.py | ||
| 30 | |||
| 31 | |||
| 32 | @pytest.fixture(scope="module") | ||
| 33 | def poky_dir(request): | ||
| 34 | """Path to poky directory.""" | ||
| 35 | path = Path(request.config.getoption("--poky-dir")) | ||
| 36 | if not path.exists(): | ||
| 37 | pytest.skip(f"Poky directory not found: {path}") | ||
| 38 | return path | ||
| 39 | |||
| 40 | |||
| 41 | @pytest.fixture(scope="module") | ||
| 42 | def build_dir(request, poky_dir): | ||
| 43 | """Path to build directory.""" | ||
| 44 | path = request.config.getoption("--build-dir") | ||
| 45 | if path: | ||
| 46 | path = Path(path) | ||
| 47 | else: | ||
| 48 | path = poky_dir / "build" | ||
| 49 | |||
| 50 | if not path.exists(): | ||
| 51 | pytest.skip(f"Build directory not found: {path}") | ||
| 52 | return path | ||
| 53 | |||
| 54 | |||
| 55 | @pytest.fixture(scope="module") | ||
| 56 | def machine(request): | ||
| 57 | """Target machine.""" | ||
| 58 | return request.config.getoption("--machine") | ||
| 59 | |||
| 60 | |||
| 61 | @pytest.fixture(scope="module") | ||
| 62 | def deploy_dir(build_dir, machine): | ||
| 63 | """Path to deploy directory for the machine.""" | ||
| 64 | path = build_dir / "tmp" / "deploy" / "images" / machine | ||
| 65 | if not path.exists(): | ||
| 66 | pytest.skip(f"Deploy directory not found: {path}") | ||
| 67 | return path | ||
| 68 | |||
| 69 | |||
| 70 | @pytest.fixture(scope="module") | ||
| 71 | def meta_virt_dir(poky_dir): | ||
| 72 | """Path to meta-virtualization layer.""" | ||
| 73 | path = poky_dir / "meta-virtualization" | ||
| 74 | if not path.exists(): | ||
| 75 | pytest.skip(f"meta-virtualization not found: {path}") | ||
| 76 | return path | ||
| 77 | |||
| 78 | |||
| 79 | @pytest.fixture(scope="module") | ||
| 80 | def layer_cache_dir(build_dir, machine): | ||
| 81 | """Path to OCI layer cache directory.""" | ||
| 82 | return build_dir / "oci-layer-cache" / machine | ||
| 83 | |||
| 84 | |||
| 85 | def run_bitbake(build_dir, recipe, task=None, extra_args=None, timeout=1800): | ||
| 86 | """Run a bitbake command within the Yocto environment.""" | ||
| 87 | # Build the bitbake command | ||
| 88 | bb_cmd = "bitbake" | ||
| 89 | if task: | ||
| 90 | bb_cmd += f" -c {task}" | ||
| 91 | bb_cmd += f" {recipe}" | ||
| 92 | if extra_args: | ||
| 93 | bb_cmd += " " + " ".join(extra_args) | ||
| 94 | |||
| 95 | # Source oe-init-build-env and run bitbake | ||
| 96 | poky_dir = build_dir.parent | ||
| 97 | full_cmd = f"bash -c 'cd {poky_dir} && source oe-init-build-env {build_dir} >/dev/null 2>&1 && {bb_cmd}'" | ||
| 98 | |||
| 99 | result = subprocess.run( | ||
| 100 | full_cmd, | ||
| 101 | shell=True, | ||
| 102 | cwd=build_dir, | ||
| 103 | timeout=timeout, | ||
| 104 | capture_output=True, | ||
| 105 | text=True, | ||
| 106 | ) | ||
| 107 | return result | ||
| 108 | |||
| 109 | |||
| 110 | def get_oci_layer_count(oci_dir): | ||
| 111 | """Get the number of layers in an OCI image using skopeo.""" | ||
| 112 | result = subprocess.run( | ||
| 113 | ["skopeo", "inspect", f"oci:{oci_dir}"], | ||
| 114 | capture_output=True, | ||
| 115 | text=True, | ||
| 116 | timeout=30, | ||
| 117 | ) | ||
| 118 | if result.returncode != 0: | ||
| 119 | return None | ||
| 120 | |||
| 121 | try: | ||
| 122 | data = json.loads(result.stdout) | ||
| 123 | return len(data.get("Layers", [])) | ||
| 124 | except json.JSONDecodeError: | ||
| 125 | return None | ||
| 126 | |||
| 127 | |||
| 128 | def get_task_log(build_dir, machine, recipe, task): | ||
| 129 | """Get the path to a bitbake task log.""" | ||
| 130 | work_dir = build_dir / "tmp" / "work" | ||
| 131 | |||
| 132 | # Find the work directory for the recipe | ||
| 133 | for arch_dir in work_dir.glob(f"*{machine}*"): | ||
| 134 | recipe_dir = arch_dir / recipe | ||
| 135 | if recipe_dir.exists(): | ||
| 136 | # Find the latest version directory | ||
| 137 | for version_dir in sorted(recipe_dir.iterdir(), reverse=True): | ||
| 138 | log_dir = version_dir / "temp" | ||
| 139 | logs = list(log_dir.glob(f"log.{task}.*")) | ||
| 140 | if logs: | ||
| 141 | return max(logs, key=lambda p: p.stat().st_mtime) | ||
| 142 | return None | ||
| 143 | |||
| 144 | |||
| 145 | class TestMultiLayerOCIClass: | ||
| 146 | """Test OCI multi-layer bbclass functionality.""" | ||
| 147 | |||
| 148 | def test_bbclass_exists(self, meta_virt_dir): | ||
| 149 | """Test that the image-oci.bbclass file exists.""" | ||
| 150 | class_file = meta_virt_dir / "classes" / "image-oci.bbclass" | ||
| 151 | assert class_file.exists(), f"Class file not found: {class_file}" | ||
| 152 | |||
| 153 | def test_umoci_inc_exists(self, meta_virt_dir): | ||
| 154 | """Test that the image-oci-umoci.inc file exists.""" | ||
| 155 | inc_file = meta_virt_dir / "classes" / "image-oci-umoci.inc" | ||
| 156 | assert inc_file.exists(), f"Include file not found: {inc_file}" | ||
| 157 | |||
| 158 | def test_multilayer_recipe_exists(self, meta_virt_dir): | ||
| 159 | """Test that the multi-layer demo recipe exists.""" | ||
| 160 | recipe = meta_virt_dir / "recipes-demo" / "images" / "app-container-multilayer.bb" | ||
| 161 | assert recipe.exists(), f"Recipe not found: {recipe}" | ||
| 162 | |||
| 163 | def test_cache_variables_defined(self, meta_virt_dir): | ||
| 164 | """Test that layer caching variables are defined in bbclass.""" | ||
| 165 | class_file = meta_virt_dir / "classes" / "image-oci.bbclass" | ||
| 166 | content = class_file.read_text() | ||
| 167 | |||
| 168 | assert "OCI_LAYER_CACHE" in content, "OCI_LAYER_CACHE not defined" | ||
| 169 | assert "OCI_LAYER_CACHE_DIR" in content, "OCI_LAYER_CACHE_DIR not defined" | ||
| 170 | |||
| 171 | def test_layer_mode_variables_defined(self, meta_virt_dir): | ||
| 172 | """Test that layer mode variables are defined in bbclass.""" | ||
| 173 | class_file = meta_virt_dir / "classes" / "image-oci.bbclass" | ||
| 174 | content = class_file.read_text() | ||
| 175 | |||
| 176 | assert "OCI_LAYER_MODE" in content, "OCI_LAYER_MODE not defined" | ||
| 177 | assert "OCI_LAYERS" in content, "OCI_LAYERS not defined" | ||
| 178 | |||
| 179 | |||
| 180 | class TestMultiLayerOCIBuild: | ||
| 181 | """Test building multi-layer OCI images.""" | ||
| 182 | |||
| 183 | @pytest.mark.slow | ||
| 184 | def test_multilayer_recipe_builds(self, build_dir): | ||
| 185 | """Test that app-container-multilayer recipe builds successfully.""" | ||
| 186 | result = run_bitbake(build_dir, "app-container-multilayer", timeout=3600) | ||
| 187 | |||
| 188 | if result.returncode != 0: | ||
| 189 | if "Nothing PROVIDES" in result.stderr: | ||
| 190 | pytest.skip("app-container-multilayer recipe not available") | ||
| 191 | pytest.fail(f"Build failed:\nstdout: {result.stdout}\nstderr: {result.stderr}") | ||
| 192 | |||
| 193 | @pytest.mark.slow | ||
| 194 | def test_multilayer_produces_correct_layers(self, build_dir, deploy_dir): | ||
| 195 | """Test that multi-layer build produces 3 layers.""" | ||
| 196 | # Ensure the recipe is built | ||
| 197 | result = run_bitbake(build_dir, "app-container-multilayer", timeout=3600) | ||
| 198 | if result.returncode != 0: | ||
| 199 | pytest.skip("Build failed, skipping layer count check") | ||
| 200 | |||
| 201 | # Find the OCI directory | ||
| 202 | oci_dirs = list(deploy_dir.glob("app-container-multilayer*-oci")) | ||
| 203 | assert len(oci_dirs) > 0, "No OCI directory found for app-container-multilayer" | ||
| 204 | |||
| 205 | # Get the actual OCI directory (resolve symlink if needed) | ||
| 206 | oci_dir = oci_dirs[0] | ||
| 207 | if oci_dir.is_symlink(): | ||
| 208 | oci_dir = oci_dir.resolve() | ||
| 209 | |||
| 210 | # Check layer count | ||
| 211 | layer_count = get_oci_layer_count(oci_dir) | ||
| 212 | assert layer_count is not None, f"Failed to inspect OCI image: {oci_dir}" | ||
| 213 | assert layer_count == 3, f"Expected 3 layers, got {layer_count}" | ||
| 214 | |||
| 215 | |||
| 216 | class TestLayerCaching: | ||
| 217 | """Test OCI layer caching functionality.""" | ||
| 218 | |||
| 219 | @pytest.mark.slow | ||
| 220 | def test_cache_directory_created(self, build_dir, layer_cache_dir): | ||
| 221 | """Test that the layer cache directory is created after build.""" | ||
| 222 | # Run the build | ||
| 223 | result = run_bitbake(build_dir, "app-container-multilayer", timeout=3600) | ||
| 224 | if result.returncode != 0: | ||
| 225 | pytest.skip("Build failed, skipping cache test") | ||
| 226 | |||
| 227 | # Check cache directory exists | ||
| 228 | assert layer_cache_dir.exists(), f"Cache directory not created: {layer_cache_dir}" | ||
| 229 | |||
| 230 | @pytest.mark.slow | ||
| 231 | def test_cache_entries_exist(self, build_dir, layer_cache_dir): | ||
| 232 | """Test that cache entries are created for each layer.""" | ||
| 233 | # Run the build | ||
| 234 | result = run_bitbake(build_dir, "app-container-multilayer", timeout=3600) | ||
| 235 | if result.returncode != 0: | ||
| 236 | pytest.skip("Build failed, skipping cache test") | ||
| 237 | |||
| 238 | # Skip if cache dir doesn't exist | ||
| 239 | if not layer_cache_dir.exists(): | ||
| 240 | pytest.skip("Cache directory not found") | ||
| 241 | |||
| 242 | # Check for cache entries (format: {hash}-{layer_name}) | ||
| 243 | cache_entries = list(layer_cache_dir.iterdir()) | ||
| 244 | assert len(cache_entries) >= 3, f"Expected at least 3 cache entries, found {len(cache_entries)}" | ||
| 245 | |||
| 246 | # Check for expected layer names | ||
| 247 | entry_names = [e.name for e in cache_entries] | ||
| 248 | has_base = any("base" in name for name in entry_names) | ||
| 249 | has_shell = any("shell" in name for name in entry_names) | ||
| 250 | has_app = any("app" in name for name in entry_names) | ||
| 251 | |||
| 252 | assert has_base, f"No cache entry for 'base' layer. Found: {entry_names}" | ||
| 253 | assert has_shell, f"No cache entry for 'shell' layer. Found: {entry_names}" | ||
| 254 | assert has_app, f"No cache entry for 'app' layer. Found: {entry_names}" | ||
| 255 | |||
| 256 | @pytest.mark.slow | ||
| 257 | def test_cache_marker_file(self, build_dir, layer_cache_dir): | ||
| 258 | """Test that cache entries have marker files.""" | ||
| 259 | # Run the build | ||
| 260 | result = run_bitbake(build_dir, "app-container-multilayer", timeout=3600) | ||
| 261 | if result.returncode != 0: | ||
| 262 | pytest.skip("Build failed, skipping cache test") | ||
| 263 | |||
| 264 | if not layer_cache_dir.exists(): | ||
| 265 | pytest.skip("Cache directory not found") | ||
| 266 | |||
| 267 | # Check each cache entry has a marker file | ||
| 268 | cache_entries = [e for e in layer_cache_dir.iterdir() if e.is_dir()] | ||
| 269 | for entry in cache_entries: | ||
| 270 | marker = entry / ".oci-layer-cache" | ||
| 271 | assert marker.exists(), f"No marker file in cache entry: {entry}" | ||
| 272 | |||
| 273 | # Check marker content | ||
| 274 | content = marker.read_text() | ||
| 275 | assert "cache_key=" in content | ||
| 276 | assert "layer_name=" in content | ||
| 277 | assert "created=" in content | ||
| 278 | |||
| 279 | @pytest.mark.slow | ||
| 280 | def test_cache_hit_on_rebuild(self, build_dir, machine): | ||
| 281 | """Test that cache hits occur on rebuild.""" | ||
| 282 | # First build - should have cache misses | ||
| 283 | result = run_bitbake(build_dir, "app-container-multilayer", timeout=3600) | ||
| 284 | if result.returncode != 0: | ||
| 285 | pytest.skip("First build failed") | ||
| 286 | |||
| 287 | # Clean the work directory to force re-run of do_image_oci | ||
| 288 | work_pattern = f"tmp/work/*{machine}*/app-container-multilayer/*/oci-layer-rootfs" | ||
| 289 | for work_dir in build_dir.glob(work_pattern): | ||
| 290 | if work_dir.exists(): | ||
| 291 | shutil.rmtree(work_dir) | ||
| 292 | |||
| 293 | # Remove stamp file to force task re-run | ||
| 294 | stamp_pattern = f"tmp/stamps/*{machine}*/app-container-multilayer/*.do_image_oci*" | ||
| 295 | for stamp in build_dir.glob(stamp_pattern): | ||
| 296 | stamp.unlink() | ||
| 297 | |||
| 298 | # Second build - should have cache hits | ||
| 299 | result = run_bitbake(build_dir, "app-container-multilayer", timeout=3600) | ||
| 300 | if result.returncode != 0: | ||
| 301 | pytest.fail(f"Second build failed:\n{result.stderr}") | ||
| 302 | |||
| 303 | # Check the log for cache hit messages | ||
| 304 | log_file = get_task_log(build_dir, machine, "app-container-multilayer", "do_image_oci") | ||
| 305 | if log_file and log_file.exists(): | ||
| 306 | log_content = log_file.read_text() | ||
| 307 | assert "OCI Cache HIT" in log_content, \ | ||
| 308 | "No cache hits found in log. Expected 'OCI Cache HIT' messages." | ||
| 309 | # Count hits vs misses | ||
| 310 | hits = log_content.count("OCI Cache HIT") | ||
| 311 | misses = log_content.count("OCI Cache MISS") | ||
| 312 | assert hits >= 3, f"Expected at least 3 cache hits, got {hits} hits and {misses} misses" | ||
| 313 | |||
| 314 | |||
| 315 | class TestSingleLayerBackwardCompat: | ||
| 316 | """Test that single-layer mode (default) still works.""" | ||
| 317 | |||
| 318 | @pytest.mark.slow | ||
| 319 | def test_single_layer_recipe_builds(self, build_dir, meta_virt_dir): | ||
| 320 | """Test that a single-layer OCI recipe still builds.""" | ||
| 321 | # Check if app-container (single-layer) recipe exists | ||
| 322 | recipe = meta_virt_dir / "recipes-demo" / "images" / "app-container.bb" | ||
| 323 | if not recipe.exists(): | ||
| 324 | pytest.skip("app-container recipe not found") | ||
| 325 | |||
| 326 | result = run_bitbake(build_dir, "app-container", timeout=3600) | ||
| 327 | if result.returncode != 0: | ||
| 328 | if "Nothing PROVIDES" in result.stderr: | ||
| 329 | pytest.skip("app-container recipe not available") | ||
| 330 | pytest.fail(f"Build failed: {result.stderr}") | ||
| 331 | |||
| 332 | @pytest.mark.slow | ||
| 333 | def test_single_layer_produces_one_layer(self, build_dir, deploy_dir, meta_virt_dir): | ||
| 334 | """Test that single-layer build produces 1 layer.""" | ||
| 335 | # Check if recipe exists | ||
| 336 | recipe = meta_virt_dir / "recipes-demo" / "images" / "app-container.bb" | ||
| 337 | if not recipe.exists(): | ||
| 338 | pytest.skip("app-container recipe not found") | ||
| 339 | |||
| 340 | result = run_bitbake(build_dir, "app-container", timeout=3600) | ||
| 341 | if result.returncode != 0: | ||
| 342 | pytest.skip("Build failed") | ||
| 343 | |||
| 344 | # Find the OCI directory | ||
| 345 | oci_dirs = list(deploy_dir.glob("app-container-*-oci")) | ||
| 346 | # Filter out multilayer | ||
| 347 | oci_dirs = [d for d in oci_dirs if "multilayer" not in d.name] | ||
| 348 | |||
| 349 | if not oci_dirs: | ||
| 350 | pytest.skip("No OCI directory found for app-container") | ||
| 351 | |||
| 352 | oci_dir = oci_dirs[0] | ||
| 353 | if oci_dir.is_symlink(): | ||
| 354 | oci_dir = oci_dir.resolve() | ||
| 355 | |||
| 356 | layer_count = get_oci_layer_count(oci_dir) | ||
| 357 | assert layer_count is not None, f"Failed to inspect OCI image: {oci_dir}" | ||
| 358 | assert layer_count == 1, f"Expected 1 layer for single-layer mode, got {layer_count}" | ||
| 359 | |||
| 360 | |||
| 361 | class TestTwoLayerBaseImage: | ||
| 362 | """Test two-layer OCI images using OCI_BASE_IMAGE.""" | ||
| 363 | |||
| 364 | def test_layered_recipe_exists(self, meta_virt_dir): | ||
| 365 | """Test that the two-layer demo recipe exists.""" | ||
| 366 | recipe = meta_virt_dir / "recipes-demo" / "images" / "app-container-layered.bb" | ||
| 367 | assert recipe.exists(), f"Recipe not found: {recipe}" | ||
| 368 | |||
| 369 | def test_layered_recipe_uses_base_image(self, meta_virt_dir): | ||
| 370 | """Test that the layered recipe uses OCI_BASE_IMAGE.""" | ||
| 371 | recipe = meta_virt_dir / "recipes-demo" / "images" / "app-container-layered.bb" | ||
| 372 | if not recipe.exists(): | ||
| 373 | pytest.skip("Recipe not found") | ||
| 374 | |||
| 375 | content = recipe.read_text() | ||
| 376 | assert "OCI_BASE_IMAGE" in content, "Recipe should use OCI_BASE_IMAGE" | ||
| 377 | assert "container-base" in content, "Recipe should use container-base as base" | ||
| 378 | |||
| 379 | @pytest.mark.slow | ||
| 380 | def test_layered_recipe_builds(self, build_dir): | ||
| 381 | """Test that app-container-layered recipe builds successfully.""" | ||
| 382 | # First ensure the base image is built | ||
| 383 | result = run_bitbake(build_dir, "container-base", timeout=3600) | ||
| 384 | if result.returncode != 0: | ||
| 385 | if "Nothing PROVIDES" in result.stderr: | ||
| 386 | pytest.skip("container-base recipe not available") | ||
| 387 | pytest.fail(f"Base image build failed: {result.stderr}") | ||
| 388 | |||
| 389 | # Now build the layered image | ||
| 390 | result = run_bitbake(build_dir, "app-container-layered", timeout=3600) | ||
| 391 | if result.returncode != 0: | ||
| 392 | if "Nothing PROVIDES" in result.stderr: | ||
| 393 | pytest.skip("app-container-layered recipe not available") | ||
| 394 | pytest.fail(f"Build failed:\nstdout: {result.stdout}\nstderr: {result.stderr}") | ||
| 395 | |||
| 396 | @pytest.mark.slow | ||
| 397 | def test_layered_produces_two_layers(self, build_dir, deploy_dir): | ||
| 398 | """Test that two-layer build produces 2 layers (base + app).""" | ||
| 399 | # Ensure the base is built first | ||
| 400 | result = run_bitbake(build_dir, "container-base", timeout=3600) | ||
| 401 | if result.returncode != 0: | ||
| 402 | pytest.skip("Base image build failed") | ||
| 403 | |||
| 404 | # Build the layered image | ||
| 405 | result = run_bitbake(build_dir, "app-container-layered", timeout=3600) | ||
| 406 | if result.returncode != 0: | ||
| 407 | pytest.skip("Build failed, skipping layer count check") | ||
| 408 | |||
| 409 | # Find the OCI directory | ||
| 410 | oci_dirs = list(deploy_dir.glob("app-container-layered*-oci")) | ||
| 411 | assert len(oci_dirs) > 0, "No OCI directory found for app-container-layered" | ||
| 412 | |||
| 413 | # Get the actual OCI directory (resolve symlink if needed) | ||
| 414 | oci_dir = oci_dirs[0] | ||
| 415 | if oci_dir.is_symlink(): | ||
| 416 | oci_dir = oci_dir.resolve() | ||
| 417 | |||
| 418 | # Check layer count - should be 2 (base + app) | ||
| 419 | layer_count = get_oci_layer_count(oci_dir) | ||
| 420 | assert layer_count is not None, f"Failed to inspect OCI image: {oci_dir}" | ||
| 421 | assert layer_count == 2, f"Expected 2 layers (base + app), got {layer_count}" | ||
| 422 | |||
| 423 | @pytest.mark.slow | ||
| 424 | def test_base_image_produces_one_layer(self, build_dir, deploy_dir): | ||
| 425 | """Test that container-base (the base image) produces 1 layer.""" | ||
| 426 | result = run_bitbake(build_dir, "container-base", timeout=3600) | ||
| 427 | if result.returncode != 0: | ||
| 428 | pytest.skip("Build failed") | ||
| 429 | |||
| 430 | # Find the OCI directory | ||
| 431 | oci_dirs = list(deploy_dir.glob("container-base*-oci")) | ||
| 432 | if not oci_dirs: | ||
| 433 | pytest.skip("No OCI directory found for container-base") | ||
| 434 | |||
| 435 | oci_dir = oci_dirs[0] | ||
| 436 | if oci_dir.is_symlink(): | ||
| 437 | oci_dir = oci_dir.resolve() | ||
| 438 | |||
| 439 | layer_count = get_oci_layer_count(oci_dir) | ||
| 440 | assert layer_count is not None, f"Failed to inspect OCI image: {oci_dir}" | ||
| 441 | assert layer_count == 1, f"Expected 1 layer for base image, got {layer_count}" | ||
| 442 | |||
| 443 | |||
| 444 | class TestLayerTypes: | ||
| 445 | """Test different OCI_LAYERS types.""" | ||
| 446 | |||
| 447 | def test_packages_layer_type(self, meta_virt_dir): | ||
| 448 | """Test that 'packages' layer type is supported.""" | ||
| 449 | recipe = meta_virt_dir / "recipes-demo" / "images" / "app-container-multilayer.bb" | ||
| 450 | if not recipe.exists(): | ||
| 451 | pytest.skip("Recipe not found") | ||
| 452 | |||
| 453 | content = recipe.read_text() | ||
| 454 | assert "packages" in content, "Recipe should use 'packages' layer type" | ||
| 455 | |||
| 456 | def test_directories_layer_type_documented(self, meta_virt_dir): | ||
| 457 | """Test that 'directories' layer type is documented.""" | ||
| 458 | class_file = meta_virt_dir / "classes" / "image-oci.bbclass" | ||
| 459 | content = class_file.read_text() | ||
| 460 | assert "directories" in content, "directories layer type should be documented" | ||
| 461 | |||
| 462 | def test_files_layer_type_documented(self, meta_virt_dir): | ||
| 463 | """Test that 'files' layer type is documented.""" | ||
| 464 | class_file = meta_virt_dir / "classes" / "image-oci.bbclass" | ||
| 465 | content = class_file.read_text() | ||
| 466 | assert "files" in content, "files layer type should be documented" | ||
