diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-06-04 22:03:10 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-06-12 02:58:55 +0000 |
| commit | dfb2c9a331e841faf3cae040439519ecf85a84a1 (patch) | |
| tree | 3b84c044c66dfceea7dd91b86b7773ca0669a3df | |
| parent | 2402ac762883774e3ba207d9cbb8018a46314360 (diff) | |
| download | meta-virtualization-dfb2c9a331e841faf3cae040439519ecf85a84a1.tar.gz | |
tests: parametrize boot tests over container profiles for full coverage
The boot tests (TestBundledContainersBoot, TestCustomServiceFileBoot)
previously depended on whatever CONTAINER_PROFILE was in local.conf,
skipping docker checks when podman was configured and vice versa.
Getting complete coverage required manually editing local.conf and
running the tests twice.
Add --container-profiles pytest option (default: docker,podman) and
a profiled_session fixture that builds container-image-host with each
profile via a temporary bitbake -R conf file, boots it, and runs all
checks. Both docker and podman get a full build-boot-verify cycle in
a single test run with no local.conf changes.
Also fix:
- run_bitbake() gains extra_vars parameter for bitbake -R overrides
- Strip OSC 3008 shell integration escape sequences from run_command
output (BusyBox shell emits these, corrupting file paths used in
subsequent commands)
- Use known file paths for service file content checks instead of
extracting paths from ls output (avoids escape sequence corruption)
- Add test_bundle_class_unpack_enabled to catch do_unpack[noexec]
regression
Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
| -rw-r--r-- | tests/conftest.py | 10 | ||||
| -rw-r--r-- | tests/test_container_cross_install.py | 426 |
2 files changed, 227 insertions, 209 deletions
diff --git a/tests/conftest.py b/tests/conftest.py index 2028ef32..6258611b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py | |||
| @@ -259,6 +259,13 @@ def pytest_addoption(parser): | |||
| 259 | default=False, | 259 | default=False, |
| 260 | help="Run secure registry tests (requires openssl, htpasswd)", | 260 | help="Run secure registry tests (requires openssl, htpasswd)", |
| 261 | ) | 261 | ) |
| 262 | # Container cross-install coverage options | ||
| 263 | parser.addoption( | ||
| 264 | "--container-profiles", | ||
| 265 | action="store", | ||
| 266 | default="docker,podman", | ||
| 267 | help="Comma-separated container profiles to test (default: docker,podman)", | ||
| 268 | ) | ||
| 262 | 269 | ||
| 263 | 270 | ||
| 264 | def _cleanup_stale_test_state(): | 271 | def _cleanup_stale_test_state(): |
| @@ -647,6 +654,9 @@ def pytest_configure(config): | |||
| 647 | "markers", "boot: marks tests that boot a QEMU image (requires built image)" | 654 | "markers", "boot: marks tests that boot a QEMU image (requires built image)" |
| 648 | ) | 655 | ) |
| 649 | config.addinivalue_line( | 656 | config.addinivalue_line( |
| 657 | "markers", "container_profile: marks tests parametrized over container profiles" | ||
| 658 | ) | ||
| 659 | config.addinivalue_line( | ||
| 650 | "markers", "k3s: marks k3s runtime tests" | 660 | "markers", "k3s: marks k3s runtime tests" |
| 651 | ) | 661 | ) |
| 652 | config.addinivalue_line( | 662 | config.addinivalue_line( |
diff --git a/tests/test_container_cross_install.py b/tests/test_container_cross_install.py index f14bfbc5..52eceb0d 100644 --- a/tests/test_container_cross_install.py +++ b/tests/test_container_cross_install.py | |||
| @@ -8,17 +8,22 @@ These tests verify that container-cross-install correctly bundles | |||
| 8 | OCI containers into Yocto images. | 8 | OCI containers into Yocto images. |
| 9 | 9 | ||
| 10 | Run with: | 10 | Run with: |
| 11 | pytest tests/test_container_cross_install.py -v | 11 | pytest tests/test_container_cross_install.py -v --poky-dir /opt/bruce/poky |
| 12 | 12 | ||
| 13 | Run boot tests (requires built image): | 13 | Full coverage (builds + boots with both docker and podman profiles): |
| 14 | pytest tests/test_container_cross_install.py::TestBundledContainersBoot -v | 14 | pytest tests/test_container_cross_install.py -v --poky-dir /opt/bruce/poky --container-profiles docker,podman |
| 15 | |||
| 16 | Single profile only: | ||
| 17 | pytest tests/test_container_cross_install.py -v --poky-dir /opt/bruce/poky --container-profiles docker | ||
| 15 | 18 | ||
| 16 | Environment variables: | 19 | Environment variables: |
| 17 | POKY_DIR: Path to poky directory (default: /opt/bruce/poky) | 20 | POKY_DIR: Path to poky directory (default: /opt/bruce/poky) |
| 18 | BUILD_DIR: Path to build directory (default: $POKY_DIR/build) | 21 | BUILD_DIR: Path to build directory (default: $POKY_DIR/build) |
| 19 | MACHINE: Target machine (default: qemux86-64) | 22 | MACHINE: Target machine (default: qemux86-64) |
| 20 | 23 | ||
| 21 | Note: These tests require a configured Yocto build environment. | 24 | Note: Boot tests (TestBundledContainersBoot, TestCustomServiceFileBoot) build |
| 25 | container-image-host with each --container-profiles profile, boot it, and | ||
| 26 | verify containers are present and runnable. No local.conf editing required. | ||
| 22 | """ | 27 | """ |
| 23 | 28 | ||
| 24 | import os | 29 | import os |
| @@ -87,11 +92,35 @@ def meta_virt_dir(poky_dir): | |||
| 87 | return path | 92 | return path |
| 88 | 93 | ||
| 89 | 94 | ||
| 90 | def run_bitbake(build_dir, recipe, task=None, extra_args=None, timeout=1800): | 95 | def run_bitbake(build_dir, recipe, task=None, extra_args=None, extra_vars=None, timeout=1800): |
| 91 | """Run a bitbake command within the Yocto environment.""" | 96 | """Run a bitbake command within the Yocto environment. |
| 97 | |||
| 98 | Args: | ||
| 99 | build_dir: Path to build directory | ||
| 100 | recipe: Recipe or mc:target to build | ||
| 101 | task: Optional task (e.g., 'rootfs') | ||
| 102 | extra_args: Optional list of extra bitbake arguments | ||
| 103 | extra_vars: Optional dict of variable overrides (written to a | ||
| 104 | temporary conf file and passed via -R) | ||
| 105 | timeout: Command timeout in seconds | ||
| 106 | """ | ||
| 107 | import tempfile | ||
| 108 | |||
| 92 | bb_cmd = "bitbake" | 109 | bb_cmd = "bitbake" |
| 93 | if task: | 110 | if task: |
| 94 | bb_cmd += f" -c {task}" | 111 | bb_cmd += f" -c {task}" |
| 112 | |||
| 113 | # Write variable overrides to a temporary conf file | ||
| 114 | conf_file = None | ||
| 115 | if extra_vars: | ||
| 116 | conf_file = tempfile.NamedTemporaryFile( | ||
| 117 | mode='w', suffix='.conf', prefix='pytest-bbvars-', | ||
| 118 | dir=str(build_dir / "conf"), delete=False) | ||
| 119 | for var, val in extra_vars.items(): | ||
| 120 | conf_file.write(f'{var} = "{val}"\n') | ||
| 121 | conf_file.close() | ||
| 122 | bb_cmd += f" -R {conf_file.name}" | ||
| 123 | |||
| 95 | bb_cmd += f" {recipe}" | 124 | bb_cmd += f" {recipe}" |
| 96 | if extra_args: | 125 | if extra_args: |
| 97 | bb_cmd += " " + " ".join(extra_args) | 126 | bb_cmd += " " + " ".join(extra_args) |
| @@ -99,14 +128,19 @@ def run_bitbake(build_dir, recipe, task=None, extra_args=None, timeout=1800): | |||
| 99 | poky_dir = build_dir.parent | 128 | poky_dir = build_dir.parent |
| 100 | full_cmd = f"bash -c 'cd {poky_dir} && source oe-init-build-env {build_dir} >/dev/null 2>&1 && {bb_cmd}'" | 129 | full_cmd = f"bash -c 'cd {poky_dir} && source oe-init-build-env {build_dir} >/dev/null 2>&1 && {bb_cmd}'" |
| 101 | 130 | ||
| 102 | result = subprocess.run( | 131 | try: |
| 103 | full_cmd, | 132 | result = subprocess.run( |
| 104 | shell=True, | 133 | full_cmd, |
| 105 | cwd=build_dir, | 134 | shell=True, |
| 106 | timeout=timeout, | 135 | cwd=build_dir, |
| 107 | capture_output=True, | 136 | timeout=timeout, |
| 108 | text=True, | 137 | capture_output=True, |
| 109 | ) | 138 | text=True, |
| 139 | ) | ||
| 140 | finally: | ||
| 141 | if conf_file: | ||
| 142 | os.unlink(conf_file.name) | ||
| 143 | |||
| 110 | return result | 144 | return result |
| 111 | 145 | ||
| 112 | 146 | ||
| @@ -439,6 +473,10 @@ class RunqemuSession: | |||
| 439 | # Get everything before the prompt | 473 | # Get everything before the prompt |
| 440 | raw_output = self.child.before | 474 | raw_output = self.child.before |
| 441 | 475 | ||
| 476 | # Strip OSC and other escape sequences from output | ||
| 477 | raw_output = re.sub(r'\x1b\]3008;[^\x07\x1b]*(?:\x07|\x1b\\)', '', raw_output) | ||
| 478 | raw_output = re.sub(r'\x1b\[[0-9;]*[A-Za-z]', '', raw_output) | ||
| 479 | |||
| 442 | # Parse: split by newlines, skip command echo (first line), take the rest | 480 | # Parse: split by newlines, skip command echo (first line), take the rest |
| 443 | lines = raw_output.replace('\r', '').split('\n') | 481 | lines = raw_output.replace('\r', '').split('\n') |
| 444 | 482 | ||
| @@ -589,6 +627,64 @@ def runqemu_session(request, poky_dir, build_dir, machine, check_rootfs_freshnes | |||
| 589 | session.stop() | 627 | session.stop() |
| 590 | 628 | ||
| 591 | 629 | ||
| 630 | def _get_container_profiles(request): | ||
| 631 | """Parse --container-profiles option into a list.""" | ||
| 632 | return [p.strip() for p in request.config.getoption("--container-profiles").split(",")] | ||
| 633 | |||
| 634 | |||
| 635 | @pytest.fixture(scope="class", params=["docker", "podman"]) | ||
| 636 | def profiled_session(request, poky_dir, build_dir, machine): | ||
| 637 | """ | ||
| 638 | Fixture that builds container-image-host with a specific CONTAINER_PROFILE, | ||
| 639 | boots it, and provides a session for testing. | ||
| 640 | |||
| 641 | Parametrized over profiles from --container-profiles (default: docker,podman). | ||
| 642 | Each profile gets a full build → boot → test cycle. | ||
| 643 | """ | ||
| 644 | profile = request.param | ||
| 645 | profiles = _get_container_profiles(request) | ||
| 646 | if profile not in profiles: | ||
| 647 | pytest.skip(f"Profile {profile} not in --container-profiles={','.join(profiles)}") | ||
| 648 | |||
| 649 | if not PEXPECT_AVAILABLE: | ||
| 650 | pytest.skip("pexpect not installed. Run: pip install pexpect") | ||
| 651 | |||
| 652 | image = request.config.getoption("--image") | ||
| 653 | fstype = request.config.getoption("--image-fstype") | ||
| 654 | timeout = request.config.getoption("--boot-timeout") | ||
| 655 | use_kvm = not request.config.getoption("--no-kvm") | ||
| 656 | |||
| 657 | # Build the image with this profile | ||
| 658 | result = run_bitbake( | ||
| 659 | build_dir, image, | ||
| 660 | extra_vars={"CONTAINER_PROFILE": profile}, | ||
| 661 | timeout=3600, | ||
| 662 | ) | ||
| 663 | if result.returncode != 0: | ||
| 664 | pytest.fail(f"Build failed for profile {profile}: {result.stderr}") | ||
| 665 | |||
| 666 | session = RunqemuSession(poky_dir, build_dir, machine, image, | ||
| 667 | fstype=fstype, use_kvm=use_kvm, timeout=timeout) | ||
| 668 | session.container_profile = profile | ||
| 669 | |||
| 670 | try: | ||
| 671 | session.start() | ||
| 672 | yield session | ||
| 673 | finally: | ||
| 674 | session.stop() | ||
| 675 | |||
| 676 | |||
| 677 | @pytest.fixture(scope="class") | ||
| 678 | def profiled_containers(profiled_session, build_dir, machine): | ||
| 679 | """Detect containers in the booted image for the active profile.""" | ||
| 680 | image = "container-image-host" | ||
| 681 | result = _detect_containers_from_rootfs(build_dir, machine, image) | ||
| 682 | if not result: | ||
| 683 | result = {'docker': [], 'podman': []} | ||
| 684 | result['profile'] = profiled_session.container_profile | ||
| 685 | return result | ||
| 686 | |||
| 687 | |||
| 592 | def _detect_containers_from_rootfs(build_dir, machine, image): | 688 | def _detect_containers_from_rootfs(build_dir, machine, image): |
| 593 | """ | 689 | """ |
| 594 | Detect bundled containers by checking storage directories in the rootfs. | 690 | Detect bundled containers by checking storage directories in the rootfs. |
| @@ -775,135 +871,75 @@ class TestBundledContainersBoot: | |||
| 775 | """ | 871 | """ |
| 776 | Boot tests to verify bundled containers are visible. | 872 | Boot tests to verify bundled containers are visible. |
| 777 | 873 | ||
| 778 | These tests boot the actual Yocto image and verify that | 874 | Parametrized over container profiles (--container-profiles, default: docker,podman). |
| 779 | `docker images` or `podman images` shows the bundled containers. | 875 | For each profile, builds container-image-host with that CONTAINER_PROFILE, |
| 780 | 876 | boots the image, and verifies the profile's containers are present and runnable. | |
| 781 | Prerequisites: | ||
| 782 | - Image must be built with BUNDLED_CONTAINERS configured | ||
| 783 | - pexpect must be installed: pip install pexpect | ||
| 784 | 877 | ||
| 785 | Run with: | 878 | Run with: |
| 786 | pytest tests/test_container_cross_install.py::TestBundledContainersBoot -v | 879 | pytest tests/test_container_cross_install.py::TestBundledContainersBoot -v |
| 787 | 880 | ||
| 788 | Options: | 881 | Options: |
| 789 | --image IMAGE Image name to boot (default: core-image-minimal) | 882 | --container-profiles Profiles to test (default: docker,podman) |
| 883 | --image IMAGE Image name to boot (default: container-image-host) | ||
| 790 | --machine MACHINE Machine to use (default: qemux86-64) | 884 | --machine MACHINE Machine to use (default: qemux86-64) |
| 791 | --boot-timeout SECS Timeout for boot (default: 120) | 885 | --boot-timeout SECS Timeout for boot (default: 120) |
| 792 | """ | 886 | """ |
| 793 | 887 | ||
| 794 | @pytest.mark.slow | 888 | @pytest.mark.slow |
| 795 | @pytest.mark.boot | 889 | @pytest.mark.boot |
| 796 | def test_system_boots(self, runqemu_session): | 890 | @pytest.mark.container_profile |
| 891 | def test_system_boots(self, profiled_session): | ||
| 797 | """Test that the system boots successfully.""" | 892 | """Test that the system boots successfully.""" |
| 798 | assert runqemu_session.booted, "System failed to boot" | 893 | assert profiled_session.booted, \ |
| 894 | f"System failed to boot with profile {profiled_session.container_profile}" | ||
| 799 | 895 | ||
| 800 | # Basic sanity check | 896 | output = profiled_session.run_command('uname -a') |
| 801 | output = runqemu_session.run_command('uname -a') | ||
| 802 | assert 'Linux' in output, f"Unexpected uname output: {output}" | 897 | assert 'Linux' in output, f"Unexpected uname output: {output}" |
| 803 | 898 | ||
| 804 | @pytest.mark.slow | 899 | @pytest.mark.slow |
| 805 | @pytest.mark.boot | 900 | @pytest.mark.boot |
| 806 | def test_docker_images_visible(self, runqemu_session, bundled_containers_config): | 901 | @pytest.mark.container_profile |
| 807 | """Test that bundled Docker containers are visible.""" | 902 | def test_containers_visible(self, profiled_session, profiled_containers): |
| 808 | expected = bundled_containers_config['docker'] | 903 | """Test that bundled containers are visible for the active profile.""" |
| 809 | if not expected: | 904 | profile = profiled_session.container_profile |
| 810 | pytest.skip("No Docker containers in bundle packages or BUNDLED_CONTAINERS") | 905 | runtime = profile # docker or podman |
| 811 | 906 | ||
| 812 | # Check if docker is available | 907 | output = profiled_session.run_command(f'which {runtime}') |
| 813 | output = runqemu_session.run_command('which docker') | 908 | assert f'/{runtime}' in output, \ |
| 814 | if '/docker' not in output: | 909 | f"{runtime} not installed in image (profile={profile})" |
| 815 | pytest.skip("docker not installed in image") | ||
| 816 | 910 | ||
| 817 | # Get docker images | 911 | output = profiled_session.run_command(f'{runtime} images', timeout=30) |
| 818 | output = runqemu_session.run_command('docker images', timeout=30) | 912 | print(f"{runtime} images output ({profile}):\n{output}") |
| 819 | print(f"docker images output:\n{output}") | ||
| 820 | 913 | ||
| 821 | # Verify each expected container is present | 914 | expected = profiled_containers.get(runtime, []) |
| 822 | missing = [] | 915 | assert expected, \ |
| 823 | for container in expected: | 916 | f"No {runtime} containers detected in rootfs (profile={profile})" |
| 824 | if container not in output: | ||
| 825 | missing.append(container) | ||
| 826 | 917 | ||
| 827 | assert not missing, f"Missing Docker containers: {missing}\nOutput:\n{output}" | 918 | missing = [c for c in expected if c not in output] |
| 919 | assert not missing, \ | ||
| 920 | f"Missing {runtime} containers: {missing}\nOutput:\n{output}" | ||
| 828 | 921 | ||
| 829 | @pytest.mark.slow | 922 | @pytest.mark.slow |
| 830 | @pytest.mark.boot | 923 | @pytest.mark.boot |
| 831 | def test_podman_images_visible(self, runqemu_session, bundled_containers_config): | 924 | @pytest.mark.container_profile |
| 832 | """Test that bundled Podman containers are visible.""" | 925 | def test_run_bundled_container(self, profiled_session, profiled_containers): |
| 833 | expected = bundled_containers_config['podman'] | 926 | """Test that a bundled container can actually run.""" |
| 834 | if not expected: | 927 | profile = profiled_session.container_profile |
| 835 | pytest.skip("No Podman containers in bundle packages or BUNDLED_CONTAINERS") | 928 | runtime = profile |
| 836 | |||
| 837 | # Check if podman is available | ||
| 838 | output = runqemu_session.run_command('which podman') | ||
| 839 | if '/podman' not in output: | ||
| 840 | pytest.skip("podman not installed in image") | ||
| 841 | |||
| 842 | # Get podman images | ||
| 843 | output = runqemu_session.run_command('podman images', timeout=30) | ||
| 844 | print(f"podman images output:\n{output}") | ||
| 845 | |||
| 846 | # Verify each expected container is present | ||
| 847 | missing = [] | ||
| 848 | for container in expected: | ||
| 849 | if container not in output: | ||
| 850 | missing.append(container) | ||
| 851 | |||
| 852 | assert not missing, f"Missing Podman containers: {missing}\nOutput:\n{output}" | ||
| 853 | |||
| 854 | @pytest.mark.slow | ||
| 855 | @pytest.mark.boot | ||
| 856 | def test_docker_run_bundled_container(self, runqemu_session, bundled_containers_config): | ||
| 857 | """Test that a bundled Docker container can actually run.""" | ||
| 858 | expected = bundled_containers_config['docker'] | ||
| 859 | if not expected: | ||
| 860 | pytest.skip("No Docker containers configured") | ||
| 861 | |||
| 862 | # Check if docker is available | ||
| 863 | output = runqemu_session.run_command('which docker') | ||
| 864 | if '/docker' not in output: | ||
| 865 | pytest.skip("docker not installed in image") | ||
| 866 | |||
| 867 | # Try to run the first bundled container with a simple command | ||
| 868 | # Use --entrypoint to override any image entrypoint, otherwise | ||
| 869 | # images with entrypoint like ["sh"] would interpret the command | ||
| 870 | # as a script argument rather than executing the binary directly | ||
| 871 | container = expected[0] | ||
| 872 | output = runqemu_session.run_command( | ||
| 873 | f'docker run --rm --entrypoint /bin/echo {container}:latest "CONTAINER_WORKS"', | ||
| 874 | timeout=60 | ||
| 875 | ) | ||
| 876 | print(f"docker run output:\n{output}") | ||
| 877 | 929 | ||
| 878 | assert 'CONTAINER_WORKS' in output, \ | 930 | expected = profiled_containers.get(runtime, []) |
| 879 | f"Container {container} failed to run.\nOutput:\n{output}" | 931 | assert expected, \ |
| 932 | f"No {runtime} containers detected in rootfs (profile={profile})" | ||
| 880 | 933 | ||
| 881 | @pytest.mark.slow | ||
| 882 | @pytest.mark.boot | ||
| 883 | def test_podman_run_bundled_container(self, runqemu_session, bundled_containers_config): | ||
| 884 | """Test that a bundled Podman container can actually run.""" | ||
| 885 | expected = bundled_containers_config['podman'] | ||
| 886 | if not expected: | ||
| 887 | pytest.skip("No Podman containers in bundle packages or BUNDLED_CONTAINERS") | ||
| 888 | |||
| 889 | # Check if podman is available | ||
| 890 | output = runqemu_session.run_command('which podman') | ||
| 891 | if '/podman' not in output: | ||
| 892 | pytest.skip("podman not installed in image") | ||
| 893 | |||
| 894 | # Try to run the first bundled container with a simple command | ||
| 895 | # Use --entrypoint to override any image entrypoint, otherwise | ||
| 896 | # images with entrypoint like ["sh"] would interpret the command | ||
| 897 | # as a script argument rather than executing the binary directly | ||
| 898 | container = expected[0] | 934 | container = expected[0] |
| 899 | output = runqemu_session.run_command( | 935 | output = profiled_session.run_command( |
| 900 | f'podman run --rm --entrypoint /bin/echo {container}:latest "CONTAINER_WORKS"', | 936 | f'{runtime} run --rm --entrypoint /bin/echo {container}:latest "CONTAINER_WORKS"', |
| 901 | timeout=60 | 937 | timeout=60 |
| 902 | ) | 938 | ) |
| 903 | print(f"podman run output:\n{output}") | 939 | print(f"{runtime} run output ({profile}):\n{output}") |
| 904 | 940 | ||
| 905 | assert 'CONTAINER_WORKS' in output, \ | 941 | assert 'CONTAINER_WORKS' in output, \ |
| 906 | f"Container {container} failed to run.\nOutput:\n{output}" | 942 | f"Container {container} failed to run with {runtime}.\nOutput:\n{output}" |
| 907 | 943 | ||
| 908 | 944 | ||
| 909 | # ============================================================================ | 945 | # ============================================================================ |
| @@ -933,6 +969,22 @@ class TestCustomServiceFileSupport: | |||
| 933 | assert "install_custom_service" in content, \ | 969 | assert "install_custom_service" in content, \ |
| 934 | "install_custom_service function not found" | 970 | "install_custom_service function not found" |
| 935 | 971 | ||
| 972 | def test_bundle_class_unpack_enabled(self, meta_virt_dir): | ||
| 973 | """Test that do_unpack is NOT disabled in container-bundle.bbclass. | ||
| 974 | |||
| 975 | Custom service files use SRC_URI file:// entries which require | ||
| 976 | do_unpack to run. If do_unpack[noexec] = "1" is set, the files | ||
| 977 | never reach UNPACKDIR and CONTAINER_SERVICE_FILE references fail. | ||
| 978 | """ | ||
| 979 | class_file = meta_virt_dir / "classes" / "container-bundle.bbclass" | ||
| 980 | content = class_file.read_text() | ||
| 981 | for line in content.splitlines(): | ||
| 982 | stripped = line.strip() | ||
| 983 | if stripped.startswith("#"): | ||
| 984 | continue | ||
| 985 | assert 'do_unpack[noexec]' not in stripped, \ | ||
| 986 | "do_unpack must not be noexec — custom service files need SRC_URI unpacking" | ||
| 987 | |||
| 936 | def test_bundle_class_has_service_file_support(self, meta_virt_dir): | 988 | def test_bundle_class_has_service_file_support(self, meta_virt_dir): |
| 937 | """Test that container-bundle.bbclass includes CONTAINER_SERVICE_FILE support.""" | 989 | """Test that container-bundle.bbclass includes CONTAINER_SERVICE_FILE support.""" |
| 938 | class_file = meta_virt_dir / "classes" / "container-bundle.bbclass" | 990 | class_file = meta_virt_dir / "classes" / "container-bundle.bbclass" |
| @@ -983,118 +1035,74 @@ class TestCustomServiceFileBoot: | |||
| 983 | """ | 1035 | """ |
| 984 | Boot tests for custom service files. | 1036 | Boot tests for custom service files. |
| 985 | 1037 | ||
| 986 | These tests verify that custom service files are properly installed | 1038 | Parametrized over container profiles — verifies that autostart services |
| 987 | and enabled in the booted system. | 1039 | (Docker systemd units or Podman Quadlet files) are correctly installed |
| 1040 | and enabled for each profile. | ||
| 988 | """ | 1041 | """ |
| 989 | 1042 | ||
| 990 | @pytest.mark.slow | 1043 | @pytest.mark.slow |
| 991 | @pytest.mark.boot | 1044 | @pytest.mark.boot |
| 992 | def test_systemd_services_directory_exists(self, runqemu_session): | 1045 | @pytest.mark.container_profile |
| 1046 | def test_systemd_services_directory_exists(self, profiled_session): | ||
| 993 | """Test that systemd service directories exist.""" | 1047 | """Test that systemd service directories exist.""" |
| 994 | output = runqemu_session.run_command('ls -la /lib/systemd/system/ | head -n 5') | 1048 | output = profiled_session.run_command('ls -la /lib/systemd/system/ | head -n 5') |
| 995 | assert 'systemd' in output or 'total' in output, \ | 1049 | assert 'systemd' in output or 'total' in output, \ |
| 996 | "Systemd system directory not accessible" | 1050 | "Systemd system directory not accessible" |
| 997 | 1051 | ||
| 998 | @pytest.mark.slow | 1052 | @pytest.mark.slow |
| 999 | @pytest.mark.boot | 1053 | @pytest.mark.boot |
| 1000 | def test_container_services_present(self, runqemu_session, bundled_containers_config): | 1054 | @pytest.mark.container_profile |
| 1001 | """Test that container service files are present (custom or generated).""" | 1055 | def test_autostart_service_present(self, profiled_session): |
| 1002 | docker_containers = bundled_containers_config.get('docker', []) | 1056 | """Test that autostart service files are present for the active profile.""" |
| 1003 | 1057 | profile = profiled_session.container_profile | |
| 1004 | if not docker_containers: | 1058 | |
| 1005 | pytest.skip("No Docker containers configured") | 1059 | if profile == 'docker': |
| 1006 | 1060 | output = profiled_session.run_command( | |
| 1007 | # Check if docker is available | 1061 | 'ls /lib/systemd/system/container-*.service 2>/dev/null || echo "NONE"') |
| 1008 | output = runqemu_session.run_command('which docker') | 1062 | assert 'NONE' not in output and '.service' in output, \ |
| 1009 | if '/docker' not in output: | 1063 | f"No Docker autostart service files found (profile={profile})" |
| 1010 | pytest.skip("docker not installed in image") | 1064 | |
| 1011 | 1065 | elif profile == 'podman': | |
| 1012 | # Check for container service files | 1066 | output = profiled_session.run_command( |
| 1013 | output = runqemu_session.run_command('ls /lib/systemd/system/container-*.service 2>/dev/null || echo "NONE"') | 1067 | 'ls /etc/containers/systemd/*.container 2>/dev/null || echo "NONE"') |
| 1014 | 1068 | assert 'NONE' not in output and '.container' in output, \ | |
| 1015 | if 'NONE' in output: | 1069 | f"No Podman Quadlet files found (profile={profile})" |
| 1016 | # No autostart services - check if any containers have autostart | ||
| 1017 | pytest.skip("No container autostart services found (containers may not have autostart enabled)") | ||
| 1018 | |||
| 1019 | # Verify at least one service file exists | ||
| 1020 | assert '.service' in output, \ | ||
| 1021 | f"No container service files found. Output: {output}" | ||
| 1022 | |||
| 1023 | @pytest.mark.slow | ||
| 1024 | @pytest.mark.boot | ||
| 1025 | def test_container_service_enabled(self, runqemu_session, bundled_containers_config): | ||
| 1026 | """Test that container services are enabled (linked in wants directory).""" | ||
| 1027 | docker_containers = bundled_containers_config.get('docker', []) | ||
| 1028 | |||
| 1029 | if not docker_containers: | ||
| 1030 | pytest.skip("No Docker containers configured") | ||
| 1031 | |||
| 1032 | # Check for enabled services in multi-user.target.wants | ||
| 1033 | output = runqemu_session.run_command( | ||
| 1034 | 'ls /etc/systemd/system/multi-user.target.wants/container-*.service 2>/dev/null || echo "NONE"' | ||
| 1035 | ) | ||
| 1036 | |||
| 1037 | if 'NONE' in output: | ||
| 1038 | pytest.skip("No container autostart services enabled") | ||
| 1039 | |||
| 1040 | # Verify services are symlinked | ||
| 1041 | assert '.service' in output, \ | ||
| 1042 | f"No enabled container services found. Output: {output}" | ||
| 1043 | 1070 | ||
| 1044 | @pytest.mark.slow | 1071 | @pytest.mark.slow |
| 1045 | @pytest.mark.boot | 1072 | @pytest.mark.boot |
| 1046 | def test_custom_service_content(self, runqemu_session, bundled_containers_config): | 1073 | @pytest.mark.container_profile |
| 1047 | """Test that custom service files have expected content markers.""" | 1074 | def test_autostart_service_running(self, profiled_session): |
| 1048 | docker_containers = bundled_containers_config.get('docker', []) | 1075 | """Test that the autostart container is actually running.""" |
| 1049 | 1076 | profile = profiled_session.container_profile | |
| 1050 | if not docker_containers: | 1077 | runtime = profile |
| 1051 | pytest.skip("No Docker containers configured") | ||
| 1052 | 1078 | ||
| 1053 | # Find a container service file | 1079 | output = profiled_session.run_command(f'{runtime} ps', timeout=30) |
| 1054 | output = runqemu_session.run_command( | 1080 | print(f"{runtime} ps output ({profile}):\n{output}") |
| 1055 | 'ls /lib/systemd/system/container-*.service 2>/dev/null | head -1' | ||
| 1056 | ) | ||
| 1057 | |||
| 1058 | if not output or 'container-' not in output: | ||
| 1059 | pytest.skip("No container service files found") | ||
| 1060 | |||
| 1061 | service_file = output.strip().split('\n')[0] | ||
| 1062 | 1081 | ||
| 1063 | # Read the service file content | 1082 | assert 'autostart-test-container' in output, \ |
| 1064 | content = runqemu_session.run_command(f'cat {service_file}') | 1083 | f"autostart-test-container not running under {runtime} (profile={profile})\nOutput:\n{output}" |
| 1065 | |||
| 1066 | # Verify it has expected systemd service structure | ||
| 1067 | assert '[Unit]' in content, f"Service file missing [Unit] section: {service_file}" | ||
| 1068 | assert '[Service]' in content, f"Service file missing [Service] section: {service_file}" | ||
| 1069 | assert '[Install]' in content, f"Service file missing [Install] section: {service_file}" | ||
| 1070 | |||
| 1071 | # Check for docker-related content | ||
| 1072 | assert 'docker' in content.lower(), \ | ||
| 1073 | f"Service file doesn't reference docker: {content}" | ||
| 1074 | 1084 | ||
| 1075 | @pytest.mark.slow | 1085 | @pytest.mark.slow |
| 1076 | @pytest.mark.boot | 1086 | @pytest.mark.boot |
| 1077 | def test_podman_quadlet_directory(self, runqemu_session, bundled_containers_config): | 1087 | @pytest.mark.container_profile |
| 1078 | """Test Podman Quadlet directory exists for Podman containers.""" | 1088 | def test_autostart_service_content(self, profiled_session): |
| 1079 | podman_containers = bundled_containers_config.get('podman', []) | 1089 | """Test that autostart service files have expected content.""" |
| 1080 | 1090 | profile = profiled_session.container_profile | |
| 1081 | if not podman_containers: | 1091 | |
| 1082 | pytest.skip("No Podman containers configured") | 1092 | if profile == 'docker': |
| 1083 | 1093 | content = profiled_session.run_command( | |
| 1084 | # Check if podman is available | 1094 | 'cat /lib/systemd/system/container-autostart-test-container.service') |
| 1085 | output = runqemu_session.run_command('which podman') | 1095 | assert '[Unit]' in content, \ |
| 1086 | if '/podman' not in output: | 1096 | f"Missing [Unit] section in docker service file.\nContent:\n{content}" |
| 1087 | pytest.skip("podman not installed in image") | 1097 | assert '[Service]' in content, \ |
| 1088 | 1098 | f"Missing [Service] section in docker service file" | |
| 1089 | # Check for Quadlet directory | 1099 | assert 'docker' in content.lower(), \ |
| 1090 | output = runqemu_session.run_command('ls -la /etc/containers/systemd/ 2>/dev/null || echo "NONE"') | 1100 | f"Service doesn't reference docker" |
| 1091 | 1101 | ||
| 1092 | if 'NONE' in output: | 1102 | elif profile == 'podman': |
| 1093 | pytest.skip("Quadlet directory not found (containers may not have autostart enabled)") | 1103 | content = profiled_session.run_command( |
| 1094 | 1104 | 'cat /etc/containers/systemd/container-autostart-test-container.container') | |
| 1095 | # Check for .container files | 1105 | assert '[Container]' in content, \ |
| 1096 | output = runqemu_session.run_command('ls /etc/containers/systemd/*.container 2>/dev/null || echo "NONE"') | 1106 | f"Missing [Container] section in podman quadlet file.\nContent:\n{content}" |
| 1097 | 1107 | assert 'Image=' in content, \ | |
| 1098 | if 'NONE' not in output: | 1108 | f"Missing Image= directive in podman quadlet file" |
| 1099 | assert '.container' in output, \ | ||
| 1100 | f"No Quadlet container files found. Output: {output}" | ||
