Skip to content

Commit 014777a

Browse files
cgwaltersjeckersb
authored andcommitted
tests: Add integration test suite with bcvk VM support
Add a new integration-tests crate that exercises the cfsctl CLI as a subprocess using libtest-mimic with linkme distributed slices (same pattern as bcvk). Tests are split into two tiers: Unprivileged CLI tests run without root, using --insecure to skip fs-verity. Privileged tests create loopback ext4 filesystems with the verity feature and run cfsctl without --insecure; when not root, they automatically re-dispatch into a bcvk ephemeral VM. Includes a Containerfile producing a bootable centos-bootc:stream10 image with cfsctl and the test binary baked in, plus a GHA workflow using bootc-ubuntu-setup for CI. Assisted-by: OpenCode (Claude Opus) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 32a646f commit 014777a

12 files changed

Lines changed: 754 additions & 2 deletions

File tree

.config/nextest.toml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# cargo-nextest configuration for composefs-rs
2+
# https://nexte.st/book/configuration
3+
#
4+
# Integration tests use libtest-mimic which is fully compatible with nextest
5+
6+
[store]
7+
dir = "target/nextest"
8+
9+
[profile.default]
10+
test-threads = "num-cpus"
11+
slow-timeout = { period = "30s", terminate-after = 2 }
12+
fail-fast = false
13+
failure-output = "immediate"
14+
success-output = "never"
15+
status-level = "pass"
16+
17+
# Profile for integration tests — run with limited parallelism due to QEMU/KVM resources
18+
[profile.integration]
19+
test-threads = 2
20+
# 20 minutes for VM tests on slow CI runners
21+
slow-timeout = { period = "1200s", terminate-after = 60 }
22+
fail-fast = false
23+
failure-output = "immediate"
24+
success-output = "never"
25+
status-level = "pass"
26+
default-filter = "package(integration-tests)"
27+
28+
[profile.integration.junit]
29+
path = "junit.xml"
30+
store-success-output = true
31+
store-failure-output = true
32+
33+
# VM tests boot an ephemeral QEMU instance per test, limit parallelism
34+
[[profile.integration.overrides]]
35+
filter = 'test(~^vm_)'
36+
threads-required = 4

.containerignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Keep the build context small: exclude everything, then allowlist sources.
2+
*
3+
!Cargo.toml
4+
!Cargo.lock
5+
!crates/

.github/workflows/integration.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Integration Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
COMPOSEFS_TEST_IMAGE: localhost/composefs-rs-test:latest
12+
13+
concurrency:
14+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
15+
cancel-in-progress: true
16+
17+
permissions:
18+
contents: read
19+
20+
jobs:
21+
integration:
22+
name: Integration tests
23+
runs-on: ubuntu-24.04
24+
timeout-minutes: 30
25+
26+
steps:
27+
- uses: actions/checkout@v5
28+
29+
- name: Setup
30+
uses: bootc-dev/actions/bootc-ubuntu-setup@main
31+
with:
32+
libvirt: true
33+
34+
- uses: dtolnay/rust-toolchain@stable
35+
36+
- uses: Swatinem/rust-cache@v2
37+
38+
- name: Run unprivileged integration tests
39+
run: just integration-unprivileged
40+
41+
- name: Run privileged integration tests (via bcvk VM)
42+
run: just integration-container

Containerfile

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Containerfile for composefs-rs
2+
#
3+
# Builds cfsctl and integration test binaries, then produces a bootable
4+
# (bootc-compatible) container image suitable for privileged integration
5+
# testing via `bcvk ephemeral run-ssh`.
6+
#
7+
# Build:
8+
# podman build --tag composefs-rs-test -f Containerfile .
9+
#
10+
# Uses BuildKit-style cache mounts for fast incremental Rust builds.
11+
12+
# -- source snapshot (keeps layer graph clean) --
13+
FROM scratch AS src
14+
COPY . /src
15+
16+
# -- build stage --
17+
FROM quay.io/centos-bootc/centos-bootc:stream10 AS build
18+
19+
RUN dnf install -y \
20+
rust cargo clippy rustfmt \
21+
openssl-devel \
22+
gcc \
23+
composefs \
24+
&& dnf clean all
25+
26+
COPY --from=src /src /src
27+
WORKDIR /src
28+
29+
# Fetch dependencies (network-intensive, cached separately)
30+
RUN --mount=type=cache,target=/src/target \
31+
--mount=type=cache,target=/root/.cargo/registry \
32+
--mount=type=cache,target=/root/.cargo/git \
33+
cargo fetch
34+
35+
# Build cfsctl and integration test binary
36+
RUN --network=none \
37+
--mount=type=cache,target=/src/target \
38+
--mount=type=cache,target=/root/.cargo/registry \
39+
--mount=type=cache,target=/root/.cargo/git \
40+
cargo build --release -p cfsctl -p integration-tests && \
41+
cp /src/target/release/cfsctl /usr/bin/cfsctl && \
42+
cp /src/target/release/cfsctl-integration-tests /usr/bin/cfsctl-integration-tests
43+
44+
# -- final bootable image --
45+
FROM quay.io/centos-bootc/centos-bootc:stream10
46+
47+
RUN dnf install -y composefs openssl && dnf clean all
48+
49+
COPY --from=build /usr/bin/cfsctl /usr/bin/cfsctl
50+
COPY --from=build /usr/bin/cfsctl-integration-tests /usr/bin/cfsctl-integration-tests

