Skip to content

Commit 9ac079b

Browse files
committed
tests: Add parameterized integration test infrastructure
This introduces a matrix-based testing approach for integration tests that need to run against multiple container images. Instead of hardcoding specific images or manually creating variants for each distribution, tests can now register as parameterized tests and automatically run once per image in BCVK_ALL_IMAGES. This enables systematic cross-distribution testing without code duplication and makes it easy to add or change the test matrix by updating environment variables rather than modifying test code. Assisted-by: Claude Code (Sonnet 4.5) Signed-off-by: Colin Walters <[email protected]>
1 parent 6b3aa05 commit 9ac079b

File tree

5 files changed

+177
-62
lines changed

5 files changed

+177
-62
lines changed

Justfile

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
PRIMARY_IMAGE := "quay.io/centos-bootc/centos-bootc:stream10"
2+
ALL_BASE_IMAGES := "quay.io/fedora/fedora-bootc:42 quay.io/centos-bootc/centos-bootc:stream9 quay.io/centos-bootc/centos-bootc:stream10"
3+
14
# Build the native binary
25
build:
36
make
@@ -17,17 +20,20 @@ unit *ARGS:
1720
fi
1821

1922
pull-test-images:
20-
podman pull -q quay.io/fedora/fedora-bootc:42 quay.io/centos-bootc/centos-bootc:stream9 quay.io/centos-bootc/centos-bootc:stream10 >/dev/null
23+
podman pull -q {{ALL_BASE_IMAGES}} >/dev/null
2124

2225
# Run integration tests (auto-detects nextest, with cleanup)
2326
test-integration *ARGS: build pull-test-images
2427
#!/usr/bin/env bash
2528
set -euo pipefail
2629
export BCVK_PATH=$(pwd)/target/release/bcvk
27-
30+
export BCVK_PRIMARY_IMAGE={{ PRIMARY_IMAGE }}
31+
# Note: BCVK_ALL_IMAGES is quoted to preserve the space-separated list
32+
export BCVK_ALL_IMAGES="{{ ALL_BASE_IMAGES }}"
33+
2834
# Clean up any leftover containers before starting
2935
cargo run --release --bin test-cleanup -p integration-tests 2>/dev/null || true
30-
36+
3137
# Run the tests
3238
if command -v cargo-nextest &> /dev/null; then
3339
cargo nextest run --release -P integration -p integration-tests {{ ARGS }}
@@ -36,10 +42,10 @@ test-integration *ARGS: build pull-test-images
3642
cargo test --release -p integration-tests -- {{ ARGS }}
3743
TEST_EXIT_CODE=$?
3844
fi
39-
45+
4046
# Clean up containers after tests complete
4147
cargo run --release --bin test-cleanup -p integration-tests 2>/dev/null || true
42-
48+
4349
exit $TEST_EXIT_CODE
4450

4551
# Clean up integration test containers

crates/integration-tests/src/lib.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ pub const LIBVIRT_INTEGRATION_TEST_LABEL: &str = "bcvk-integration";
1717
/// A test function that returns a Result
1818
pub type TestFn = fn() -> color_eyre::Result<()>;
1919

20+
/// A parameterized test function that takes an image parameter
21+
pub type ParameterizedTestFn = fn(&str) -> color_eyre::Result<()>;
22+
2023
/// Metadata for a registered integration test
2124
#[derive(Debug)]
2225
pub struct IntegrationTest {
@@ -33,6 +36,92 @@ impl IntegrationTest {
3336
}
3437
}
3538

