test: add --mode vm, the autopkgtest escape hatch

VM virtualization is the one backend pkh does not reimplement: the qemu
runner carries the serial-console protocol, image overlay management
and reboot support, and it is the only one satisfying
isolation-machine/needs-reboot tests. pkh execs the installed
autopkgtest with its qemu runner (--image required, --cpus/--ram-size
passthrough) in its own process group so Ctrl+C reaches the testbed
cleanup, parses the --summary file back into the pkh report model and
maps autopkgtest's exit classes onto the 0/1/2 contract (12/14/16/20
are runtime errors; the raw code rides the JSON report). Pockets, PPAs
and injection are refused in vm mode for now: they would need the
keyring setup inside the image.
This commit is contained in:
2026-09-27 21:33:11 +02:00
parent 15cc19007f
commit 7790b9452d
5 changed files with 334 additions and 16 deletions
+62 -13
View File
@@ -17,6 +17,7 @@ pub mod expand;
pub mod options;
pub mod outcome;
pub(crate) mod runner;
pub(crate) mod vm;
pub use options::{TestMode, TestOptions};
pub use outcome::TestRunReport;
@@ -117,6 +118,38 @@ pub async fn run(opts: TestOptions<'_>) -> Result<TestRunReport, Box<dyn std::er
});
let mode = opts.mode.unwrap_or_default();
// The vm escape hatch goes through the installed autopkgtest; it
// owns the artifacts directory (passed as -o, which must not exist
// yet), so the path is only computed, not created.
if mode == TestMode::Vm {
let run_dir = run_dir_path(&package, &series, &arch);
let (outcomes, raw_code) = vm::run_vm(
&tree,
&binaries.paths,
&tests_control.tests,
&run_dir,
opts.image.as_deref(),
&opts,
view,
)?;
return finish(
TestRunReport {
package,
version,
series,
arch,
mode: mode.label().to_string(),
binaries: binaries.paths,
binaries_note: binaries.note,
outcomes,
artifacts_dir: run_dir,
raw_exit_code: Some(raw_code),
},
view,
);
}
if mode == TestMode::Local {
log::warn!(
"Tests run directly on the current context: they install packages \
@@ -217,22 +250,32 @@ pub async fn run(opts: TestOptions<'_>) -> Result<TestRunReport, Box<dyn std::er
drop_shell(&bed, &staged_tree, &env, view)?;
}
let report = TestRunReport {
package,
version,
series,
arch,
mode: mode.label().to_string(),
binaries: binaries.paths,
binaries_note: binaries.note,
outcomes,
artifacts_dir: run_dir.clone(),
raw_exit_code: None,
};
finish(
TestRunReport {
package,
version,
series,
arch,
mode: mode.label().to_string(),
binaries: binaries.paths,
binaries_note: binaries.note,
outcomes,
artifacts_dir: run_dir,
raw_exit_code: None,
},
view,
)
}
/// Announce the outcome on the view and hand the report back.
fn finish(
report: TestRunReport,
view: &dyn BuildView,
) -> Result<TestRunReport, Box<dyn std::error::Error>> {
if report.has_failures() {
view.finish_failure();
} else {
view.finish_success(&[run_dir]);
view.finish_success(std::slice::from_ref(&report.artifacts_dir));
}
Ok(report)
}
@@ -535,6 +578,12 @@ fn create_run_dir(package: &str, series: &str, arch: &str) -> std::io::Result<Pa
Ok(dir)
}
/// The run directory path for an identity, without creating it.
fn run_dir_path(package: &str, series: &str, arch: &str) -> PathBuf {
let timestamp = chrono::Utc::now().format("%Y%m%dT%H%M%S");
runs_root().join(format!("{package}-{series}-{arch}-{timestamp}"))
}
/// The test-run artifacts root: `$PKH_TESTS_DIR`, defaulting to
/// `/var/tmp/pkh/tests`.
pub fn runs_root() -> PathBuf {