From cf5df6496aec06eeee6915b3e1341c65260f956f Mon Sep 17 00:00:00 2001 From: "bootc-bot[bot]" <225049296+bootc-bot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:07:59 +0000 Subject: [PATCH 1/3] ephemeral: Add test-basic subcommand Adds `bcvk ephemeral test-basic` which boots an ephemeral VM and verifies that systemd reaches a healthy state via `systemctl is-system-running`. This provides a quick smoke test for bootc container images. The subcommand: - Boots an ephemeral VM from the specified container image - Waits for SSH to be ready - Runs systemctl is-system-running to check system health - Returns success if the system is in "running" or "degraded" state - Automatically cleans up the container on exit Integration tests are included to verify the functionality. Generated-by: AI --- crates/integration-tests/src/main.rs | 1 + .../src/tests/run_ephemeral_test_basic.rs | 103 +++++++++++++++++ crates/kit/src/ephemeral.rs | 109 ++++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 crates/integration-tests/src/tests/run_ephemeral_test_basic.rs diff --git a/crates/integration-tests/src/main.rs b/crates/integration-tests/src/main.rs index 296f1ce2a..9c90dc152 100644 --- a/crates/integration-tests/src/main.rs +++ b/crates/integration-tests/src/main.rs @@ -22,6 +22,7 @@ mod tests { pub mod run_ephemeral; pub mod run_ephemeral_ignition; pub mod run_ephemeral_ssh; + pub mod run_ephemeral_test_basic; pub mod to_disk; pub mod varlink; } diff --git a/crates/integration-tests/src/tests/run_ephemeral_test_basic.rs b/crates/integration-tests/src/tests/run_ephemeral_test_basic.rs new file mode 100644 index 000000000..087f26043 --- /dev/null +++ b/crates/integration-tests/src/tests/run_ephemeral_test_basic.rs @@ -0,0 +1,103 @@ +//! Integration tests for ephemeral test-basic command +//! +//! ⚠️ **CRITICAL INTEGRATION TEST POLICY** ⚠️ +//! +//! INTEGRATION TESTS MUST NEVER "warn and continue" ON FAILURES! +//! +//! If something is not working: +//! - Use `todo!("reason why this doesn't work yet")` +//! - Use `panic!("clear error message")` +//! - Use `assert!()` and `unwrap()` to fail hard +//! +//! NEVER use patterns like: +//! - "Note: test failed - likely due to..." +//! - "This is acceptable in CI/testing environments" +//! - Warning and continuing on failures + +use integration_tests::{integration_test, parameterized_integration_test}; +use itest::TestResult; +use xshell::cmd; + +use crate::{get_bck_command, get_test_image, shell}; + +/// Test the basic smoke test command +/// +/// This test verifies that `bcvk ephemeral test-basic` successfully boots +/// a bootc container image and verifies systemd reaches a healthy state. +fn test_ephemeral_test_basic() -> TestResult { + println!("Running test: bcvk ephemeral test-basic"); + + let sh = shell()?; + let bcvk = get_bck_command()?; + let image = get_test_image(); + + println!("Testing with image: {}", image); + + // Run the test-basic command + // This should boot the VM, check systemd health, and clean up + let output = cmd!(sh, "{bcvk} ephemeral test-basic {image}").output()?; + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + if !output.status.success() { + eprintln!("Command failed with exit code: {:?}", output.status.code()); + eprintln!("stdout: {}", stdout); + eprintln!("stderr: {}", stderr); + return Err(anyhow::anyhow!( + "test-basic command failed for image {}", + image + ) + .into()); + } + + // Verify expected output + assert!( + stdout.contains("System health check passed"), + "Expected success message not found in output. stdout: {}", + stdout + ); + + println!("Test passed: bcvk ephemeral test-basic"); + Ok(()) +} +integration_test!(test_ephemeral_test_basic); + +/// Parameterized test that runs test-basic against all configured test images +/// +/// Uses BCVK_ALL_IMAGES environment variable to get the list of images to test. +fn test_ephemeral_test_basic_parameterized(image: &str) -> TestResult { + println!("Running parameterized test: bcvk ephemeral test-basic (image: {})", image); + + let sh = shell()?; + let bcvk = get_bck_command()?; + + // Run the test-basic command + let output = cmd!(sh, "{bcvk} ephemeral test-basic {image}").output()?; + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + if !output.status.success() { + eprintln!("Command failed with exit code: {:?}", output.status.code()); + eprintln!("stdout: {}", stdout); + eprintln!("stderr: {}", stderr); + return Err(anyhow::anyhow!( + "test-basic command failed for image {}", + image + ) + .into()); + } + + // Verify expected output + assert!( + stdout.contains("System health check passed"), + "Expected success message not found in output for image {}. stdout: {}", + image, + stdout + ); + + println!("Parameterized test passed for image: {}", image); + Ok(()) +} +parameterized_integration_test!(test_ephemeral_test_basic_parameterized); diff --git a/crates/kit/src/ephemeral.rs b/crates/kit/src/ephemeral.rs index 0c4745cb3..f6886698a 100644 --- a/crates/kit/src/ephemeral.rs +++ b/crates/kit/src/ephemeral.rs @@ -39,6 +39,17 @@ pub struct SshOpts { pub args: Vec, } +/// Options for the test-basic subcommand +#[derive(clap::Parser, Debug)] +pub struct TestBasicOpts { + /// Container image to test + #[clap(help = "Container image to run basic smoke test on")] + pub image: String, + + #[clap(flatten)] + pub common: crate::run_ephemeral::CommonVmOpts, +} + /// Container list entry for ephemeral VMs #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] @@ -155,6 +166,15 @@ pub enum EphemeralCommands { #[clap(short, long)] force: bool, }, + + /// Run basic smoke test on a bootc container image + /// + /// Boots an ephemeral VM from the specified container image and verifies + /// that systemd reaches a healthy state via `systemctl is-system-running`. + /// This provides a quick sanity check to validate that a bootc container + /// image can successfully boot and reach a working state. + #[clap(name = "test-basic")] + TestBasic(TestBasicOpts), } impl EphemeralCommands { @@ -216,6 +236,7 @@ impl EphemeralCommands { Ok(()) } EphemeralCommands::RmAll { force } => remove_all_ephemeral_containers(force), + EphemeralCommands::TestBasic(opts) => test_basic(opts), } } } @@ -335,3 +356,91 @@ fn remove_all_ephemeral_containers(force: bool) -> Result<()> { Ok(()) } + +/// Run basic smoke test on a bootc container image +/// +/// Boots an ephemeral VM and verifies systemd reaches a healthy state +fn test_basic(opts: TestBasicOpts) -> Result<()> { + use crate::run_ephemeral::{run_detached, CommonPodmanOptions, RunEphemeralOpts}; + use std::process::Stdio; + + println!("Running basic smoke test on {}", opts.image); + + // Build ephemeral VM options + let ephemeral_opts = RunEphemeralOpts { + image: opts.image.clone(), + common: opts.common, + podman: CommonPodmanOptions { + detach: true, + ..Default::default() + }, + debug_entrypoint: None, + bind_mounts: Vec::new(), + mount_disk_files: Vec::new(), + }; + + // Start the ephemeral VM + let container_id = run_detached(ephemeral_opts)?; + println!("Started ephemeral VM: {}", container_id); + + // Ensure cleanup on any exit path + let _cleanup = ContainerCleanup { container_id: container_id.clone() }; + + // Wait for SSH to be ready + let progress_bar = crate::boot_progress::create_boot_progress_bar(); + let (duration, progress_bar) = run_ephemeral_ssh::wait_for_ssh_ready(&container_id, None, progress_bar)?; + progress_bar.finish_and_clear(); + println!("VM ready after {:.1}s", duration.as_secs_f64()); + + // Run systemctl is-system-running to check system health + println!("Checking system health..."); + let status = Command::new("podman") + .args([ + "exec", + "--", + &container_id, + "/var/lib/bcvk/entrypoint", + "ssh-exec", + "systemctl", + "is-system-running", + ]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .context("Failed to run systemctl is-system-running")?; + + let output = String::from_utf8_lossy(&status.stdout); + let state = output.trim(); + + // systemd is-system-running returns: + // - "running" or "degraded": system is operational + // - other states or non-zero exit: system has issues + let is_healthy = matches!(state, "running" | "degraded"); + + if is_healthy { + println!("✓ System health check passed (state: {})", state); + Ok(()) + } else { + Err(eyre!( + "System health check failed: systemctl is-system-running returned '{}' (exit code: {})", + state, + status.status.code().unwrap_or(-1) + )) + } +} + +/// RAII guard for ephemeral container cleanup +struct ContainerCleanup { + container_id: String, +} + +impl Drop for ContainerCleanup { + fn drop(&mut self) { + use std::process::Stdio; + let _ = Command::new("podman") + .args(["rm", "-f", "--", &self.container_id]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } +} From 2495305eef0f22d7fbac59aeb02f3c4620df2c21 Mon Sep 17 00:00:00 2001 From: "bootc-bot[bot]" <225049296+bootc-bot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:26:13 +0000 Subject: [PATCH 2/3] refactor: Deduplicate ContainerCleanup RAII guard Extract the ContainerCleanup struct from both run_ephemeral_ssh.rs and ephemeral.rs into a shared pub(crate) implementation in ephemeral.rs. This eliminates code duplication while using the better implementation that includes a new() constructor and debug logging. Both modules now import and use the shared ContainerCleanup via crate::ephemeral::ContainerCleanup. Assisted-by: AI --- crates/kit/src/ephemeral.rs | 45 +++++++++++++++++++---------- crates/kit/src/run_ephemeral_ssh.rs | 28 +----------------- 2 files changed, 30 insertions(+), 43 deletions(-) diff --git a/crates/kit/src/ephemeral.rs b/crates/kit/src/ephemeral.rs index f6886698a..0d2e2192e 100644 --- a/crates/kit/src/ephemeral.rs +++ b/crates/kit/src/ephemeral.rs @@ -19,6 +19,34 @@ use crate::ssh; /// Label used to identify bcvk ephemeral containers const EPHEMERAL_LABEL: &str = "bcvk.ephemeral=1"; +/// RAII guard for ephemeral container cleanup +/// Ensures container is removed when dropped, even on error paths +pub(crate) struct ContainerCleanup { + container_id: String, +} + +impl ContainerCleanup { + pub(crate) fn new(container_id: String) -> Self { + Self { container_id } + } +} + +impl Drop for ContainerCleanup { + fn drop(&mut self) { + use std::process::Stdio; + tracing::debug!("Cleaning up ephemeral container {}", self.container_id); + let result = Command::new("podman") + .args(["rm", "-f", "--", &self.container_id]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + + if let Err(e) = result { + tracing::warn!("Failed to remove container {}: {}", self.container_id, e); + } + } +} + /// SSH connection options for accessing running VMs. /// /// Provides secure shell access to VMs running within containers, @@ -384,7 +412,7 @@ fn test_basic(opts: TestBasicOpts) -> Result<()> { println!("Started ephemeral VM: {}", container_id); // Ensure cleanup on any exit path - let _cleanup = ContainerCleanup { container_id: container_id.clone() }; + let _cleanup = ContainerCleanup::new(container_id.clone()); // Wait for SSH to be ready let progress_bar = crate::boot_progress::create_boot_progress_bar(); @@ -429,18 +457,3 @@ fn test_basic(opts: TestBasicOpts) -> Result<()> { } } -/// RAII guard for ephemeral container cleanup -struct ContainerCleanup { - container_id: String, -} - -impl Drop for ContainerCleanup { - fn drop(&mut self) { - use std::process::Stdio; - let _ = Command::new("podman") - .args(["rm", "-f", "--", &self.container_id]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - } -} diff --git a/crates/kit/src/run_ephemeral_ssh.rs b/crates/kit/src/run_ephemeral_ssh.rs index 07c594d41..ece6597ab 100644 --- a/crates/kit/src/run_ephemeral_ssh.rs +++ b/crates/kit/src/run_ephemeral_ssh.rs @@ -6,6 +6,7 @@ use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; use tracing::debug; +use crate::ephemeral::ContainerCleanup; use crate::run_ephemeral::{run_detached, RunEphemeralOpts}; use crate::ssh; use crate::supervisor_status::{SupervisorState, SupervisorStatus}; @@ -94,33 +95,6 @@ fn show_container_logs(container_name: &str) { } } -/// RAII guard for ephemeral container cleanup -/// Ensures container is removed when dropped, even on error paths -struct ContainerCleanup { - container_id: String, -} - -impl ContainerCleanup { - fn new(container_id: String) -> Self { - Self { container_id } - } -} - -impl Drop for ContainerCleanup { - fn drop(&mut self) { - debug!("Cleaning up ephemeral container {}", self.container_id); - let result = Command::new("podman") - .args(["rm", "-f", "--", &self.container_id]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - - if let Err(e) = result { - tracing::warn!("Failed to remove container {}: {}", self.container_id, e); - } - } -} - /// Timeout waiting for connection pub(crate) const SSH_TIMEOUT: std::time::Duration = const { Duration::from_secs(240) }; From 3ff3ea7dbf4802bb53d0d11be45c361734c4beef Mon Sep 17 00:00:00 2001 From: "bootc-bot[bot]" <225049296+bootc-bot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:00:52 +0000 Subject: [PATCH 3/3] fix: Use systemctl status instead of is-system-running Change the health check from 'systemctl is-system-running' to 'systemctl status --no-pager' with output redirected to /dev/null, as the bootupd fix hasn't shipped yet and is-system-running fails. Also simplify the integration tests to use .run() instead of custom output capturing, as requested in review. Assisted-by: AI --- .../src/tests/run_ephemeral_test_basic.rs | 47 +------------------ crates/kit/src/ephemeral.rs | 30 +++++------- 2 files changed, 13 insertions(+), 64 deletions(-) diff --git a/crates/integration-tests/src/tests/run_ephemeral_test_basic.rs b/crates/integration-tests/src/tests/run_ephemeral_test_basic.rs index 087f26043..dd9d2a60b 100644 --- a/crates/integration-tests/src/tests/run_ephemeral_test_basic.rs +++ b/crates/integration-tests/src/tests/run_ephemeral_test_basic.rs @@ -35,28 +35,7 @@ fn test_ephemeral_test_basic() -> TestResult { // Run the test-basic command // This should boot the VM, check systemd health, and clean up - let output = cmd!(sh, "{bcvk} ephemeral test-basic {image}").output()?; - - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - - if !output.status.success() { - eprintln!("Command failed with exit code: {:?}", output.status.code()); - eprintln!("stdout: {}", stdout); - eprintln!("stderr: {}", stderr); - return Err(anyhow::anyhow!( - "test-basic command failed for image {}", - image - ) - .into()); - } - - // Verify expected output - assert!( - stdout.contains("System health check passed"), - "Expected success message not found in output. stdout: {}", - stdout - ); + cmd!(sh, "{bcvk} ephemeral test-basic {image}").run()?; println!("Test passed: bcvk ephemeral test-basic"); Ok(()) @@ -73,29 +52,7 @@ fn test_ephemeral_test_basic_parameterized(image: &str) -> TestResult { let bcvk = get_bck_command()?; // Run the test-basic command - let output = cmd!(sh, "{bcvk} ephemeral test-basic {image}").output()?; - - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - - if !output.status.success() { - eprintln!("Command failed with exit code: {:?}", output.status.code()); - eprintln!("stdout: {}", stdout); - eprintln!("stderr: {}", stderr); - return Err(anyhow::anyhow!( - "test-basic command failed for image {}", - image - ) - .into()); - } - - // Verify expected output - assert!( - stdout.contains("System health check passed"), - "Expected success message not found in output for image {}. stdout: {}", - image, - stdout - ); + cmd!(sh, "{bcvk} ephemeral test-basic {image}").run()?; println!("Parameterized test passed for image: {}", image); Ok(()) diff --git a/crates/kit/src/ephemeral.rs b/crates/kit/src/ephemeral.rs index 0d2e2192e..7f4f1c33f 100644 --- a/crates/kit/src/ephemeral.rs +++ b/crates/kit/src/ephemeral.rs @@ -420,7 +420,7 @@ fn test_basic(opts: TestBasicOpts) -> Result<()> { progress_bar.finish_and_clear(); println!("VM ready after {:.1}s", duration.as_secs_f64()); - // Run systemctl is-system-running to check system health + // Run systemctl status to check system health println!("Checking system health..."); let status = Command::new("podman") .args([ @@ -430,29 +430,21 @@ fn test_basic(opts: TestBasicOpts) -> Result<()> { "/var/lib/bcvk/entrypoint", "ssh-exec", "systemctl", - "is-system-running", + "status", + "--no-pager", ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .context("Failed to run systemctl is-system-running")?; + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .context("Failed to run systemctl status")?; - let output = String::from_utf8_lossy(&status.stdout); - let state = output.trim(); - - // systemd is-system-running returns: - // - "running" or "degraded": system is operational - // - other states or non-zero exit: system has issues - let is_healthy = matches!(state, "running" | "degraded"); - - if is_healthy { - println!("✓ System health check passed (state: {})", state); + if status.success() { + println!("✓ System health check passed"); Ok(()) } else { Err(eyre!( - "System health check failed: systemctl is-system-running returned '{}' (exit code: {})", - state, - status.status.code().unwrap_or(-1) + "System health check failed: systemctl status exited with code {}", + status.code().unwrap_or(-1) )) } }