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:
+11
-1
@@ -391,7 +391,10 @@ fn main() {
|
|||||||
.arg(arg!(-p --pocket <pocket> "Resolve test dependencies from a specific distribution pocket (updates, security, proposed)").required(false))
|
.arg(arg!(-p --pocket <pocket> "Resolve test dependencies from a specific distribution pocket (updates, security, proposed)").required(false))
|
||||||
.arg(arg!(--ppa <ppa> "Add a PPA as test dependency source (can be specified multiple times)").required(false).action(clap::ArgAction::Append))
|
.arg(arg!(--ppa <ppa> "Add a PPA as test dependency source (can be specified multiple times)").required(false).action(clap::ArgAction::Append))
|
||||||
.arg(arg!(--inject <package> "Inject a package into the testbed before the tests (.deb path or package name, can be specified multiple times)").required(false).action(clap::ArgAction::Append))
|
.arg(arg!(--inject <package> "Inject a package into the testbed before the tests (.deb path or package name, can be specified multiple times)").required(false).action(clap::ArgAction::Append))
|
||||||
.arg(arg!(--mode <mode> "Testbed mode: chroot (default, an ephemeral unshare chroot) or local (the current context, no isolation)").required(false))
|
.arg(arg!(--mode <mode> "Testbed mode: chroot (default, an ephemeral unshare chroot), local (the current context, no isolation) or vm (a qemu VM through autopkgtest)").required(false))
|
||||||
|
.arg(arg!(--image <file> "VM image for --mode vm (required there)").required(false))
|
||||||
|
.arg(arg!(--cpus <n> "VM virtual CPUs (vm mode)").required(false).value_parser(clap::value_parser!(u32)))
|
||||||
|
.arg(arg!(--"ram-size" <mib> "VM memory size in MiB (vm mode)").required(false).value_parser(clap::value_parser!(u32)))
|
||||||
.arg(arg!(--debs <deb> "Explicit binary package to test, overriding the pkh deb output reuse (can be specified multiple times)").required(false).action(clap::ArgAction::Append))
|
.arg(arg!(--debs <deb> "Explicit binary package to test, overriding the pkh deb output reuse (can be specified multiple times)").required(false).action(clap::ArgAction::Append))
|
||||||
.arg(
|
.arg(
|
||||||
clap::Arg::new("no_build")
|
clap::Arg::new("no_build")
|
||||||
@@ -1062,6 +1065,13 @@ fn main() {
|
|||||||
.map(|v| v.cloned().collect())
|
.map(|v| v.cloned().collect())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
mode,
|
mode,
|
||||||
|
image: sub_matches
|
||||||
|
.get_one::<String>("image")
|
||||||
|
.map(std::path::PathBuf::from),
|
||||||
|
cpus: sub_matches.get_one::<u32>("cpus").copied(),
|
||||||
|
ram_size: sub_matches
|
||||||
|
.get_one::<u32>("ram-size")
|
||||||
|
.map(|v| v.to_string()),
|
||||||
debs: sub_matches
|
debs: sub_matches
|
||||||
.get_many::<String>("debs")
|
.get_many::<String>("debs")
|
||||||
.map(|v| v.map(std::path::PathBuf::from).collect())
|
.map(|v| v.map(std::path::PathBuf::from).collect())
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ impl Bed {
|
|||||||
Ok(Bed::Chroot(guard))
|
Ok(Bed::Chroot(guard))
|
||||||
}
|
}
|
||||||
TestMode::Local => Ok(Bed::Local(base_ctx)),
|
TestMode::Local => Ok(Bed::Local(base_ctx)),
|
||||||
|
// The vm mode never builds a Bed: run_vm execs autopkgtest.
|
||||||
|
TestMode::Vm => unreachable!("vm mode does not prepare a Bed"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+62
-13
@@ -17,6 +17,7 @@ pub mod expand;
|
|||||||
pub mod options;
|
pub mod options;
|
||||||
pub mod outcome;
|
pub mod outcome;
|
||||||
pub(crate) mod runner;
|
pub(crate) mod runner;
|
||||||
|
pub(crate) mod vm;
|
||||||
|
|
||||||
pub use options::{TestMode, TestOptions};
|
pub use options::{TestMode, TestOptions};
|
||||||
pub use outcome::TestRunReport;
|
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();
|
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 {
|
if mode == TestMode::Local {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Tests run directly on the current context: they install packages \
|
"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)?;
|
drop_shell(&bed, &staged_tree, &env, view)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let report = TestRunReport {
|
finish(
|
||||||
package,
|
TestRunReport {
|
||||||
version,
|
package,
|
||||||
series,
|
version,
|
||||||
arch,
|
series,
|
||||||
mode: mode.label().to_string(),
|
arch,
|
||||||
binaries: binaries.paths,
|
mode: mode.label().to_string(),
|
||||||
binaries_note: binaries.note,
|
binaries: binaries.paths,
|
||||||
outcomes,
|
binaries_note: binaries.note,
|
||||||
artifacts_dir: run_dir.clone(),
|
outcomes,
|
||||||
raw_exit_code: None,
|
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() {
|
if report.has_failures() {
|
||||||
view.finish_failure();
|
view.finish_failure();
|
||||||
} else {
|
} else {
|
||||||
view.finish_success(&[run_dir]);
|
view.finish_success(std::slice::from_ref(&report.artifacts_dir));
|
||||||
}
|
}
|
||||||
Ok(report)
|
Ok(report)
|
||||||
}
|
}
|
||||||
@@ -535,6 +578,12 @@ fn create_run_dir(package: &str, series: &str, arch: &str) -> std::io::Result<Pa
|
|||||||
Ok(dir)
|
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
|
/// The test-run artifacts root: `$PKH_TESTS_DIR`, defaulting to
|
||||||
/// `/var/tmp/pkh/tests`.
|
/// `/var/tmp/pkh/tests`.
|
||||||
pub fn runs_root() -> PathBuf {
|
pub fn runs_root() -> PathBuf {
|
||||||
|
|||||||
+17
-2
@@ -17,6 +17,9 @@ pub enum TestMode {
|
|||||||
Chroot,
|
Chroot,
|
||||||
/// The current context directly: no isolation, tests can rewrite it.
|
/// The current context directly: no isolation, tests can rewrite it.
|
||||||
Local,
|
Local,
|
||||||
|
/// A qemu virtual machine, through the installed autopkgtest's qemu
|
||||||
|
/// runner — the one backend pkh does not reimplement.
|
||||||
|
Vm,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestMode {
|
impl TestMode {
|
||||||
@@ -25,6 +28,7 @@ impl TestMode {
|
|||||||
match self {
|
match self {
|
||||||
TestMode::Chroot => "chroot",
|
TestMode::Chroot => "chroot",
|
||||||
TestMode::Local => "local",
|
TestMode::Local => "local",
|
||||||
|
TestMode::Vm => "vm",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +37,7 @@ impl TestMode {
|
|||||||
match value {
|
match value {
|
||||||
"chroot" => Some(TestMode::Chroot),
|
"chroot" => Some(TestMode::Chroot),
|
||||||
"local" => Some(TestMode::Local),
|
"local" => Some(TestMode::Local),
|
||||||
|
"vm" => Some(TestMode::Vm),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,6 +63,12 @@ pub struct TestOptions<'a> {
|
|||||||
pub inject: Vec<String>,
|
pub inject: Vec<String>,
|
||||||
/// Testbed mode; defaults to [`TestMode::Chroot`].
|
/// Testbed mode; defaults to [`TestMode::Chroot`].
|
||||||
pub mode: Option<TestMode>,
|
pub mode: Option<TestMode>,
|
||||||
|
/// VM image for [`TestMode::Vm`] (required there).
|
||||||
|
pub image: Option<PathBuf>,
|
||||||
|
/// VM virtual CPUs ([`TestMode::Vm`], passthrough).
|
||||||
|
pub cpus: Option<u32>,
|
||||||
|
/// VM memory size in MiB ([`TestMode::Vm`], passthrough).
|
||||||
|
pub ram_size: Option<String>,
|
||||||
/// Explicit binary packages to test; overrides the `pkh deb` output
|
/// Explicit binary packages to test; overrides the `pkh deb` output
|
||||||
/// reuse.
|
/// reuse.
|
||||||
pub debs: Vec<PathBuf>,
|
pub debs: Vec<PathBuf>,
|
||||||
@@ -106,6 +117,9 @@ impl Default for TestOptions<'_> {
|
|||||||
ppa: Vec::new(),
|
ppa: Vec::new(),
|
||||||
inject: Vec::new(),
|
inject: Vec::new(),
|
||||||
mode: None,
|
mode: None,
|
||||||
|
image: None,
|
||||||
|
cpus: None,
|
||||||
|
ram_size: None,
|
||||||
debs: Vec::new(),
|
debs: Vec::new(),
|
||||||
no_build: false,
|
no_build: false,
|
||||||
build: false,
|
build: false,
|
||||||
@@ -197,8 +211,9 @@ mod tests {
|
|||||||
fn mode_parsing() {
|
fn mode_parsing() {
|
||||||
assert_eq!(TestMode::parse("chroot"), Some(TestMode::Chroot));
|
assert_eq!(TestMode::parse("chroot"), Some(TestMode::Chroot));
|
||||||
assert_eq!(TestMode::parse("local"), Some(TestMode::Local));
|
assert_eq!(TestMode::parse("local"), Some(TestMode::Local));
|
||||||
assert_eq!(TestMode::parse("vm"), None);
|
assert_eq!(TestMode::parse("vm"), Some(TestMode::Vm));
|
||||||
|
assert_eq!(TestMode::parse("other"), None);
|
||||||
assert_eq!(TestMode::default(), TestMode::Chroot);
|
assert_eq!(TestMode::default(), TestMode::Chroot);
|
||||||
assert_eq!(TestMode::Local.label(), "local");
|
assert_eq!(TestMode::Vm.label(), "vm");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+242
@@ -0,0 +1,242 @@
|
|||||||
|
//! The `--mode vm` escape hatch: exec the installed `autopkgtest` with
|
||||||
|
//! its qemu runner.
|
||||||
|
//!
|
||||||
|
//! VM virtualization is the one backend pkh deliberately does not
|
||||||
|
//! reimplement: autopkgtest-virt-qemu carries a serial-console control
|
||||||
|
//! protocol, image overlay management and the reboot protocol, and it is
|
||||||
|
//! the only runner satisfying `isolation-machine`/`needs-reboot` tests.
|
||||||
|
//! Everything around it stays pkh's: binary resolution, the report
|
||||||
|
//! model, the exit-code contract and the artifacts directory.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
use crate::debian::tests_control::Test;
|
||||||
|
use crate::interrupt;
|
||||||
|
use crate::report::BuildView;
|
||||||
|
use crate::test::outcome::{TestOutcome, Verdict};
|
||||||
|
|
||||||
|
/// Run the tests through autopkgtest's qemu runner.
|
||||||
|
///
|
||||||
|
/// `tree` is the source tree, `binaries` the fresh binary packages under
|
||||||
|
/// test (may be empty: autopkgtest then builds inside the VM),
|
||||||
|
/// `run_dir` the artifacts directory (passed as `-o`; autopkgtest
|
||||||
|
/// requires it to not exist yet) and `image` the VM image path. Returns
|
||||||
|
/// the per-test outcomes and autopkgtest's raw exit code; the caller
|
||||||
|
/// builds the full report around them.
|
||||||
|
pub(crate) fn run_vm(
|
||||||
|
tree: &Path,
|
||||||
|
binaries: &[PathBuf],
|
||||||
|
tests: &[Test],
|
||||||
|
run_dir: &Path,
|
||||||
|
image: Option<&Path>,
|
||||||
|
opts: &crate::test::options::TestOptions<'_>,
|
||||||
|
view: &dyn BuildView,
|
||||||
|
) -> Result<(Vec<TestOutcome>, i32), Box<dyn std::error::Error>> {
|
||||||
|
let Some(image) = image else {
|
||||||
|
return Err(
|
||||||
|
"--mode vm needs a VM image: pass --image, or build one with \
|
||||||
|
'autopkgtest-build-qemu <series> <image>' (Debian) or \
|
||||||
|
'autopkgtest-buildvm-ubuntu-cloud -r <series> -a <arch>' (Ubuntu)"
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
if !image.exists() {
|
||||||
|
return Err(format!(
|
||||||
|
"VM image '{}' does not exist: pass --image, or build one with \
|
||||||
|
'autopkgtest-build-qemu <series> <image>' (Debian) or \
|
||||||
|
'autopkgtest-buildvm-ubuntu-cloud -r <series> -a <arch>' (Ubuntu)",
|
||||||
|
image.display()
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
if !opts.ppa.is_empty() || opts.pocket.is_some() || !opts.inject.is_empty() {
|
||||||
|
return Err(
|
||||||
|
"--pocket, --ppa and --inject are not supported with --mode vm yet: \
|
||||||
|
configure them inside the image or use a chroot/local run"
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
view.message("Running autopkgtest with the qemu runner");
|
||||||
|
|
||||||
|
let mut command = Command::new("autopkgtest");
|
||||||
|
command.arg("-o").arg(run_dir);
|
||||||
|
let summary_path = run_dir.join("autopkgtest-summary");
|
||||||
|
command.arg("--summary").arg(&summary_path);
|
||||||
|
if opts.apt_upgrade {
|
||||||
|
command.arg("--apt-upgrade");
|
||||||
|
}
|
||||||
|
for name in &opts.test_names {
|
||||||
|
command.arg("--test-name").arg(name);
|
||||||
|
}
|
||||||
|
for name in &opts.skip_tests {
|
||||||
|
command.arg("--skip-test").arg(name);
|
||||||
|
}
|
||||||
|
for setup in &opts.setup_commands {
|
||||||
|
command.arg("--setup-commands").arg(setup);
|
||||||
|
}
|
||||||
|
if opts.shell {
|
||||||
|
command.arg("--shell");
|
||||||
|
}
|
||||||
|
if opts.shell_fail {
|
||||||
|
command.arg("--shell-fail");
|
||||||
|
}
|
||||||
|
if let Some(secs) = opts.timeout {
|
||||||
|
command.arg("--timeout").arg(secs.to_string());
|
||||||
|
}
|
||||||
|
if let Some(factor) = opts.timeout_factor {
|
||||||
|
command.arg("--timeout-factor").arg(factor.to_string());
|
||||||
|
}
|
||||||
|
for deb in binaries {
|
||||||
|
// Absolute paths: autopkgtest reads a bare name as an archive
|
||||||
|
// source package.
|
||||||
|
command.arg(deb.canonicalize()?);
|
||||||
|
}
|
||||||
|
command.arg(tree.canonicalize()?);
|
||||||
|
command.arg("--");
|
||||||
|
command.arg("qemu");
|
||||||
|
if let Some(cpus) = opts.cpus {
|
||||||
|
command.arg("--cpus").arg(cpus.to_string());
|
||||||
|
}
|
||||||
|
if let Some(ram) = &opts.ram_size {
|
||||||
|
command.arg("--ram-size").arg(ram);
|
||||||
|
}
|
||||||
|
command.arg(image);
|
||||||
|
|
||||||
|
// The autopkgtest process must die with us: run it in its own
|
||||||
|
// process group and SIGTERM the group from the interrupt-time
|
||||||
|
// cleanup hook (autopkgtest cleans up its testbed on TERM).
|
||||||
|
let mut child = command
|
||||||
|
.stdin(Stdio::inherit())
|
||||||
|
.stdout(Stdio::inherit())
|
||||||
|
.stderr(Stdio::inherit())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("cannot run autopkgtest (is it installed?): {e}"))?;
|
||||||
|
let pid = child.id();
|
||||||
|
// Best-effort: without its own group, the cleanup kill below would
|
||||||
|
// only reach the autopkgtest front-end, not its virt server.
|
||||||
|
unsafe { libc::setpgid(pid as libc::pid_t, pid as libc::pid_t) };
|
||||||
|
let hook = interrupt::register_cleanup_hook(Box::new(move || {
|
||||||
|
// SAFETY: kill(2) to the spawned autopkgtest process group
|
||||||
|
unsafe { libc::kill(-(pid as libc::pid_t), libc::SIGTERM) };
|
||||||
|
}));
|
||||||
|
|
||||||
|
let status = child.wait()?;
|
||||||
|
drop(hook);
|
||||||
|
let raw_code = status.code().unwrap_or(16);
|
||||||
|
|
||||||
|
// autopkgtest's own failure classes are runtime errors here; the
|
||||||
|
// pass/fail/skip classes become the report.
|
||||||
|
if matches!(raw_code, 12 | 14 | 16 | 20) {
|
||||||
|
return Err(format!(
|
||||||
|
"autopkgtest failed (exit {raw_code}): check the artifacts in '{}'",
|
||||||
|
run_dir.display()
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let outcomes = parse_summary(&summary_path, tests)?;
|
||||||
|
Ok((outcomes, raw_code))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse autopkgtest's `--summary` file: one line per test, `<testname>
|
||||||
|
/// PASS|FAIL|SKIP|FLAKY [reason...]`. Tolerant by design — the format is
|
||||||
|
/// stable but not formally versioned; unparseable lines are skipped, and
|
||||||
|
/// a missing file yields no outcomes (the raw exit code carries the
|
||||||
|
/// verdict).
|
||||||
|
pub(crate) fn parse_summary(
|
||||||
|
summary_path: &Path,
|
||||||
|
tests: &[Test],
|
||||||
|
) -> Result<Vec<TestOutcome>, Box<dyn std::error::Error>> {
|
||||||
|
let mut outcomes = Vec::new();
|
||||||
|
let Ok(content) = std::fs::read_to_string(summary_path) else {
|
||||||
|
return Ok(outcomes);
|
||||||
|
};
|
||||||
|
for line in content.lines() {
|
||||||
|
let mut tokens = line.splitn(3, ' ');
|
||||||
|
let Some(name) = tokens.next() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(verdict) = tokens.next() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// Summary names carry the argid prefix ("<argid>-<testname>"); a
|
||||||
|
// declared test matching by suffix wins, else the token stands.
|
||||||
|
let name = name.to_string();
|
||||||
|
let name = match tests
|
||||||
|
.iter()
|
||||||
|
.find(|test| name.ends_with(&test.name) || test.name == name)
|
||||||
|
{
|
||||||
|
Some(test) => test.name.clone(),
|
||||||
|
None => name,
|
||||||
|
};
|
||||||
|
let reason = tokens.next().map(str::to_string);
|
||||||
|
let verdict = match verdict.trim() {
|
||||||
|
"PASS" => Verdict::Pass,
|
||||||
|
"FAIL" => Verdict::Fail,
|
||||||
|
"SKIP" => Verdict::Skip,
|
||||||
|
"FLAKY" => Verdict::Flaky,
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
outcomes.push(TestOutcome {
|
||||||
|
name,
|
||||||
|
verdict,
|
||||||
|
reason,
|
||||||
|
duration: std::time::Duration::ZERO,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(outcomes)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::debian::tests_control::Test;
|
||||||
|
|
||||||
|
fn test(name: &str) -> Test {
|
||||||
|
Test {
|
||||||
|
name: name.to_string(),
|
||||||
|
script: Some(name.to_string()),
|
||||||
|
command: None,
|
||||||
|
depends: None,
|
||||||
|
restrictions: Vec::new(),
|
||||||
|
tests_directory: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The summary parser maps the four verdicts, keeps reasons and
|
||||||
|
/// resolves argid-prefixed names back to the declared tests.
|
||||||
|
#[test]
|
||||||
|
fn summary_parsing() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("summary");
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
"hello-test-demo-smoke PASS\n\
|
||||||
|
hello-test-demo-command1 FAIL exit code 1\n\
|
||||||
|
container SKIP required isolation not available\n\
|
||||||
|
flaky-one FLAKY passed on retry\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let tests = [test("smoke"), test("command1")];
|
||||||
|
let outcomes = parse_summary(&path, &tests).unwrap();
|
||||||
|
assert_eq!(outcomes.len(), 4);
|
||||||
|
assert_eq!(outcomes[0].name, "smoke");
|
||||||
|
assert_eq!(outcomes[0].verdict, Verdict::Pass);
|
||||||
|
assert_eq!(outcomes[1].verdict, Verdict::Fail);
|
||||||
|
assert_eq!(outcomes[1].reason.as_deref(), Some("exit code 1"));
|
||||||
|
assert_eq!(outcomes[2].name, "container");
|
||||||
|
assert_eq!(outcomes[2].verdict, Verdict::Skip);
|
||||||
|
assert_eq!(outcomes[3].verdict, Verdict::Flaky);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A missing summary file parses to no outcomes: the raw exit code
|
||||||
|
/// carries the verdict.
|
||||||
|
#[test]
|
||||||
|
fn missing_summary_yields_no_outcomes() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let outcomes = parse_summary(&dir.path().join("absent"), &[]).unwrap();
|
||||||
|
assert!(outcomes.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user