Skip to content
1 change: 1 addition & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ of Firecracker processes. There are two layers:
- `rootfs` — the rootfs disk path, composed from `guest_kernel` +
`rootfs_mode` (Ubuntu 24.04 for 5.10, Amazon Linux 2023 otherwise).
- `pci_enabled` — auto-parametrized over `True`/`False`.
- `vm_backend` — auto-parametrized over `"kvm"`.
- `cpu_template` — `None` by default.
- `huge_pages` — `HugePagesConfig.NONE` by default. See note below.
- `vcpu_count`, `mem_size_mib` — `2` and `256` by default.
Expand Down
93 changes: 75 additions & 18 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,17 @@
from framework import defs, utils
from framework.artifacts import ALL_GUEST_KERNELS, disks
from framework.defs import ARTIFACT_DIR, DEFAULT_BINARY_DIR
from framework.microvm import HugePagesConfig, MicroVMFactory, SnapshotType
from framework.microvm import MicroVMFactory, SnapshotType
from framework.properties import global_props
from framework.utils_cpu_templates import get_cpu_template_name
from framework.utils_hugepages import HugePagesConfig
from framework.vm_backend import (
available_vm_backend_params,
available_vm_backends,
get_vm_backend,
vm_backend_probe_report,
)
from framework.vm_lifecycle import VmLifecycle
from host_tools.metrics import get_metrics_logger
from host_tools.network import NetNs

Expand Down Expand Up @@ -104,10 +112,20 @@ def pytest_report_header():
[
f"EC2 AMI: {global_props.ami}",
f"EC2 Instance ID: {global_props.instance_id}",
*vm_backend_probe_report(),
]
)


@pytest.hookimpl(trylast=True)
def pytest_collection_modifyitems(items):
"""Fail only when the selected tests require an unavailable VM backend."""
if not available_vm_backends() and any(
"vm_backend" in getattr(item, "fixturenames", ()) for item in items
):
pytest.exit("No VM backends are available on this host", returncode=5)


@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call): # pylint:disable=unused-argument
"""Plugin to get test results in fixtures
Expand Down Expand Up @@ -389,7 +407,9 @@ def get(self, _netns_id):

@pytest.fixture()
# pylint: disable=unused-argument
def microvm_factory(request, record_property, results_dir, netns_factory, reap_orphans):
def microvm_factory(
request, record_property, results_dir, netns_factory, reap_orphans, vm_backend
):
"""Fixture to create microvms simply.