Justfile

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ build:
1212
build-release:
1313
cargo build --workspace --release
1414

15-
# Run all tests
15+
# Run unit tests (excludes integration-tests crate)
1616
test:
17-
cargo test --workspace
17+
cargo test --workspace --exclude integration-tests
1818

1919
# Run clippy lints
2020
clippy:
@@ -31,6 +31,26 @@ fmt:
3131
# Run all checks (clippy + fmt + test)
3232
check: clippy fmt-check test
3333

34+
COMPOSEFS_TEST_IMAGE := "localhost/composefs-rs-test:latest"
35+
36+
# Run integration tests (builds cfsctl first); pass extra args to the harness
37+
test-integration *ARGS: build
38+
CFSCTL_PATH=$(pwd)/target/debug/cfsctl cargo run -p integration-tests -- {{ ARGS }}
39+
40+
# Run only the fast unprivileged integration tests (no root, no VM)
41+
integration-unprivileged: build
42+
CFSCTL_PATH=$(pwd)/target/debug/cfsctl cargo run -p integration-tests -- --skip privileged_
43+
44+
# Build the test container image for VM-based integration tests
45+
integration-container-build:
46+
podman build -t {{COMPOSEFS_TEST_IMAGE}} -f Containerfile .
47+
48+
# Run all integration tests; privileged tests dispatch to a bcvk VM
49+
integration-container: build integration-container-build
50+
COMPOSEFS_TEST_IMAGE={{COMPOSEFS_TEST_IMAGE}} \
51+
CFSCTL_PATH=$(pwd)/target/debug/cfsctl \
52+
cargo run -p integration-tests
53+
3454
# Clean build artifacts
3555
clean:
3656
cargo clean
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "integration-tests"
3+
version = "0.1.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[[bin]]
8+
name = "cfsctl-integration-tests"
9+
path = "src/main.rs"
10+
11+
[dependencies]
12+
anyhow = "1"
13+
xshell = "0.2"
14+
libtest-mimic = "0.8"
15+
linkme = "0.3"
16+
paste = "1"
17+
rustix = { version = "1.0.0", default-features = false, features = ["process"] }
18+
tempfile = "3"
19+
20+
[lints]
21+
workspace = true

