diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-15 21:50:38 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-21 18:00:26 -0500 |
| commit | 02bce5b72e8725ba58d82627c780e376ac59a84b (patch) | |
| tree | 8e01635166b3d475fbf4fef34150ccde7ccda21a /tests | |
| parent | 640fc9278435c49b8c59d78c18d024c66b3d6e6a (diff) | |
| download | meta-virtualization-02bce5b72e8725ba58d82627c780e376ac59a84b.tar.gz | |
vcontainer: add multi-arch OCI support
Add functions to detect and handle multi-architecture OCI Image Index
format with automatic platform selection during import. Also add
oci-multiarch.bbclass for build-time multi-arch OCI creation.
Runtime support (vcontainer-common.sh):
- is_oci_image_index() - detect multi-arch OCI images
- get_oci_platforms() - list available platforms
- select_platform_manifest() - select manifest for target architecture
- extract_platform_oci() - extract single platform to new OCI dir
- normalize_arch_to_oci/from_oci() - architecture name mapping
- Update vimport to auto-select platform from multi-arch images
Build-time support (oci-multiarch.bbclass):
- Create OCI Image Index from multiconfig builds
- Collect images from vruntime-aarch64, vruntime-x86-64
- Combine blobs and create unified manifest list
Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/README.md | 63 | ||||
| -rw-r--r-- | tests/conftest.py | 10 | ||||
| -rw-r--r-- | tests/test_multiarch_oci.py | 711 |
3 files changed, 777 insertions, 7 deletions
diff --git a/tests/README.md b/tests/README.md index 92719a66..4f5aed28 100644 --- a/tests/README.md +++ b/tests/README.md | |||
| @@ -33,10 +33,10 @@ source oe-init-build-env | |||
| 33 | 33 | ||
| 34 | # 3. Build the standalone SDK tarball (includes blobs + QEMU) | 34 | # 3. Build the standalone SDK tarball (includes blobs + QEMU) |
| 35 | MACHINE=qemux86-64 bitbake vcontainer-tarball | 35 | MACHINE=qemux86-64 bitbake vcontainer-tarball |
| 36 | # Output: tmp/deploy/sdk/vcontainer-standalone-x86_64.sh | 36 | # Output: tmp/deploy/sdk/vcontainer-standalone.sh |
| 37 | 37 | ||
| 38 | # 4. Extract the tarball (self-extracting installer) | 38 | # 4. Extract the tarball (self-extracting installer) |
| 39 | /opt/bruce/poky/build/tmp/deploy/sdk/vcontainer-standalone-x86_64.sh -d /tmp/vcontainer -y | 39 | /opt/bruce/poky/build/tmp/deploy/sdk/vcontainer-standalone.sh -d /tmp/vcontainer -y |
| 40 | 40 | ||
| 41 | # 5. Set up the environment | 41 | # 5. Set up the environment |
| 42 | cd /tmp/vcontainer | 42 | cd /tmp/vcontainer |
| @@ -441,6 +441,20 @@ tests/ | |||
| 441 | │ ├── TestVdkrRecipes # vdkr builds | 441 | │ ├── TestVdkrRecipes # vdkr builds |
| 442 | │ ├── TestMulticonfig # multiconfig setup | 442 | │ ├── TestMulticonfig # multiconfig setup |
| 443 | │ └── TestBundledContainersBoot # boot and verify containers | 443 | │ └── TestBundledContainersBoot # boot and verify containers |
| 444 | ├── test_multiarch_oci.py # Multi-architecture OCI tests | ||
| 445 | │ ├── TestOCIImageIndexDetection # multi-arch OCI detection | ||
| 446 | │ ├── TestPlatformSelection # arch selection (aarch64/x86_64) | ||
| 447 | │ ├── TestGetOCIPlatforms # platform listing | ||
| 448 | │ ├── TestExtractPlatformOCI # single-platform extraction | ||
| 449 | │ ├── TestMultiArchOCIClass # oci-multiarch.bbclass tests | ||
| 450 | │ ├── TestBackwardCompatibility # single-arch OCI compat | ||
| 451 | │ ├── TestVrunnerMultiArch # vrunner.sh multi-arch support | ||
| 452 | │ ├── TestVcontainerCommonMultiArch # vcontainer-common.sh support | ||
| 453 | │ └── TestContainerRegistryMultiArch # registry manifest list support | ||
| 454 | ├── test_multilayer_oci.py # Multi-layer OCI tests | ||
| 455 | │ ├── TestMultiLayerOCIClass # OCI_LAYERS support | ||
| 456 | │ ├── TestMultiLayerOCIBuild # layer build verification | ||
| 457 | │ └── TestLayerCaching # layer cache tests | ||
| 444 | └── README.md # This file | 458 | └── README.md # This file |
| 445 | ``` | 459 | ``` |
| 446 | 460 | ||
| @@ -448,7 +462,46 @@ tests/ | |||
| 448 | 462 | ||
| 449 | ## Quick Reference | 463 | ## Quick Reference |
| 450 | 464 | ||
| 451 | ### Full vdkr + vpdmn test run (recommended) | 465 | ### Full Multi-Architecture Regression Test (recommended) |
| 466 | |||
| 467 | This builds everything needed for comprehensive testing of both x86_64 and aarch64: | ||
| 468 | |||
| 469 | ```bash | ||
| 470 | # Build all components (blobs, SDK, images, containers for both architectures) | ||
| 471 | cd /opt/bruce/poky && source oe-init-build-env && \ | ||
| 472 | bitbake mc:vruntime-aarch64:vdkr-initramfs-create && \ | ||
| 473 | bitbake mc:vruntime-x86-64:vdkr-initramfs-create && \ | ||
| 474 | bitbake mc:vruntime-aarch64:vpdmn-initramfs-create && \ | ||
| 475 | bitbake mc:vruntime-x86-64:vpdmn-initramfs-create && \ | ||
| 476 | MACHINE=qemux86-64 bitbake vcontainer-tarball && \ | ||
| 477 | MACHINE=qemux86-64 bitbake container-image-host && \ | ||
| 478 | MACHINE=qemuarm64 bitbake container-image-host && \ | ||
| 479 | MACHINE=qemux86-64 bitbake container-app-base && \ | ||
| 480 | MACHINE=qemuarm64 bitbake container-app-base | ||
| 481 | ``` | ||
| 482 | |||
| 483 | Then extract the SDK and run the full test suite: | ||
| 484 | |||
| 485 | ```bash | ||
| 486 | # Extract SDK and run all tests | ||
| 487 | /opt/bruce/poky/build/tmp/deploy/sdk/vcontainer-standalone.sh -d /tmp/vcontainer -y && \ | ||
| 488 | cd /opt/bruce/poky/meta-virtualization && \ | ||
| 489 | pytest tests/ -v --vdkr-dir /tmp/vcontainer --poky-dir /opt/bruce/poky | ||
| 490 | ``` | ||
| 491 | |||
| 492 | To test a specific architecture: | ||
| 493 | |||
| 494 | ```bash | ||
| 495 | # Test x86_64 | ||
| 496 | pytest tests/ -v --vdkr-dir /tmp/vcontainer --poky-dir /opt/bruce/poky --arch x86_64 | ||
| 497 | |||
| 498 | # Test aarch64 | ||
| 499 | pytest tests/ -v --vdkr-dir /tmp/vcontainer --poky-dir /opt/bruce/poky --arch aarch64 | ||
| 500 | ``` | ||
| 501 | |||
| 502 | --- | ||
| 503 | |||
| 504 | ### Full vdkr + vpdmn test run (single architecture) | ||
| 452 | 505 | ||
| 453 | ```bash | 506 | ```bash |
| 454 | # 1. Build the unified standalone SDK (includes both vdkr and vpdmn) | 507 | # 1. Build the unified standalone SDK (includes both vdkr and vpdmn) |
| @@ -457,7 +510,7 @@ source oe-init-build-env | |||
| 457 | MACHINE=qemux86-64 bitbake vcontainer-tarball | 510 | MACHINE=qemux86-64 bitbake vcontainer-tarball |
| 458 | 511 | ||
| 459 | # 2. Extract the tarball (self-extracting installer) | 512 | # 2. Extract the tarball (self-extracting installer) |
| 460 | /opt/bruce/poky/build/tmp/deploy/sdk/vcontainer-standalone-x86_64.sh -d /tmp/vcontainer -y | 513 | /opt/bruce/poky/build/tmp/deploy/sdk/vcontainer-standalone.sh -d /tmp/vcontainer -y |
| 461 | 514 | ||
| 462 | # 3. Run fast tests for both tools (skips network and slow tests) | 515 | # 3. Run fast tests for both tools (skips network and slow tests) |
| 463 | cd /opt/bruce/poky/meta-virtualization | 516 | cd /opt/bruce/poky/meta-virtualization |
| @@ -474,7 +527,7 @@ pytest tests/test_vdkr.py tests/test_vpdmn.py -v --vdkr-dir /tmp/vcontainer | |||
| 474 | MACHINE=qemux86-64 bitbake vcontainer-tarball | 527 | MACHINE=qemux86-64 bitbake vcontainer-tarball |
| 475 | 528 | ||
| 476 | # Extract | 529 | # Extract |
| 477 | /opt/bruce/poky/build/tmp/deploy/sdk/vcontainer-standalone-x86_64.sh -d /tmp/vcontainer -y | 530 | /opt/bruce/poky/build/tmp/deploy/sdk/vcontainer-standalone.sh -d /tmp/vcontainer -y |
| 478 | 531 | ||
| 479 | # Run vdkr tests only | 532 | # Run vdkr tests only |
| 480 | pytest tests/test_vdkr.py -v --vdkr-dir /tmp/vcontainer | 533 | pytest tests/test_vdkr.py -v --vdkr-dir /tmp/vcontainer |
diff --git a/tests/conftest.py b/tests/conftest.py index accb8f17..a0b8fd0b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py | |||
| @@ -416,14 +416,17 @@ class VdkrRunner: | |||
| 416 | raise AssertionError(error_msg) | 416 | raise AssertionError(error_msg) |
| 417 | return result | 417 | return result |
| 418 | 418 | ||
| 419 | def memres_start(self, timeout=120, port_forwards=None): | 419 | def memres_start(self, timeout=120, port_forwards=None, no_registry=False): |
| 420 | """Start memory resident mode. | 420 | """Start memory resident mode. |
| 421 | 421 | ||
| 422 | Args: | 422 | Args: |
| 423 | timeout: Command timeout in seconds | 423 | timeout: Command timeout in seconds |
| 424 | port_forwards: List of port forwards, e.g., ["8080:80", "2222:22"] | 424 | port_forwards: List of port forwards, e.g., ["8080:80", "2222:22"] |
| 425 | no_registry: Disable baked-in registry (default False - registry check is now smart) | ||
| 425 | """ | 426 | """ |
| 426 | args = ["memres", "start"] | 427 | args = ["memres", "start"] |
| 428 | if no_registry: | ||
| 429 | args.append("--no-registry") | ||
| 427 | if port_forwards: | 430 | if port_forwards: |
| 428 | for pf in port_forwards: | 431 | for pf in port_forwards: |
| 429 | args.extend(["-p", pf]) | 432 | args.extend(["-p", pf]) |
| @@ -646,14 +649,17 @@ class VpdmnRunner: | |||
| 646 | raise AssertionError(error_msg) | 649 | raise AssertionError(error_msg) |
| 647 | return result | 650 | return result |
| 648 | 651 | ||
| 649 | def memres_start(self, timeout=120, port_forwards=None): | 652 | def memres_start(self, timeout=120, port_forwards=None, no_registry=False): |
| 650 | """Start memory resident mode. | 653 | """Start memory resident mode. |
| 651 | 654 | ||
| 652 | Args: | 655 | Args: |
| 653 | timeout: Command timeout in seconds | 656 | timeout: Command timeout in seconds |
| 654 | port_forwards: List of port forwards, e.g., ["8080:80", "2222:22"] | 657 | port_forwards: List of port forwards, e.g., ["8080:80", "2222:22"] |
| 658 | no_registry: Disable baked-in registry (default False - registry check is now smart) | ||
| 655 | """ | 659 | """ |
| 656 | args = ["memres", "start"] | 660 | args = ["memres", "start"] |
| 661 | if no_registry: | ||
| 662 | args.append("--no-registry") | ||
| 657 | if port_forwards: | 663 | if port_forwards: |
| 658 | for pf in port_forwards: | 664 | for pf in port_forwards: |
| 659 | args.extend(["-p", pf]) | 665 | args.extend(["-p", pf]) |
diff --git a/tests/test_multiarch_oci.py b/tests/test_multiarch_oci.py new file mode 100644 index 00000000..3ae642cd --- /dev/null +++ b/tests/test_multiarch_oci.py | |||
| @@ -0,0 +1,711 @@ | |||
| 1 | # SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield | ||
| 2 | # | ||
| 3 | # SPDX-License-Identifier: MIT | ||
| 4 | """ | ||
| 5 | Tests for multi-architecture OCI container support. | ||
| 6 | |||
| 7 | These tests verify: | ||
| 8 | - OCI Image Index detection and platform selection | ||
| 9 | - Multi-arch import via vdkr/vrunner | ||
| 10 | - Architecture normalization (aarch64 <-> arm64, x86_64 <-> amd64) | ||
| 11 | - Backward compatibility with single-arch OCI images | ||
| 12 | |||
| 13 | Run with: | ||
| 14 | pytest tests/test_multiarch_oci.py -v --poky-dir /opt/bruce/poky | ||
| 15 | |||
| 16 | Environment variables: | ||
| 17 | POKY_DIR: Path to poky directory (default: /opt/bruce/poky) | ||
| 18 | |||
| 19 | Note: Some tests require the shell scripts from meta-virtualization/recipes-containers/vcontainer/files/ | ||
| 20 | """ | ||
| 21 | |||
| 22 | import os | ||
| 23 | import json | ||
| 24 | import subprocess | ||
| 25 | import tempfile | ||
| 26 | import shutil | ||
| 27 | import pytest | ||
| 28 | from pathlib import Path | ||
| 29 | |||
| 30 | |||
| 31 | # Note: Command line options are defined in conftest.py | ||
| 32 | |||
| 33 | |||
| 34 | @pytest.fixture(scope="module") | ||
| 35 | def meta_virt_dir(request): | ||
| 36 | """Path to meta-virtualization layer.""" | ||
| 37 | poky_dir = Path(request.config.getoption("--poky-dir")) | ||
| 38 | path = poky_dir / "meta-virtualization" | ||
| 39 | if not path.exists(): | ||
| 40 | pytest.skip(f"meta-virtualization not found: {path}") | ||
| 41 | return path | ||
| 42 | |||
| 43 | |||
| 44 | @pytest.fixture(scope="module") | ||
| 45 | def vcontainer_files_dir(meta_virt_dir): | ||
| 46 | """Path to vcontainer shell scripts.""" | ||
| 47 | path = meta_virt_dir / "recipes-containers" / "vcontainer" / "files" | ||
| 48 | if not path.exists(): | ||
| 49 | pytest.skip(f"vcontainer files not found: {path}") | ||
| 50 | return path | ||
| 51 | |||
| 52 | |||
| 53 | @pytest.fixture | ||
| 54 | def multiarch_oci_dir(tmp_path): | ||
| 55 | """Create a mock multi-arch OCI directory for testing. | ||
| 56 | |||
| 57 | Creates an OCI Image Index with two platforms: arm64 and amd64. | ||
| 58 | The blobs are minimal mock data sufficient for testing detection. | ||
| 59 | """ | ||
| 60 | oci_dir = tmp_path / "test-multiarch-oci" | ||
| 61 | oci_dir.mkdir() | ||
| 62 | |||
| 63 | # Create blobs directory | ||
| 64 | blobs = oci_dir / "blobs" / "sha256" | ||
| 65 | blobs.mkdir(parents=True) | ||
| 66 | |||
| 67 | # Create mock config for arm64 | ||
| 68 | arm64_config = { | ||
| 69 | "architecture": "arm64", | ||
| 70 | "os": "linux", | ||
| 71 | "config": {}, | ||
| 72 | "rootfs": {"type": "layers", "diff_ids": []} | ||
| 73 | } | ||
| 74 | arm64_config_json = json.dumps(arm64_config) | ||
| 75 | arm64_config_digest = create_mock_blob(blobs, arm64_config_json) | ||
| 76 | |||
| 77 | # Create mock config for amd64 | ||
| 78 | amd64_config = { | ||
| 79 | "architecture": "amd64", | ||
| 80 | "os": "linux", | ||
| 81 | "config": {}, | ||
| 82 | "rootfs": {"type": "layers", "diff_ids": []} | ||
| 83 | } | ||
| 84 | amd64_config_json = json.dumps(amd64_config) | ||
| 85 | amd64_config_digest = create_mock_blob(blobs, amd64_config_json) | ||
| 86 | |||
| 87 | # Create mock layer blob (shared between both) | ||
| 88 | layer_content = b"mock layer content for testing" | ||
| 89 | layer_digest = create_mock_blob(blobs, layer_content, binary=True) | ||
| 90 | |||
| 91 | # Create manifest for arm64 | ||
| 92 | arm64_manifest = { | ||
| 93 | "schemaVersion": 2, | ||
| 94 | "mediaType": "application/vnd.oci.image.manifest.v1+json", | ||
| 95 | "config": { | ||
| 96 | "mediaType": "application/vnd.oci.image.config.v1+json", | ||
| 97 | "digest": f"sha256:{arm64_config_digest}", | ||
| 98 | "size": len(arm64_config_json) | ||
| 99 | }, | ||
| 100 | "layers": [ | ||
| 101 | { | ||
| 102 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", | ||
| 103 | "digest": f"sha256:{layer_digest}", | ||
| 104 | "size": len(layer_content) | ||
| 105 | } | ||
| 106 | ] | ||
| 107 | } | ||
| 108 | arm64_manifest_json = json.dumps(arm64_manifest) | ||
| 109 | arm64_manifest_digest = create_mock_blob(blobs, arm64_manifest_json) | ||
| 110 | |||
| 111 | # Create manifest for amd64 | ||
| 112 | amd64_manifest = { | ||
| 113 | "schemaVersion": 2, | ||
| 114 | "mediaType": "application/vnd.oci.image.manifest.v1+json", | ||
| 115 | "config": { | ||
| 116 | "mediaType": "application/vnd.oci.image.config.v1+json", | ||
| 117 | "digest": f"sha256:{amd64_config_digest}", | ||
| 118 | "size": len(amd64_config_json) | ||
| 119 | }, | ||
| 120 | "layers": [ | ||
| 121 | { | ||
| 122 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", | ||
| 123 | "digest": f"sha256:{layer_digest}", | ||
| 124 | "size": len(layer_content) | ||
| 125 | } | ||
| 126 | ] | ||
| 127 | } | ||
| 128 | amd64_manifest_json = json.dumps(amd64_manifest) | ||
| 129 | amd64_manifest_digest = create_mock_blob(blobs, amd64_manifest_json) | ||
| 130 | |||
| 131 | # Create OCI Image Index | ||
| 132 | index = { | ||
| 133 | "schemaVersion": 2, | ||
| 134 | "mediaType": "application/vnd.oci.image.index.v1+json", | ||
| 135 | "manifests": [ | ||
| 136 | { | ||
| 137 | "mediaType": "application/vnd.oci.image.manifest.v1+json", | ||
| 138 | "digest": f"sha256:{arm64_manifest_digest}", | ||
| 139 | "size": len(arm64_manifest_json), | ||
| 140 | "platform": {"architecture": "arm64", "os": "linux"} | ||
| 141 | }, | ||
| 142 | { | ||
| 143 | "mediaType": "application/vnd.oci.image.manifest.v1+json", | ||
| 144 | "digest": f"sha256:{amd64_manifest_digest}", | ||
| 145 | "size": len(amd64_manifest_json), | ||
| 146 | "platform": {"architecture": "amd64", "os": "linux"} | ||
| 147 | } | ||
| 148 | ] | ||
| 149 | } | ||
| 150 | |||
| 151 | (oci_dir / "index.json").write_text(json.dumps(index, indent=2)) | ||
| 152 | (oci_dir / "oci-layout").write_text('{"imageLayoutVersion": "1.0.0"}') | ||
| 153 | |||
| 154 | return oci_dir | ||
| 155 | |||
| 156 | |||
| 157 | @pytest.fixture | ||
| 158 | def singlearch_oci_dir(tmp_path): | ||
| 159 | """Create a mock single-arch OCI directory for testing. | ||
| 160 | |||
| 161 | Creates a standard single-arch OCI without platform info in index.json. | ||
| 162 | """ | ||
| 163 | oci_dir = tmp_path / "test-singlearch-oci" | ||
| 164 | oci_dir.mkdir() | ||
| 165 | |||
| 166 | # Create blobs directory | ||
| 167 | blobs = oci_dir / "blobs" / "sha256" | ||
| 168 | blobs.mkdir(parents=True) | ||
| 169 | |||
| 170 | # Create mock config | ||
| 171 | config = { | ||
| 172 | "architecture": "amd64", | ||
| 173 | "os": "linux", | ||
| 174 | "config": {}, | ||
| 175 | "rootfs": {"type": "layers", "diff_ids": []} | ||
| 176 | } | ||
| 177 | config_json = json.dumps(config) | ||
| 178 | config_digest = create_mock_blob(blobs, config_json) | ||
| 179 | |||
| 180 | # Create mock layer | ||
| 181 | layer_content = b"mock layer content" | ||
| 182 | layer_digest = create_mock_blob(blobs, layer_content, binary=True) | ||
| 183 | |||
| 184 | # Create manifest | ||
| 185 | manifest = { | ||
| 186 | "schemaVersion": 2, | ||
| 187 | "mediaType": "application/vnd.oci.image.manifest.v1+json", | ||
| 188 | "config": { | ||
| 189 | "mediaType": "application/vnd.oci.image.config.v1+json", | ||
| 190 | "digest": f"sha256:{config_digest}", | ||
| 191 | "size": len(config_json) | ||
| 192 | }, | ||
| 193 | "layers": [ | ||
| 194 | { | ||
| 195 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", | ||
| 196 | "digest": f"sha256:{layer_digest}", | ||
| 197 | "size": len(layer_content) | ||
| 198 | } | ||
| 199 | ] | ||
| 200 | } | ||
| 201 | manifest_json = json.dumps(manifest) | ||
| 202 | manifest_digest = create_mock_blob(blobs, manifest_json) | ||
| 203 | |||
| 204 | # Create standard index.json WITHOUT platform info | ||
| 205 | index = { | ||
| 206 | "schemaVersion": 2, | ||
| 207 | "manifests": [ | ||
| 208 | { | ||
| 209 | "mediaType": "application/vnd.oci.image.manifest.v1+json", | ||
| 210 | "digest": f"sha256:{manifest_digest}", | ||
| 211 | "size": len(manifest_json) | ||
| 212 | } | ||
| 213 | ] | ||
| 214 | } | ||
| 215 | |||
| 216 | (oci_dir / "index.json").write_text(json.dumps(index, indent=2)) | ||
| 217 | (oci_dir / "oci-layout").write_text('{"imageLayoutVersion": "1.0.0"}') | ||
| 218 | |||
| 219 | return oci_dir | ||
| 220 | |||
| 221 | |||
| 222 | def create_mock_blob(blobs_dir, content, binary=False): | ||
| 223 | """Create a mock blob and return its digest (without sha256: prefix).""" | ||
| 224 | import hashlib | ||
| 225 | |||
| 226 | if isinstance(content, str): | ||
| 227 | content_bytes = content.encode('utf-8') | ||
| 228 | else: | ||
| 229 | content_bytes = content | ||
| 230 | |||
| 231 | digest = hashlib.sha256(content_bytes).hexdigest() | ||
| 232 | blob_path = blobs_dir / digest | ||
| 233 | |||
| 234 | if binary: | ||
| 235 | blob_path.write_bytes(content_bytes) | ||
| 236 | else: | ||
| 237 | blob_path.write_text(content) | ||
| 238 | |||
| 239 | return digest | ||
| 240 | |||
| 241 | |||
| 242 | def source_shell_functions(vcontainer_files_dir, tmp_path): | ||
| 243 | """Create a test script that sources the shell functions.""" | ||
| 244 | # We need to extract specific functions from vcontainer-common.sh | ||
| 245 | # for unit testing without running the full script | ||
| 246 | test_script = tmp_path / "test_functions.sh" | ||
| 247 | |||
| 248 | # Copy the necessary functions from vcontainer-common.sh | ||
| 249 | vcontainer_common = vcontainer_files_dir / "vcontainer-common.sh" | ||
| 250 | if not vcontainer_common.exists(): | ||
| 251 | return None | ||
| 252 | |||
| 253 | # Read and extract the multi-arch functions | ||
| 254 | content = vcontainer_common.read_text() | ||
| 255 | |||
| 256 | # Find the multi-arch section | ||
| 257 | start_marker = "# Multi-Architecture OCI Support" | ||
| 258 | end_marker = "show_usage()" | ||
| 259 | |||
| 260 | start_idx = content.find(start_marker) | ||
| 261 | end_idx = content.find(end_marker, start_idx) | ||
| 262 | |||
| 263 | if start_idx == -1 or end_idx == -1: | ||
| 264 | return None | ||
| 265 | |||
| 266 | functions = content[start_idx:end_idx] | ||
| 267 | |||
| 268 | test_script.write_text(f"""#!/bin/bash | ||
| 269 | # Extracted multi-arch functions for testing | ||
| 270 | |||
| 271 | {functions} | ||
| 272 | |||
| 273 | # Test harness | ||
| 274 | "$@" | ||
| 275 | """) | ||
| 276 | test_script.chmod(0o755) | ||
| 277 | return test_script | ||
| 278 | |||
| 279 | |||
| 280 | class TestOCIImageIndexDetection: | ||
| 281 | """Test OCI Image Index detection functions.""" | ||
| 282 | |||
| 283 | def test_is_oci_image_index_with_multiarch(self, multiarch_oci_dir, vcontainer_files_dir, tmp_path): | ||
| 284 | """Test that is_oci_image_index detects multi-arch OCI.""" | ||
| 285 | test_script = source_shell_functions(vcontainer_files_dir, tmp_path) | ||
| 286 | if test_script is None: | ||
| 287 | pytest.skip("Could not extract shell functions") | ||
| 288 | |||
| 289 | result = subprocess.run( | ||
| 290 | [str(test_script), "is_oci_image_index", str(multiarch_oci_dir)], | ||
| 291 | capture_output=True, | ||
| 292 | text=True, | ||
| 293 | timeout=10 | ||
| 294 | ) | ||
| 295 | assert result.returncode == 0, f"Expected multi-arch detection to succeed. stderr: {result.stderr}" | ||
| 296 | |||
| 297 | def test_is_oci_image_index_with_single_arch(self, singlearch_oci_dir, vcontainer_files_dir, tmp_path): | ||
| 298 | """Test that is_oci_image_index returns false for single-arch OCI.""" | ||
| 299 | test_script = source_shell_functions(vcontainer_files_dir, tmp_path) | ||
| 300 | if test_script is None: | ||
| 301 | pytest.skip("Could not extract shell functions") | ||
| 302 | |||
| 303 | result = subprocess.run( | ||
| 304 | [str(test_script), "is_oci_image_index", str(singlearch_oci_dir)], | ||
| 305 | capture_output=True, | ||
| 306 | text=True, | ||
| 307 | timeout=10 | ||
| 308 | ) | ||
| 309 | # Single-arch without platform info should return non-zero | ||
| 310 | assert result.returncode != 0, "Expected single-arch detection to fail" | ||
| 311 | |||
| 312 | def test_is_oci_image_index_missing_file(self, tmp_path, vcontainer_files_dir): | ||
| 313 | """Test that is_oci_image_index handles missing index.json.""" | ||
| 314 | test_script = source_shell_functions(vcontainer_files_dir, tmp_path) | ||
| 315 | if test_script is None: | ||
| 316 | pytest.skip("Could not extract shell functions") | ||
| 317 | |||
| 318 | empty_dir = tmp_path / "empty-oci" | ||
| 319 | empty_dir.mkdir() | ||
| 320 | |||
| 321 | result = subprocess.run( | ||
| 322 | [str(test_script), "is_oci_image_index", str(empty_dir)], | ||
| 323 | capture_output=True, | ||
| 324 | text=True, | ||
| 325 | timeout=10 | ||
| 326 | ) | ||
| 327 | assert result.returncode != 0, "Expected missing file detection to fail" | ||
| 328 | |||
| 329 | |||
| 330 | class TestPlatformSelection: | ||
| 331 | """Test architecture selection functions.""" | ||
| 332 | |||
| 333 | def test_select_platform_manifest_aarch64(self, multiarch_oci_dir, vcontainer_files_dir, tmp_path): | ||
| 334 | """Test selecting arm64 manifest for aarch64 target.""" | ||
| 335 | test_script = source_shell_functions(vcontainer_files_dir, tmp_path) | ||
| 336 | if test_script is None: | ||
| 337 | pytest.skip("Could not extract shell functions") | ||
| 338 | |||
| 339 | result = subprocess.run( | ||
| 340 | [str(test_script), "select_platform_manifest", str(multiarch_oci_dir), "aarch64"], | ||
| 341 | capture_output=True, | ||
| 342 | text=True, | ||
| 343 | timeout=10 | ||
| 344 | ) | ||
| 345 | assert result.returncode == 0, f"Expected platform selection to succeed. stderr: {result.stderr}" | ||
| 346 | # Should output a digest | ||
| 347 | assert result.stdout.strip(), "Expected digest output" | ||
| 348 | |||
| 349 | def test_select_platform_manifest_x86_64(self, multiarch_oci_dir, vcontainer_files_dir, tmp_path): | ||
| 350 | """Test selecting amd64 manifest for x86_64 target.""" | ||
| 351 | test_script = source_shell_functions(vcontainer_files_dir, tmp_path) | ||
| 352 | if test_script is None: | ||
| 353 | pytest.skip("Could not extract shell functions") | ||
| 354 | |||
| 355 | result = subprocess.run( | ||
| 356 | [str(test_script), "select_platform_manifest", str(multiarch_oci_dir), "x86_64"], | ||
| 357 | capture_output=True, | ||
| 358 | text=True, | ||
| 359 | timeout=10 | ||
| 360 | ) | ||
| 361 | assert result.returncode == 0, f"Expected platform selection to succeed. stderr: {result.stderr}" | ||
| 362 | assert result.stdout.strip(), "Expected digest output" | ||
| 363 | |||
| 364 | def test_select_platform_manifest_not_found(self, multiarch_oci_dir, vcontainer_files_dir, tmp_path): | ||
| 365 | """Test that selecting missing platform returns error.""" | ||
| 366 | test_script = source_shell_functions(vcontainer_files_dir, tmp_path) | ||
| 367 | if test_script is None: | ||
| 368 | pytest.skip("Could not extract shell functions") | ||
| 369 | |||
| 370 | result = subprocess.run( | ||
| 371 | [str(test_script), "select_platform_manifest", str(multiarch_oci_dir), "riscv64"], | ||
| 372 | capture_output=True, | ||
| 373 | text=True, | ||
| 374 | timeout=10 | ||
| 375 | ) | ||
| 376 | assert result.returncode != 0 or not result.stdout.strip(), "Expected missing platform to fail" | ||
| 377 | |||
| 378 | def test_arch_normalization_aarch64_to_arm64(self, vcontainer_files_dir, tmp_path): | ||
| 379 | """Test that aarch64 normalizes to arm64.""" | ||
| 380 | test_script = source_shell_functions(vcontainer_files_dir, tmp_path) | ||
| 381 | if test_script is None: | ||
| 382 | pytest.skip("Could not extract shell functions") | ||
| 383 | |||
| 384 | result = subprocess.run( | ||
| 385 | [str(test_script), "normalize_arch_to_oci", "aarch64"], | ||
| 386 | capture_output=True, | ||
| 387 | text=True, | ||
| 388 | timeout=10 | ||
| 389 | ) | ||
| 390 | assert result.stdout.strip() == "arm64" | ||
| 391 | |||
| 392 | def test_arch_normalization_x86_64_to_amd64(self, vcontainer_files_dir, tmp_path): | ||
| 393 | """Test that x86_64 normalizes to amd64.""" | ||
| 394 | test_script = source_shell_functions(vcontainer_files_dir, tmp_path) | ||
| 395 | if test_script is None: | ||
| 396 | pytest.skip("Could not extract shell functions") | ||
| 397 | |||
| 398 | result = subprocess.run( | ||
| 399 | [str(test_script), "normalize_arch_to_oci", "x86_64"], | ||
| 400 | capture_output=True, | ||
| 401 | text=True, | ||
| 402 | timeout=10 | ||
| 403 | ) | ||
| 404 | assert result.stdout.strip() == "amd64" | ||
| 405 | |||
| 406 | |||
| 407 | class TestGetOCIPlatforms: | ||
| 408 | """Test platform listing function.""" | ||
| 409 | |||
| 410 | def test_get_platforms_multiarch(self, multiarch_oci_dir, vcontainer_files_dir, tmp_path): | ||
| 411 | """Test getting available platforms from multi-arch OCI.""" | ||
| 412 | test_script = source_shell_functions(vcontainer_files_dir, tmp_path) | ||
| 413 | if test_script is None: | ||
| 414 | pytest.skip("Could not extract shell functions") | ||
| 415 | |||
| 416 | result = subprocess.run( | ||
| 417 | [str(test_script), "get_oci_platforms", str(multiarch_oci_dir)], | ||
| 418 | capture_output=True, | ||
| 419 | text=True, | ||
| 420 | timeout=10 | ||
| 421 | ) | ||
| 422 | assert result.returncode == 0 | ||
| 423 | platforms = result.stdout.strip().split() | ||
| 424 | assert "arm64" in platforms | ||
| 425 | assert "amd64" in platforms | ||
| 426 | |||
| 427 | |||
| 428 | class TestExtractPlatformOCI: | ||
| 429 | """Test single-platform extraction function.""" | ||
| 430 | |||
| 431 | def test_extract_platform_creates_valid_oci(self, multiarch_oci_dir, vcontainer_files_dir, tmp_path): | ||
| 432 | """Test that extract_platform_oci creates a valid single-arch OCI.""" | ||
| 433 | test_script = source_shell_functions(vcontainer_files_dir, tmp_path) | ||
| 434 | if test_script is None: | ||
| 435 | pytest.skip("Could not extract shell functions") | ||
| 436 | |||
| 437 | # First get the arm64 manifest digest | ||
| 438 | result = subprocess.run( | ||
| 439 | [str(test_script), "select_platform_manifest", str(multiarch_oci_dir), "aarch64"], | ||
| 440 | capture_output=True, | ||
| 441 | text=True, | ||
| 442 | timeout=10 | ||
| 443 | ) | ||
| 444 | assert result.returncode == 0 | ||
| 445 | manifest_digest = result.stdout.strip() | ||
| 446 | |||
| 447 | # Extract platform to new directory | ||
| 448 | extracted_dir = tmp_path / "extracted-oci" | ||
| 449 | result = subprocess.run( | ||
| 450 | [str(test_script), "extract_platform_oci", str(multiarch_oci_dir), str(extracted_dir), manifest_digest], | ||
| 451 | capture_output=True, | ||
| 452 | text=True, | ||
| 453 | timeout=10 | ||
| 454 | ) | ||
| 455 | assert result.returncode == 0, f"Extraction failed: {result.stderr}" | ||
| 456 | |||
| 457 | # Verify extracted OCI structure | ||
| 458 | assert (extracted_dir / "index.json").exists() | ||
| 459 | assert (extracted_dir / "oci-layout").exists() | ||
| 460 | assert (extracted_dir / "blobs" / "sha256").is_dir() | ||
| 461 | |||
| 462 | # Verify index.json has single manifest | ||
| 463 | index = json.loads((extracted_dir / "index.json").read_text()) | ||
| 464 | assert len(index.get("manifests", [])) == 1 | ||
| 465 | assert index["manifests"][0]["digest"] == f"sha256:{manifest_digest}" | ||
| 466 | |||
| 467 | |||
| 468 | class TestMultiArchOCIClass: | ||
| 469 | """Test oci-multiarch.bbclass file.""" | ||
| 470 | |||
| 471 | def test_bbclass_exists(self, meta_virt_dir): | ||
| 472 | """Test that the oci-multiarch.bbclass file exists.""" | ||
| 473 | class_file = meta_virt_dir / "classes" / "oci-multiarch.bbclass" | ||
| 474 | assert class_file.exists(), f"Class file not found: {class_file}" | ||
| 475 | |||
| 476 | def test_bbclass_has_required_variables(self, meta_virt_dir): | ||
| 477 | """Test that oci-multiarch.bbclass defines required variables.""" | ||
| 478 | class_file = meta_virt_dir / "classes" / "oci-multiarch.bbclass" | ||
| 479 | content = class_file.read_text() | ||
| 480 | |||
| 481 | assert "OCI_MULTIARCH_RECIPE" in content | ||
| 482 | assert "OCI_MULTIARCH_PLATFORMS" in content | ||
| 483 | assert "OCI_MULTIARCH_MC" in content | ||
| 484 | |||
| 485 | def test_bbclass_creates_image_index(self, meta_virt_dir): | ||
| 486 | """Test that oci-multiarch.bbclass creates OCI Image Index.""" | ||
| 487 | class_file = meta_virt_dir / "classes" / "oci-multiarch.bbclass" | ||
| 488 | content = class_file.read_text() | ||
| 489 | |||
| 490 | assert "do_create_multiarch_index" in content | ||
| 491 | assert "application/vnd.oci.image.index.v1+json" in content | ||
| 492 | |||
| 493 | |||
| 494 | class TestBackwardCompatibility: | ||
| 495 | """Test backward compatibility with single-arch OCI images.""" | ||
| 496 | |||
| 497 | def test_single_arch_oci_structure(self, singlearch_oci_dir): | ||
| 498 | """Verify single-arch OCI has expected structure.""" | ||
| 499 | assert (singlearch_oci_dir / "index.json").exists() | ||
| 500 | assert (singlearch_oci_dir / "oci-layout").exists() | ||
| 501 | |||
| 502 | index = json.loads((singlearch_oci_dir / "index.json").read_text()) | ||
| 503 | assert "manifests" in index | ||
| 504 | assert len(index["manifests"]) == 1 | ||
| 505 | # Single-arch should NOT have platform in manifest entry | ||
| 506 | assert "platform" not in index["manifests"][0] | ||
| 507 | |||
| 508 | def test_single_arch_detection_fails(self, singlearch_oci_dir, vcontainer_files_dir, tmp_path): | ||
| 509 | """Test that single-arch OCI is not detected as multi-arch.""" | ||
| 510 | test_script = source_shell_functions(vcontainer_files_dir, tmp_path) | ||
| 511 | if test_script is None: | ||
| 512 | pytest.skip("Could not extract shell functions") | ||
| 513 | |||
| 514 | result = subprocess.run( | ||
| 515 | [str(test_script), "is_oci_image_index", str(singlearch_oci_dir)], | ||
| 516 | capture_output=True, | ||
| 517 | text=True, | ||
| 518 | timeout=10 | ||
| 519 | ) | ||
| 520 | # Should return non-zero (not a multi-arch image index) | ||
| 521 | assert result.returncode != 0 | ||
| 522 | |||
| 523 | |||
| 524 | class TestVrunnerMultiArch: | ||
| 525 | """Test vrunner.sh multi-arch support.""" | ||
| 526 | |||
| 527 | def test_vrunner_has_multiarch_functions(self, vcontainer_files_dir): | ||
| 528 | """Test that vrunner.sh contains multi-arch functions.""" | ||
| 529 | vrunner = vcontainer_files_dir / "vrunner.sh" | ||
| 530 | assert vrunner.exists() | ||
| 531 | |||
| 532 | content = vrunner.read_text() | ||
| 533 | assert "is_oci_image_index" in content | ||
| 534 | assert "select_platform_manifest" in content | ||
| 535 | assert "extract_platform_oci" in content | ||
| 536 | assert "normalize_arch_to_oci" in content | ||
| 537 | |||
| 538 | def test_vrunner_batch_import_handles_multiarch(self, vcontainer_files_dir): | ||
| 539 | """Test that vrunner batch import section checks for multi-arch.""" | ||
| 540 | vrunner = vcontainer_files_dir / "vrunner.sh" | ||
| 541 | content = vrunner.read_text() | ||
| 542 | |||
| 543 | # Batch import section should check for multi-arch | ||
| 544 | assert "BATCH_IMPORT" in content | ||
| 545 | # The multi-arch handling should be in the batch processing loop | ||
| 546 | assert "is_oci_image_index" in content | ||
| 547 | |||
| 548 | |||
| 549 | class TestVcontainerCommonMultiArch: | ||
| 550 | """Test vcontainer-common.sh multi-arch support.""" | ||
| 551 | |||
| 552 | def test_vcontainer_common_has_multiarch_functions(self, vcontainer_files_dir): | ||
| 553 | """Test that vcontainer-common.sh contains multi-arch functions.""" | ||
| 554 | vcontainer_common = vcontainer_files_dir / "vcontainer-common.sh" | ||
| 555 | assert vcontainer_common.exists() | ||
| 556 | |||
| 557 | content = vcontainer_common.read_text() | ||
| 558 | assert "is_oci_image_index" in content | ||
| 559 | assert "select_platform_manifest" in content | ||
| 560 | assert "extract_platform_oci" in content | ||
| 561 | assert "get_oci_platforms" in content | ||
| 562 | assert "normalize_arch_to_oci" in content | ||
| 563 | assert "normalize_arch_from_oci" in content | ||
| 564 | |||
| 565 | def test_vimport_handles_multiarch(self, vcontainer_files_dir): | ||
| 566 | """Test that vimport section handles multi-arch OCI.""" | ||
| 567 | vcontainer_common = vcontainer_files_dir / "vcontainer-common.sh" | ||
| 568 | content = vcontainer_common.read_text() | ||
| 569 | |||
| 570 | # vimport should detect and handle multi-arch | ||
| 571 | assert "vimport)" in content | ||
| 572 | # Should have multi-arch detection in OCI handling | ||
| 573 | assert "Multi-arch OCI detected" in content or "is_oci_image_index" in content | ||
| 574 | |||
| 575 | |||
| 576 | class TestContainerRegistryMultiArch: | ||
| 577 | """Test container registry multi-arch support.""" | ||
| 578 | |||
| 579 | def test_registry_script_has_manifest_list_support(self, meta_virt_dir): | ||
| 580 | """Test that container-registry-index.bb has manifest list support.""" | ||
| 581 | registry_bb = meta_virt_dir / "recipes-containers" / "container-registry" / "container-registry-index.bb" | ||
| 582 | assert registry_bb.exists() | ||
| 583 | |||
| 584 | content = registry_bb.read_text() | ||
| 585 | # Should have manifest list functions | ||
| 586 | assert "update_manifest_list" in content | ||
| 587 | assert "is_manifest_list" in content | ||
| 588 | assert "get_manifest_list" in content | ||
| 589 | assert "push_by_digest" in content | ||
| 590 | |||
| 591 | def test_registry_script_always_creates_manifest_lists(self, meta_virt_dir): | ||
| 592 | """Test that push always creates manifest lists.""" | ||
| 593 | registry_bb = meta_virt_dir / "recipes-containers" / "container-registry" / "container-registry-index.bb" | ||
| 594 | content = registry_bb.read_text() | ||
| 595 | |||
| 596 | # Should mention manifest lists in push output | ||
| 597 | assert "manifest list" in content.lower() | ||
| 598 | |||
| 599 | def test_registry_script_has_multi_directory_support(self, meta_virt_dir): | ||
| 600 | """Test that container-registry-index.bb supports multi-directory scanning.""" | ||
| 601 | registry_bb = meta_virt_dir / "recipes-containers" / "container-registry" / "container-registry-index.bb" | ||
| 602 | content = registry_bb.read_text() | ||
| 603 | |||
| 604 | # Should have DEPLOY_DIR_IMAGES variable for multi-arch scanning | ||
| 605 | assert "DEPLOY_DIR_IMAGES" in content | ||
| 606 | # Should iterate over machine directories | ||
| 607 | assert "machine_dir" in content | ||
| 608 | # Should show which machine the image is from | ||
| 609 | assert "[from $machine_name]" in content or "from $machine_name" in content | ||
| 610 | |||
| 611 | def test_registry_script_supports_push_by_path(self, meta_virt_dir): | ||
| 612 | """Test that push command supports direct OCI directory path.""" | ||
| 613 | registry_bb = meta_virt_dir / "recipes-containers" / "container-registry" / "container-registry-index.bb" | ||
| 614 | content = registry_bb.read_text() | ||
| 615 | |||
| 616 | # Should detect if argument is a path to an OCI directory | ||
| 617 | assert "index.json" in content | ||
| 618 | # Should have direct path mode | ||
| 619 | assert "Direct path mode" in content or "Pushing OCI directory" in content | ||
| 620 | |||
| 621 | def test_registry_script_supports_push_by_name(self, meta_virt_dir): | ||
| 622 | """Test that push by name scans all machine directories.""" | ||
| 623 | registry_bb = meta_virt_dir / "recipes-containers" / "container-registry" / "container-registry-index.bb" | ||
| 624 | content = registry_bb.read_text() | ||
| 625 | |||
| 626 | # Should support name filter mode | ||
| 627 | assert "image_filter" in content | ||
| 628 | # Should scan all architectures when pushing by name | ||
| 629 | assert "all architectures" in content.lower() or "all archs" in content.lower() | ||
| 630 | |||
| 631 | def test_registry_script_env_var_override(self, meta_virt_dir): | ||
| 632 | """Test that DEPLOY_DIR_IMAGES can be overridden via environment.""" | ||
| 633 | registry_bb = meta_virt_dir / "recipes-containers" / "container-registry" / "container-registry-index.bb" | ||
| 634 | content = registry_bb.read_text() | ||
| 635 | |||
| 636 | # Should use environment variable with fallback to baked-in value | ||
| 637 | assert "${DEPLOY_DIR_IMAGES:-" in content or "DEPLOY_DIR_IMAGES:-" in content | ||
| 638 | |||
| 639 | |||
| 640 | class TestRegistryMultiArchIntegration: | ||
| 641 | """Integration tests for registry multi-arch push (requires registry fixture).""" | ||
| 642 | |||
| 643 | @pytest.fixture | ||
| 644 | def mock_deploy_dirs(self, tmp_path, multiarch_oci_dir): | ||
| 645 | """Create mock deploy directory structure with multiple machines.""" | ||
| 646 | deploy_images = tmp_path / "deploy" / "images" | ||
| 647 | |||
| 648 | # Create qemuarm64 machine dir with arm64 OCI | ||
| 649 | arm64_dir = deploy_images / "qemuarm64" | ||
| 650 | arm64_dir.mkdir(parents=True) | ||
| 651 | arm64_oci = arm64_dir / "container-base-latest-oci" | ||
| 652 | shutil.copytree(multiarch_oci_dir, arm64_oci) | ||
| 653 | |||
| 654 | # Modify the arm64 OCI to only have arm64 manifest | ||
| 655 | arm64_index = json.loads((arm64_oci / "index.json").read_text()) | ||
| 656 | arm64_index["manifests"] = [m for m in arm64_index["manifests"] | ||
| 657 | if m.get("platform", {}).get("architecture") == "arm64"] | ||
| 658 | (arm64_oci / "index.json").write_text(json.dumps(arm64_index, indent=2)) | ||
| 659 | |||
| 660 | # Create qemux86-64 machine dir with amd64 OCI | ||
| 661 | amd64_dir = deploy_images / "qemux86-64" | ||
| 662 | amd64_dir.mkdir(parents=True) | ||
| 663 | amd64_oci = amd64_dir / "container-base-latest-oci" | ||
| 664 | shutil.copytree(multiarch_oci_dir, amd64_oci) | ||
| 665 | |||
| 666 | # Modify the amd64 OCI to only have amd64 manifest | ||
| 667 | amd64_index = json.loads((amd64_oci / "index.json").read_text()) | ||
| 668 | amd64_index["manifests"] = [m for m in amd64_index["manifests"] | ||
| 669 | if m.get("platform", {}).get("architecture") == "amd64"] | ||
| 670 | (amd64_oci / "index.json").write_text(json.dumps(amd64_index, indent=2)) | ||
| 671 | |||
| 672 | return deploy_images | ||
| 673 | |||
| 674 | def test_mock_deploy_structure(self, mock_deploy_dirs): | ||
| 675 | """Verify mock deploy directory structure is correct.""" | ||
| 676 | assert (mock_deploy_dirs / "qemuarm64" / "container-base-latest-oci" / "index.json").exists() | ||
| 677 | assert (mock_deploy_dirs / "qemux86-64" / "container-base-latest-oci" / "index.json").exists() | ||
| 678 | |||
| 679 | # Verify arm64 OCI has arm64 only | ||
| 680 | arm64_index = json.loads( | ||
| 681 | (mock_deploy_dirs / "qemuarm64" / "container-base-latest-oci" / "index.json").read_text() | ||
| 682 | ) | ||
| 683 | assert len(arm64_index["manifests"]) == 1 | ||
| 684 | assert arm64_index["manifests"][0]["platform"]["architecture"] == "arm64" | ||
| 685 | |||
| 686 | # Verify amd64 OCI has amd64 only | ||
| 687 | amd64_index = json.loads( | ||
| 688 | (mock_deploy_dirs / "qemux86-64" / "container-base-latest-oci" / "index.json").read_text() | ||
| 689 | ) | ||
| 690 | assert len(amd64_index["manifests"]) == 1 | ||
| 691 | assert amd64_index["manifests"][0]["platform"]["architecture"] == "amd64" | ||
| 692 | |||
| 693 | def test_discover_oci_in_multiple_machines(self, mock_deploy_dirs): | ||
| 694 | """Test that OCI directories can be discovered in multiple machine dirs.""" | ||
| 695 | found = [] | ||
| 696 | for machine_dir in mock_deploy_dirs.iterdir(): | ||
| 697 | if machine_dir.is_dir(): | ||
| 698 | for oci_dir in machine_dir.glob("*-oci"): | ||
| 699 | if (oci_dir / "index.json").exists(): | ||
| 700 | index = json.loads((oci_dir / "index.json").read_text()) | ||
| 701 | arch = index["manifests"][0].get("platform", {}).get("architecture", "unknown") | ||
| 702 | found.append((machine_dir.name, oci_dir.name, arch)) | ||
| 703 | |||
| 704 | assert len(found) == 2 | ||
| 705 | machines = {f[0] for f in found} | ||
| 706 | assert "qemuarm64" in machines | ||
| 707 | assert "qemux86-64" in machines | ||
| 708 | |||
| 709 | archs = {f[2] for f in found} | ||
| 710 | assert "arm64" in archs | ||
| 711 | assert "amd64" in archs | ||
