summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-01-15 21:50:38 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-01-21 18:00:26 -0500
commit02bce5b72e8725ba58d82627c780e376ac59a84b (patch)
tree8e01635166b3d475fbf4fef34150ccde7ccda21a /tests
parent640fc9278435c49b8c59d78c18d024c66b3d6e6a (diff)
downloadmeta-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.md63
-rw-r--r--tests/conftest.py10
-rw-r--r--tests/test_multiarch_oci.py711
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)
35MACHINE=qemux86-64 bitbake vcontainer-tarball 35MACHINE=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
42cd /tmp/vcontainer 42cd /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
467This 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)
471cd /opt/bruce/poky && source oe-init-build-env && \
472bitbake mc:vruntime-aarch64:vdkr-initramfs-create && \
473bitbake mc:vruntime-x86-64:vdkr-initramfs-create && \
474bitbake mc:vruntime-aarch64:vpdmn-initramfs-create && \
475bitbake mc:vruntime-x86-64:vpdmn-initramfs-create && \
476MACHINE=qemux86-64 bitbake vcontainer-tarball && \
477MACHINE=qemux86-64 bitbake container-image-host && \
478MACHINE=qemuarm64 bitbake container-image-host && \
479MACHINE=qemux86-64 bitbake container-app-base && \
480MACHINE=qemuarm64 bitbake container-app-base
481```
482
483Then 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 && \
488cd /opt/bruce/poky/meta-virtualization && \
489pytest tests/ -v --vdkr-dir /tmp/vcontainer --poky-dir /opt/bruce/poky
490```
491
492To test a specific architecture:
493
494```bash
495# Test x86_64
496pytest tests/ -v --vdkr-dir /tmp/vcontainer --poky-dir /opt/bruce/poky --arch x86_64
497
498# Test aarch64
499pytest 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
457MACHINE=qemux86-64 bitbake vcontainer-tarball 510MACHINE=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)
463cd /opt/bruce/poky/meta-virtualization 516cd /opt/bruce/poky/meta-virtualization
@@ -474,7 +527,7 @@ pytest tests/test_vdkr.py tests/test_vpdmn.py -v --vdkr-dir /tmp/vcontainer
474MACHINE=qemux86-64 bitbake vcontainer-tarball 527MACHINE=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
480pytest tests/test_vdkr.py -v --vdkr-dir /tmp/vcontainer 533pytest 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"""
5Tests for multi-architecture OCI container support.
6
7These 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
13Run with:
14 pytest tests/test_multiarch_oci.py -v --poky-dir /opt/bruce/poky
15
16Environment variables:
17 POKY_DIR: Path to poky directory (default: /opt/bruce/poky)
18
19Note: Some tests require the shell scripts from meta-virtualization/recipes-containers/vcontainer/files/
20"""
21
22import os
23import json
24import subprocess
25import tempfile
26import shutil
27import pytest
28from pathlib import Path
29
30
31# Note: Command line options are defined in conftest.py
32
33
34@pytest.fixture(scope="module")
35def 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")
45def 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
54def 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
158def 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
222def 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
242def 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
280class 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
330class 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
407class 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
428class 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
468class 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
494class 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
524class 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
549class 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
576class 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
640class 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