39+
/// Metadata for a parameterized integration test that runs once per image
40+
#[derive(Debug)]
41+
pub struct ParameterizedIntegrationTest {
42+
/// Base name of the integration test (will be suffixed with image identifier)
43+
pub name: &'static str,
44+
/// Parameterized test function to execute
45+
pub f: ParameterizedTestFn,
46+
}
47+
48+
impl ParameterizedIntegrationTest {
49+
/// Create a new parameterized integration test with the given name and function
50+
pub const fn new(name: &'static str, f: ParameterizedTestFn) -> Self {
51+
Self { name, f }
52+
}
53+
}
54+
3655
/// Distributed slice holding all registered integration tests
3756
#[distributed_slice]
3857
pub static INTEGRATION_TESTS: [IntegrationTest];
58+
59+
/// Distributed slice holding all registered parameterized integration tests
60+
#[distributed_slice]
61+
pub static PARAMETERIZED_INTEGRATION_TESTS: [ParameterizedIntegrationTest];
62+
63+
/// Create a test suffix from an image name by replacing invalid characters with underscores
64+
///
65+
/// Replaces all non-alphanumeric characters with `_` to create a predictable, filesystem-safe
66+
/// test name suffix.
67+
///
68+
/// Examples:
69+
/// - "quay.io/fedora/fedora-bootc:42" -> "quay_io_fedora_fedora_bootc_42"
70+
/// - "quay.io/centos-bootc/centos-bootc:stream10" -> "quay_io_centos_bootc_centos_bootc_stream10"
71+
/// - "quay.io/image@sha256:abc123" -> "quay_io_image_sha256_abc123"
72+
pub fn image_to_test_suffix(image: &str) -> String {
73+
image
74+
.chars()
75+
.map(|c| if c.is_alphanumeric() { c } else { '_' })
76+
.collect()
77+
}
78+
79+
#[cfg(test)]
80+
mod tests {
81+
use super::*;
82+
83+
#[test]
84+
fn test_image_to_test_suffix_basic() {
85+
assert_eq!(
86+
image_to_test_suffix("quay.io/fedora/fedora-bootc:42"),
87+
"quay_io_fedora_fedora_bootc_42"
88+
);
89+
}
90+
91+
#[test]
92+
fn test_image_to_test_suffix_stream() {
93+
assert_eq!(
94+
image_to_test_suffix("quay.io/centos-bootc/centos-bootc:stream10"),
95+
"quay_io_centos_bootc_centos_bootc_stream10"
96+
);
97+
}
98+
99+
#[test]
100+
fn test_image_to_test_suffix_digest() {
101+
assert_eq!(
102+
image_to_test_suffix("quay.io/image@sha256:abc123"),
103+
"quay_io_image_sha256_abc123"
104+
);
105+
}
106+
107+
#[test]
108+
fn test_image_to_test_suffix_complex() {
109+
assert_eq!(
110+
image_to_test_suffix("registry.example.com:5000/my-org/my-image:v1.2.3"),
111+
"registry_example_com_5000_my_org_my_image_v1_2_3"
112+
);
113+
}
114+
115+
#[test]
116+
fn test_image_to_test_suffix_only_alphanumeric() {
117+
assert_eq!(image_to_test_suffix("simpleimage"), "simpleimage");
118+
}
119+
120+
#[test]
121+
fn test_image_to_test_suffix_special_chars() {
122+
assert_eq!(
123+
image_to_test_suffix("image/with@special:chars-here.now"),
124+
"image_with_special_chars_here_now"
125+
);
126+
}
127+
}

crates/integration-tests/src/main.rs

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ use xshell::{cmd, Shell};
1111

1212
// Re-export constants from lib for internal use
1313
pub(crate) use integration_tests::{
14-
IntegrationTest, INTEGRATION_TESTS, INTEGRATION_TEST_LABEL, LIBVIRT_INTEGRATION_TEST_LABEL,
14+
image_to_test_suffix, IntegrationTest, ParameterizedIntegrationTest, INTEGRATION_TESTS,
15+
INTEGRATION_TEST_LABEL, LIBVIRT_INTEGRATION_TEST_LABEL, PARAMETERIZED_INTEGRATION_TESTS,
1516
};
1617
use linkme::distributed_slice;
1718

@@ -43,30 +44,39 @@ pub(crate) fn get_bck_command() -> Result<String> {
4344
return Ok("bcvk".to_owned());
4445
}
4546

46-
/// Get the default bootc image to use for tests
47+
/// Get the primary bootc image to use for tests
4748
///
48-
/// Checks BCVK_TEST_IMAGE environment variable first, then falls back to default.
49-
/// This allows easily overriding the base image for all integration tests.
50-
///
51-
/// Default images:
52-
/// - Primary: quay.io/fedora/fedora-bootc:42 (Fedora 42 with latest features)
53-
/// - Alternative: quay.io/centos-bootc/centos-bootc:stream9 (CentOS Stream 9 for compatibility testing)
49+
/// Checks BCVK_PRIMARY_IMAGE environment variable first, then falls back to BCVK_TEST_IMAGE
50+
/// for backwards compatibility, then to a hardcoded default.
5451
pub(crate) fn get_test_image() -> String {
55-
std::env::var("BCVK_TEST_IMAGE")
56-
.unwrap_or_else(|_| "quay.io/fedora/fedora-bootc:42".to_string())
52+
std::env::var("BCVK_PRIMARY_IMAGE")
53+
.or_else(|_| std::env::var("BCVK_TEST_IMAGE"))
54+
.unwrap_or_else(|_| "quay.io/centos-bootc/centos-bootc:stream10".to_string())
5755
}
5856

