Skip to content

Testing

Updated

Tests are separated by what they need to observe. A unit test that reads a struct, an integration test that needs a hypervisor, and an acceptance run on real hardware answer different questions, and the tooling keeps them apart so that a green local run never implies hardware was touched.

Terminal window
tools/preflight.sh

This is the local source gate: nineteen steps under set -euo pipefail, covering the frontend build, source hygiene, file-size limits, runtime-string language, the documentation and schema contract, the release and acceptance contract tests, dependency advisories and licenses, the secret-history scan, ESLint, formatting, cargo fmt, Clippy with -D warnings, and the whole Rust test and doctest suite. It requires cargo-deny on PATH.

Its first step builds the console, which is not housekeeping: the UI is compiled into the binary, so a stale or missing web/dist has to fail here rather than ship an agent with no interface.

Terminal window
cargo nextest run --profile ci --all-targets # or: cargo test --all-targets
cargo test --doc

tests/ holds the integration suites: lifecycle, persistence, supervision, storage planning, network contracts, session auth, snapshots, instance API, and the recovery paths across an agent restart. Where a shape must not drift, a test asserts against a recorded fixture in tests/fixtures/ instead of restating it in prose.

Tests that need a host, and are skipped by design

Section titled “Tests that need a host, and are skipped by design”

Every integration suite in tests/ that spawns a real cloud-hypervisor or qemu-storage-daemon process is marked #[ignore], so an ordinary run reports it as skipped rather than passed.

That marking exists because of a specific failure. These suites used to return early when their resources were missing, and the test framework recorded that as a pass. CI was green while nobody knew when a hardware-dependent behaviour had last been checked. Now they are invisible by default and, when you name them explicitly, they panic if a prerequisite is absent instead of pretending.

Three unit tests in src/ that drive a real hypervisor are not in that tier and still return early while CH_TEST_ASSETS_DIR is unset: vmm_ping_real_ch, spawn_ch_creates_socket and spawn_ch_runs_in_new_session pass without running when the assets are absent.

  1. Install the assets. Point CH_TEST_ASSETS_DIR at a directory containing:

    File Purpose
    hypervisor-fw Boot firmware, passed to Cloud Hypervisor as firmware_path
    dummy-disk.raw Throwaway disk fixture; each test copies it before use

    /usr/bin/cloud-hypervisor must also exist.

  2. Run the whole ignored set:

    Terminal window
    CH_TEST_ASSETS_DIR=/var/lib/virtainer-test \
    cargo nextest run --run-ignored all
  3. Or name a group:

    Terminal window
    # qemu-storage-daemon only: needs qemu-storage-daemon and qemu-img on PATH
    cargo nextest run --test qsd_integration --run-ignored all
    # online snapshot, through the manager, against a real qsd; also needs qemu-io
    cargo nextest run --test snapshot_manager_e2e --run-ignored all
    # the thaw guard's real systemd round trip; needs to create a system unit
    sudo cargo nextest run --test thaw_guard_integration --run-ignored all
Terminal window
cd web && pnpm test:e2e:mocked # PW_MOCKED=1, no agent required
cd web && pnpm test:e2e:live # PW_LIVE=1, drives a running agent

The mocked suite runs Chromium, Firefox and WebKit, in light and dark, at a narrow viewport, with axe accessibility checks. It is part of release acceptance, which is why it is invoked locally against the release environment rather than on a hosted runner.

The scripts that build and ship the product are themselves under test, in tools/: test-host-image-build-contract.sh pins what the image bake must refuse and must stage, and siblings pin the release artifact set, the acceptance contract, the AppVM acceptance harness, the notification path and the secret scanner. They run inside preflight with the container tooling stubbed, so they assert on the shape of a build without performing one.

The evidence is checked later, and by a different script than the one that ships it: tools/check-release-evidence-bundle.sh opens the acceptance archive, hashes it against the receipt, re-hashes every file the receipt references, and rejects a missing or unreferenced member. tools/release.sh --promote runs tools/check-release-acceptance.sh, which re-derives the source digest of the tree it is about to release and requires a passing result for every name the receipt has to carry.

.github/workflows/ci.yml is a manual, workflow_dispatch workflow. Every gate it runs also runs in preflight, so it is a backstop and not the enforcement point. It installs no container tooling and builds no host image. The full-history secret scan is no longer the exception to that: tools/check-secret-history.sh runs the same pinned Gitleaks 8.29.1 across the whole history, in preflight, so no gate here is the only place it runs.