new file mode 100755
@@ -0,0 +1,165 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Run meta-virtualization pytest test suites against the vcontainer
+# standalone SDK (vdkr/vpdmn) that was built by the previous bitbake
+# step.
+#
+# Arguments:
+# $1 - suite name: one of "vcontainer", "vdkr", "vpdmn"
+# $2 - bitbake build directory (${BUILDDIR})
+# $3 - path to the meta-virtualization layer
+#
+# Optional environment variables:
+# RESULTS_DIR - directory to copy pytest artefacts (junit xml / log) to
+# VCONTAINER_EXTRACT_DIR - where to extract the standalone SDK tarball
+# (default: ${builddir}/vcontainer-test-extracted)
+# TEST_OCI_IMAGE - path to an OCI image directory (enables vdkr/vpdmn
+# import tests)
+# VDKR_ARCH - target architecture for vdkr/vpdmn tests (default: x86_64)
+#
+# The script is intentionally conservative: any pytest tests that cannot run
+# in the CI environment (those marked "slow", "network", "boot") are skipped
+# are skipped so that the autobuilder step completes without needing network
+# access. Those can be re-enabled by exporting META_VIRT_PYTEST_MARKERS
+# before invocation.
+#
+# It is assumed that /dev/kvm is writable by the CI user running the tests,
+# since the performance is significantly faster with 'memres'.
+#
+
+set -e
+set -u
+set -o pipefail
+set -x
+
+if [ $# -lt 3 ]; then
+ echo "Usage: $0 <suite> <builddir> <meta-virtualization-dir>" >&2
+ echo " suite: vcontainer | vdkr | vpdmn" >&2
+ exit 2
+fi
+
+suite="$1"
+builddir=$(realpath "$2")
+metavirtdir=$(realpath "$3")
+
+if [ ! -d "$metavirtdir/tests" ]; then
+ echo "ERROR: meta-virtualization tests directory not found at $metavirtdir/tests" >&2
+ exit 1
+fi
+
+# Locate the vcontainer standalone SDK tarball. Prefer an externally-built
+# SDK passed via VCONTAINER_SDK (the autobuilder -tests jobs share the SDK
+# produced by the separate vcontainer-tarball builder), and fall back to
+# looking in the local build's deploy/sdk directory when running stand-alone.
+sdk_tarball=""
+if [ -n "${VCONTAINER_SDK:-}" ]; then
+ if [ -f "$VCONTAINER_SDK" ]; then
+ sdk_tarball="$VCONTAINER_SDK"
+ else
+ echo "ERROR: VCONTAINER_SDK=$VCONTAINER_SDK is set but not a file" >&2
+ exit 1
+ fi
+fi
+if [ -z "$sdk_tarball" ]; then
+ sdk_tarball="$builddir/tmp/deploy/sdk/vcontainer-standalone.sh"
+ if [ ! -f "$sdk_tarball" ]; then
+ # Try to find any matching tarball in case naming changed (e.g. versioned)
+ alt=$(ls -1 "$builddir"/tmp/deploy/sdk/vcontainer-*.sh 2>/dev/null | head -n1 || true)
+ if [ -n "$alt" ]; then
+ sdk_tarball="$alt"
+ else
+ echo "ERROR: vcontainer standalone SDK not found." >&2
+ echo " Set VCONTAINER_SDK to an existing SDK installer, or" >&2
+ echo " build vcontainer-tarball so $builddir/tmp/deploy/sdk/vcontainer-standalone.sh exists." >&2
+ exit 1
+ fi
+ fi
+fi
+
+extract_dir="${VCONTAINER_EXTRACT_DIR:-$builddir/vcontainer-test-extracted}"
+rm -rf "$extract_dir"
+mkdir -p "$(dirname "$extract_dir")"
+
+# Self-extracting installer (silent, -y agrees to license, -d picks dir)
+"$sdk_tarball" -d "$extract_dir" -y
+
+# Prepare a Python venv so we don't pollute the worker's system packages.
+python3 -m venv "$builddir/meta-virt-test-venv"
+# shellcheck disable=SC1091
+source "$builddir/meta-virt-test-venv/bin/activate"
+# Avoid warnings by upgrading pip; install pytest/pexpect into the venv via pip.
+python3 -m pip install --quiet --upgrade pip setuptools wheel
+python3 -m pip install --quiet --upgrade pytest pytest-timeout pexpect
+
+# Default marker filter excludes long running / infrastructure dependent tests.
+marker_filter="${META_VIRT_PYTEST_MARKERS:-not slow and not network and not boot and not incus and not k3s}"
+
+# Per-suite test file selection. Uses -k/-m for fine-grained filtering and
+# keeps the CLI small for logging clarity.
+case "$suite" in
+ vdkr)
+ test_files=(
+ "tests/test_vdkr.py"
+ "tests/test_vdkr_registry.py"
+ )
+ ;;
+ vpdmn)
+ test_files=(
+ "tests/test_vpdmn.py"
+ )
+ ;;
+ vcontainer)
+ # Broad vcontainer/bbclass/tooling coverage that doesn't require the
+ # vdkr/vpdmn CLI harness to be running.
+ test_files=(
+ "tests/test_container_cross_install.py"
+ "tests/test_container_registry_script.py"
+ "tests/test_vcontainer_auth_config.py"
+ "tests/test_multiarch_oci.py"
+ "tests/test_multilayer_oci.py"
+ )
+ ;;
+ *)
+ echo "ERROR: unknown suite '$suite' (expected vcontainer|vdkr|vpdmn)" >&2
+ exit 2
+ ;;
+esac
+
+pytest_args=(
+ -v
+ --tb=short
+ -m "$marker_filter"
+ --vdkr-dir "$extract_dir"
+ --junitxml="$builddir/pytest-$suite-results.xml"
+)
+
+# Allow tests that consume an OCI image (import/save/load) to find one.
+if [ -n "${TEST_OCI_IMAGE:-}" ] && [ -d "${TEST_OCI_IMAGE}" ]; then
+ pytest_args+=(--oci-image "$TEST_OCI_IMAGE")
+fi
+
+# Pass architecture through when set in the environment (default is x86_64).
+if [ -n "${VDKR_ARCH:-}" ]; then
+ pytest_args+=(--arch "$VDKR_ARCH")
+fi
+
+cd "$metavirtdir"
+# Don't let a single failing test kill the whole step - collect the junit
+# report, then surface the exit code via the junit file + exit status.
+set +e
+python3 -m pytest "${pytest_args[@]}" "${test_files[@]}"
+rc=$?
+set -e
+
+# Copy artefacts to the results dir if one was provided.
+if [ -n "${RESULTS_DIR:-}" ]; then
+ mkdir -p "$RESULTS_DIR"
+ cp -f "$builddir/pytest-$suite-results.xml" "$RESULTS_DIR/" 2>/dev/null || true
+ if [ -f /tmp/pytest-vcontainer.log ]; then
+ cp -f /tmp/pytest-vcontainer.log "$RESULTS_DIR/pytest-$suite.log" || true
+ fi
+fi
+
+exit $rc