59-
/// Get an alternative bootc image for cross-platform testing
57+
/// Get all test images for matrix testing
58+
///
59+
/// Parses BCVK_ALL_IMAGES environment variable, which should be a whitespace-separated
60+
/// list of container images (spaces, tabs, and newlines are all acceptable separators).
61+
/// Falls back to a single-element vec containing the primary image if not set or empty.
6062
///
61-
/// Returns a different image from the primary test image to test compatibility.
62-
/// If BCVK_TEST_IMAGE is set to Fedora, returns CentOS Stream 9.
63-
/// If BCVK_TEST_IMAGE is set to CentOS, returns Fedora.
64-
pub(crate) fn get_alternative_test_image() -> String {
65-
let primary = get_test_image();
66-
if primary.contains("centos") {
67-
"quay.io/fedora/fedora-bootc:42".to_string()
63+
/// Example: `export BCVK_ALL_IMAGES="quay.io/fedora/fedora-bootc:42 quay.io/centos-bootc/centos-bootc:stream9"`
64+
pub(crate) fn get_all_test_images() -> Vec<String> {
65+
if let Ok(all_images) = std::env::var("BCVK_ALL_IMAGES") {
66+
let images: Vec<String> = all_images
67+
.split_whitespace()
68+
.filter(|s| !s.is_empty())
69+
.map(|s| s.to_string())
70+
.collect();
71+
72+
if images.is_empty() {
73+
eprintln!("Warning: BCVK_ALL_IMAGES is set but empty, falling back to primary image");
74+
vec![get_test_image()]
75+
} else {
76+
images
77+
}
6878
} else {
69-
"quay.io/centos-bootc/centos-bootc:stream9".to_string()
79+
vec![get_test_image()]
7080
}
7181
}
7282

@@ -183,15 +193,29 @@ fn test_images_list() -> Result<()> {
183193
fn main() {
184194
let args = Arguments::from_args();
185195

186-
// Collect tests from the distributed slice
187-
let tests: Vec<Trial> = INTEGRATION_TESTS
188-
.iter()
189-
.map(|test| {
190-
let name = test.name;
191-
let f = test.f;
192-
Trial::test(name, move || f().map_err(|e| format!("{:?}", e).into()))
193-
})
194-
.collect();
196+
let mut tests: Vec<Trial> = Vec::new();
197+
198+
// Collect regular tests from the distributed slice
199+
tests.extend(INTEGRATION_TESTS.iter().map(|test| {
200+
let name = test.name;
201+
let f = test.f;
202+
Trial::test(name, move || f().map_err(|e| format!("{:?}", e).into()))
203+
}));
204+
205+
// Collect parameterized tests and generate variants for each image
206+
let all_images = get_all_test_images();
207+
for param_test in PARAMETERIZED_INTEGRATION_TESTS.iter() {
208+
for image in &all_images {
209+
let image = image.clone();
210+
let test_suffix = image_to_test_suffix(&image);
211+
let test_name = format!("{}_{}", param_test.name, test_suffix);
212+
let f = param_test.f;
213+
214+
tests.push(Trial::test(test_name, move || {
215+
f(&image).map_err(|e| format!("{:?}", e).into())
216+
}));
217+
}
218+
}
195219

196220
// Run the tests and exit with the result
197221
libtest_mimic::run(&args, tests).exit();

crates/integration-tests/src/tests/run_ephemeral_ssh.rs

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ use std::thread;
2121
use std::time::Duration;
2222

2323
use crate::{
24-
get_alternative_test_image, get_test_image, run_bcvk, IntegrationTest, INTEGRATION_TESTS,
25-
INTEGRATION_TEST_LABEL,
24+
get_test_image, run_bcvk, IntegrationTest, ParameterizedIntegrationTest, INTEGRATION_TESTS,
25+
INTEGRATION_TEST_LABEL, PARAMETERIZED_INTEGRATION_TESTS,
2626
};
2727

2828
#[distributed_slice(INTEGRATION_TESTS)]
@@ -144,22 +144,18 @@ fn test_run_ephemeral_ssh_exit_code() -> Result<()> {
144144
Ok(())
145145
}
146146

147-
#[distributed_slice(INTEGRATION_TESTS)]
148-
static TEST_RUN_EPHEMERAL_SSH_CROSS_DISTRO_COMPATIBILITY: IntegrationTest = IntegrationTest::new(
149-
"run_ephemeral_ssh_cross_distro_compatibility",
150-
test_run_ephemeral_ssh_cross_distro_compatibility,
151-
);
152-
153-
/// Test SSH functionality across different bootc images (Fedora and CentOS)
154-
/// This test verifies that our systemd version compatibility fix works correctly
155-
/// with both newer systemd (Fedora) and older systemd (CentOS Stream 9)
156-
fn test_run_ephemeral_ssh_cross_distro_compatibility() -> Result<()> {
157-
test_ssh_with_image(&get_test_image(), "primary")?;
158-
test_ssh_with_image(&get_alternative_test_image(), "alternative")?;
159-
Ok(())
160-
}
147+
#[distributed_slice(PARAMETERIZED_INTEGRATION_TESTS)]
148+
static TEST_RUN_EPHEMERAL_SSH_CROSS_DISTRO_COMPATIBILITY: ParameterizedIntegrationTest =
149+
ParameterizedIntegrationTest::new(
150+
"run_ephemeral_ssh_cross_distro_compatibility",
151+
test_run_ephemeral_ssh_cross_distro_compatibility,
152+
);
161153

162-
fn test_ssh_with_image(image: &str, image_type: &str) -> Result<()> {
154+
/// Test SSH functionality across different bootc images
155+
/// This parameterized test runs once per image in BCVK_ALL_IMAGES and verifies
156+
/// that our systemd version compatibility fix works correctly with both newer
157+
/// systemd (Fedora) and older systemd (CentOS Stream 9)
158+
fn test_run_ephemeral_ssh_cross_distro_compatibility(image: &str) -> Result<()> {
163159
let output = run_bcvk(&[
164160
"ephemeral",
165161
"run-ssh",
@@ -173,34 +169,34 @@ fn test_ssh_with_image(image: &str, image_type: &str) -> Result<()> {
173169

174170
assert!(
175171
output.success(),
176-
"{} image SSH test failed: {}",
177-
image_type,
172+
"SSH test failed for image {}: {}",
173+
image,
178174
output.stderr
179175
);
180176

181177
assert!(
182178
output.stdout.contains("systemd"),
183-
"{} image: systemd version not found. Got: {}",
184-
image_type,
179+
"systemd version not found for image {}. Got: {}",
180+
image,
185181
output.stdout
186182
);
187183

188184
// Log systemd version for diagnostic purposes
189185
if let Some(version_line) = output.stdout.lines().next() {
190-
eprintln!("{} image systemd version: {}", image_type, version_line);
186+
eprintln!("Image {} systemd version: {}", image, version_line);
191187

192188
let version_parts: Vec<&str> = version_line.split_whitespace().collect();
193189
if version_parts.len() >= 2 {
194190
if let Ok(version_num) = version_parts[1].parse::<u32>() {
195191
if version_num >= 254 {
196192
eprintln!(
197193
"✓ {} supports vmm.notify_socket (version {})",
198-
image_type, version_num
194+
image, version_num
199195
);
200196
} else {
201197
eprintln!(
202198
"✓ {} falls back to SSH polling (version {} < 254)",
203-
image_type, version_num
199+
image, version_num
204200
);
205201
}
206202
}

crates/integration-tests/src/tests/to_disk.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use linkme::distributed_slice;
2020
use std::process::Command;
2121
use tempfile::TempDir;
2222

23-
use crate::{run_bcvk, IntegrationTest, INTEGRATION_TESTS, INTEGRATION_TEST_LABEL};
23+
use crate::{get_test_image, run_bcvk, IntegrationTest, INTEGRATION_TESTS, INTEGRATION_TEST_LABEL};
2424

2525
#[distributed_slice(INTEGRATION_TESTS)]
2626
static TEST_TO_DISK: IntegrationTest = IntegrationTest::new("to_disk", test_to_disk);
@@ -35,7 +35,7 @@ fn test_to_disk() -> Result<()> {
3535
"to-disk",
3636
"--label",
3737
INTEGRATION_TEST_LABEL,
38-
"quay.io/centos-bootc/centos-bootc:stream10",
38+
&get_test_image(),
3939
disk_path.as_str(),
4040
])?;
4141

@@ -104,7 +104,7 @@ fn test_to_disk_qcow2() -> Result<()> {
104104
"--format=qcow2",
105105
"--label",
106106
INTEGRATION_TEST_LABEL,
107-
"quay.io/centos-bootc/centos-bootc:stream10",
107+
&get_test_image(),
108108
disk_path.as_str(),
109109
])?;
110110

@@ -162,7 +162,7 @@ fn test_to_disk_caching() -> Result<()> {
162162
"to-disk",
163163
"--label",
164164
INTEGRATION_TEST_LABEL,
165-
"quay.io/centos-bootc/centos-bootc:stream10",
165+
&get_test_image(),
166166
disk_path.as_str(),
167167
])?;
168168

@@ -189,7 +189,7 @@ fn test_to_disk_caching() -> Result<()> {
189189
"to-disk",
190190
"--label",
191191
INTEGRATION_TEST_LABEL,
192-
"quay.io/centos-bootc/centos-bootc:stream10",
192+
&get_test_image(),
193193
disk_path.as_str(),
194194
])?;
195195

0 commit comments

Comments
 (0)