`reap_orphans` is requested only for teardown ordering (reaping runs
Expand Down Expand Up @@ -420,6 +440,7 @@ def microvm_factory(request, record_property, results_dir, netns_factory, reap_o
binary_dir,
netns_factory=netns_factory,
custom_cpu_template=custom_cpu_template,
backend=vm_backend,
)
yield uvm_factory

Expand Down Expand Up @@ -554,6 +575,20 @@ def huge_pages(request):
return getattr(request, "param", HugePagesConfig.NONE)


@pytest.fixture(params=available_vm_backend_params())
def vm_backend(request, record_property):
"""Backend used by the `uvm*` fixtures.

Tests are auto-multiplied over the backends this host can actually run
(`available_vm_backends`).
"""
if request.param not in available_vm_backends():
pytest.fail(f"Backend {request.param!r} not available on this host")
backend = get_vm_backend(request.param)
record_property("vm_backend", str(backend))
return backend


# =============================================================================
# Composable uvm fixture system
# =============================================================================
Expand All @@ -570,19 +605,37 @@ def huge_pages(request):
# defaults baked into their bodies. Override a dim with
# `@pytest.mark.parametrize(<dim>, [...], indirect=True)` or use the helpers
# from `framework.artifacts` (`pin_guest_kernel`, `pin_rootfs_mode`,
# `pin_pci`, `pin_cpu_template`).
# `pin_pci`) and `framework.utils_cpu_templates` (`pin_cpu_template`).
#
# Module-level `pytestmark` works for tests that don't override that same
# dim per-test — pytest's parametrize markers do NOT merge: a pytestmark +
# per-test parametrize on the same argname raises "duplicate parametrization".
#
# Modules may also customise a lifecycle *stage* by defining a fixture with
# the same name as `uvm_configured` or `uvm_booted` (fixture shadowing).
# Pytest resolves the module-local fixture when the shared later stages
# (`uvm_restored`, `uvm_any`) request that name, so booted and restored
# variants both pick up the customisation. The override may request the
# stage it replaces (same-name chaining) to decorate rather than
# reimplement it, e.g. `def uvm_booted(uvm_booted): ...`.
#
# Shadow a stage only when every lifecycle-aware test in the module wants
# the identical customisation; files with several boot configurations
# should keep explicitly named fixtures. Prefer overriding a dimension
# over shadowing a stage when the customisation is a plain configuration
# value with an existing dimension; knobs too rare to be worth a dimension
# (e.g. `boot_args`) shadow `uvm_configured` instead. See `test_rng.py`
# (stage overrides), `test_sysgenid.py` (stage decoration) and
# `test_fips.py` (custom boot args via a `uvm_configured` override).
#
# Dimensions:
# guest_kernel Path to a guest kernel artifact auto-multiplied
# guest_kernel Logical guest kernel variant auto-multiplied
# over ALL_GUEST_KERNELS
# rootfs_mode "ro" | "rw" default "ro"
# rootfs Path to a rootfs disk, composed from (composed)
# guest_kernel + rootfs_mode (Ubuntu for 5.10, AL2023 otherwise)
# pci_enabled True / False auto-multiplied
# vm_backend "kvm" auto-multiplied
# cpu_template None | static name | custom dict default None
# huge_pages HugePagesConfig default NONE
# vcpu_count int default 2
Expand All @@ -591,21 +644,21 @@ def huge_pages(request):

@pytest.fixture(params=ALL_GUEST_KERNELS)
def guest_kernel(request, record_property):
"""Path to the guest kernel artifact.
"""Logical guest kernel variant.

Default: parametrized over every supported kernel, so every test that
requests this fixture (directly or via `uvm` etc.) runs once per kernel.

Override with `@pin_guest_kernel(<Path or catalogue>)` (from
Override with `@pin_guest_kernel(<catalogue or pytest.param>)` (from
`framework.artifacts`) to restrict to one kernel or a smaller subset —
e.g. for tests of Firecracker functionality that don't depend on the
guest kernel, use `@pin_guest_kernel(GUEST_KERNEL_DEFAULT)`.
"""
kernel_path = request.param
if kernel_path is None:
kernel = request.param
if kernel is None:
pytest.fail(f"No kernel artifacts found in {ARTIFACT_DIR}")
record_property("guest_kernel", kernel_path.stem[2:])
return kernel_path
record_property("guest_kernel", kernel.metric_id)
return kernel


@pytest.fixture
Expand All @@ -623,7 +676,7 @@ def rootfs(guest_kernel, rootfs_mode):

Ubuntu for 5.10, AL2023 otherwise (AL2023 does not officially support 5.10).
"""
distro = "ubuntu" if guest_kernel.stem[2:] == "linux-5.10" else "amazonlinux"
distro = "ubuntu" if guest_kernel.version.startswith("5.10") else "amazonlinux"
suffix = {"ro": "squashfs", "rw": "ext4"}[rootfs_mode]
disk_list = disks(f"{distro}*.{suffix}")
if not disk_list:
Expand All @@ -632,10 +685,14 @@ def rootfs(guest_kernel, rootfs_mode):


@pytest.fixture
def uvm(microvm_factory, guest_kernel, rootfs, pci_enabled):
def uvm(microvm_factory, vm_backend, guest_kernel, rootfs, pci_enabled):
"""Built microVM (chroot only). Caller drives spawn/basic_config/start."""
vm = microvm_factory.build(guest_kernel, rootfs, pci=pci_enabled)
return vm
return microvm_factory.build(
guest_kernel,
rootfs,
pci=pci_enabled,
backend=vm_backend,
)


@pytest.fixture
Expand All @@ -646,9 +703,8 @@ def uvm_configured(uvm, vcpu_count, mem_size_mib, huge_pages, cpu_template):
vcpu_count=vcpu_count,
mem_size_mib=mem_size_mib,
huge_pages=huge_pages,
cpu_template=cpu_template,
)
if cpu_template is not None:
uvm.set_cpu_template(cpu_template)
return uvm


Expand All @@ -670,7 +726,7 @@ def uvm_restored(uvm_booted, microvm_factory):
return restored


@pytest.fixture(params=["booted", "restored"])
@pytest.fixture(params=tuple(VmLifecycle), ids=lambda lifecycle: lifecycle.value)
def uvm_lifecycle(request):
"""Parametrized over the two lifecycle end-states a test may want.

Expand All @@ -686,6 +742,7 @@ def uvm_lifecycle(request):
def uvm_any(
uvm_lifecycle,
request,
vm_backend,
guest_kernel,
rootfs,
pci_enabled,
Expand All @@ -706,4 +763,4 @@ def uvm_any(
those names would error with "function uses no fixture").
"""
# pylint: disable=unused-argument
return request.getfixturevalue(f"uvm_{uvm_lifecycle}")
return request.getfixturevalue(f"uvm_{uvm_lifecycle.value}")
Loading
Loading