crates/integration-tests/README.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# composefs-rs integration tests
2+
3+
Integration tests for `cfsctl` that exercise the CLI as a subprocess.
4+
Unlike the workspace's unit tests, these run the actual binary against
5+
real repositories and (optionally) real kernels with fs-verity.
6+
7+
The test binary uses `libtest-mimic` with `linkme` distributed slices
8+
instead of `#[test]`, following the same pattern as
9+
[bcvk](https://github.com/bootc-dev/bcvk).
10+
11+
## Test tiers
12+
13+
**CLI tests** (`test_*`) run on the host without root. They pass
14+
`--insecure` to skip fs-verity and use temp dirs for repositories.
15+
These are fast and need no special setup.
16+
17+
**Privileged tests** (`privileged_*`) need root and real fs-verity
18+
support. They create loopback ext4 filesystems with the verity feature
19+
and run `cfsctl` without `--insecure`. When run on the host (not as
20+
root), each test automatically re-executes itself inside a
21+
[bcvk](https://github.com/bootc-dev/bcvk) ephemeral VM — no separate
22+
wrapper functions needed. Set `COMPOSEFS_TEST_IMAGE` to enable this.
23+
24+
## Running
25+
26+
```sh
27+
# Fast CLI tests only (no root, no VM)
28+
just integration-unprivileged
29+
30+
# Everything, privileged tests auto-dispatch to bcvk VM
31+
just integration-container
32+
```
33+
34+
The container image is built from the repo's `Containerfile` and
35+
includes both `cfsctl` and `cfsctl-integration-tests` at `/usr/bin/`.
36+
It's based on `centos-bootc:stream10` so bcvk can boot it as a VM.
37+
38+
## Running privileged tests without bcvk
39+
40+
If you have root and a kernel with fs-verity support (5.4+), you can
41+
skip the VM dispatch entirely. The tests create loopback ext4
42+
filesystems with the verity feature, so you need `mkfs.ext4` and
43+
`mount` available.
44+
45+
**Direct execution as root** (e.g. inside a VM, a privileged
46+
container, or a test machine):
47+
48+
```sh
49+
cfsctl-integration-tests privileged_
50+
```
51+
52+
Or from the workspace:
53+
54+
```sh
55+
sudo CFSCTL_PATH=$(pwd)/target/debug/cfsctl cargo run -p integration-tests -- privileged_
56+
```
57+
58+
**Inside a privileged container** — useful on hosts without native
59+
fs-verity or when you don't want to install dependencies locally:
60+
61+
```sh
62+
podman run --privileged -v $(pwd):/src:Z -w /src \
63+
quay.io/fedora/fedora:41 \
64+
bash -c 'dnf -y install cargo composefs e2fsprogs && \
65+
cargo build -p cfsctl -p integration-tests && \
66+
CFSCTL_PATH=$(pwd)/target/debug/cfsctl \
67+
cargo run -p integration-tests -- privileged_'
68+
```
69+
70+
### Environment variables
71+
72+
| Variable | Purpose |
73+
|---|---|
74+
| `CFSCTL_PATH` | Path to `cfsctl` binary. Auto-detected if not set. |
75+
| `COMPOSEFS_TEST_IMAGE` | Container image for VM dispatch. Setting this triggers bcvk auto-dispatch for privileged tests. |
76+
| `BCVK_PATH` | Path to `bcvk` binary. Found in `PATH` if not set. |
77+
| `COMPOSEFS_IN_VM` | Set automatically inside VMs to prevent recursive dispatch. |
78+
79+
## Adding tests
80+
81+
Define a function returning `anyhow::Result<()>` and register it:
82+
83+
```rust
84+
fn test_my_feature() -> Result<()> {
85+
let sh = Shell::new()?;
86+
let cfsctl = cfsctl()?;
87+
// ...
88+
Ok(())
89+
}
90+
integration_test!(test_my_feature);
91+
```
92+
93+
For privileged tests, call `require_privileged()` at the top. This
94+
handles both execution paths — running directly as root, or
95+
re-dispatching into a VM:
96+
97+
```rust
98+
fn privileged_my_feature() -> Result<()> {
99+
if require_privileged("privileged_my_feature")?.is_some() {
100+
return Ok(());
101+
}
102+
// ... test body runs as root with fs-verity ...
103+
Ok(())
104+
}
105+
integration_test!(privileged_my_feature);
106+
```
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//! Integration test infrastructure for composefs-rs.
2+
//!
3+
//! Provides test registration via [`linkme`] distributed slices and the
4+
//! [`integration_test!`] macro, collected and executed by a custom
5+
//! [`libtest_mimic`] harness in `main.rs`.
6+
7+
// linkme requires unsafe for distributed slices
8+
#![allow(unsafe_code)]
9+
10+
/// A test function that returns a Result.
11+
pub type TestFn = fn() -> anyhow::Result<()>;
12+
13+
/// Metadata for a registered integration test.
14+
#[derive(Debug)]
15+
pub struct IntegrationTest {
16+
/// Name of the integration test.
17+
pub name: &'static str,
18+
/// Test function to execute.
19+
pub f: TestFn,
20+
}
21+
22+
impl IntegrationTest {
23+
/// Create a new integration test with the given name and function.
24+
pub const fn new(name: &'static str, f: TestFn) -> Self {
25+
Self { name, f }
26+
}
27+
}
28+
29+
/// Distributed slice holding all registered integration tests.
30+
#[linkme::distributed_slice]
31+
pub static INTEGRATION_TESTS: [IntegrationTest];
32+
33+
/// Register an integration test function with less boilerplate.
34+
///
35+
/// # Examples
36+
///
37+
/// ```ignore
38+
/// fn test_something() -> anyhow::Result<()> {
39+
/// Ok(())
40+
/// }
41+
/// integration_test!(test_something);
42+
/// ```
43+
#[macro_export]
44+
macro_rules! integration_test {
45+
($fn_name:ident) => {
46+
::paste::paste! {
47+
#[::linkme::distributed_slice($crate::INTEGRATION_TESTS)]
48+
static [<$fn_name:upper>]: $crate::IntegrationTest =
49+
$crate::IntegrationTest::new(stringify!($fn_name), $fn_name);
50+
}
51+
};
52+
}

0 commit comments

Comments
 (0)