test,cli: wire pkh test into the CLI
Argument wiring only, per the report ports: the live view carries phases and the outcome, the per-test report renders after the run (text or --json) and decides the exit code; runtime errors exit 2. --list-tests prints the parsed debian/tests/control and exits without booting a testbed.
This commit is contained in:
+213
@@ -381,6 +381,82 @@ fn main() {
|
||||
.long_about("List the resumable build sessions recorded from this tree, one per target (series/arch), with the id to pass to 'pkh deb --resume <id>'.\nRead-only: never builds or removes anything ('pkh prune' removes old sessions)."),
|
||||
)
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("test")
|
||||
.about("Run the package's DEP-8 as-installed tests (autopkgtests)")
|
||||
.arg(arg!([path] "Source tree to test (default: the current directory)").required(false))
|
||||
.arg(arg!(-s --series <series> "Target distribution series").required(false))
|
||||
.arg(arg!(-d --dist <dist> "Target distribution (debian, ubuntu)").required(false))
|
||||
.arg(arg!(-a --arch <arch> "Target architecture").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!(--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!(--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(
|
||||
clap::Arg::new("no_build")
|
||||
.long("no-build")
|
||||
.action(clap::ArgAction::SetTrue)
|
||||
.help("Never build: fail when no fresh 'pkh deb' output exists next to the tree"),
|
||||
)
|
||||
.arg(arg!(--build "Ignore fresh pkh deb output and rebuild the binaries first").required(false))
|
||||
.arg(
|
||||
clap::Arg::new("test_name")
|
||||
.long("test-name")
|
||||
.value_name("NAME")
|
||||
.action(clap::ArgAction::Append)
|
||||
.help("Run only this test (can be specified multiple times)"),
|
||||
)
|
||||
.arg(
|
||||
clap::Arg::new("skip_test")
|
||||
.long("skip-test")
|
||||
.value_name("NAME")
|
||||
.action(clap::ArgAction::Append)
|
||||
.help("Skip this test, reporting it as skipped (can be specified multiple times)"),
|
||||
)
|
||||
.arg(
|
||||
clap::Arg::new("setup_commands")
|
||||
.long("setup-commands")
|
||||
.value_name("CMD")
|
||||
.action(clap::ArgAction::Append)
|
||||
.help("Shell command run in the testbed before the tests (can be specified multiple times)"),
|
||||
)
|
||||
.arg(
|
||||
clap::Arg::new("apt_upgrade")
|
||||
.long("apt-upgrade")
|
||||
.action(clap::ArgAction::SetTrue)
|
||||
.help("Update, upgrade and autopurge the testbed before the tests"),
|
||||
)
|
||||
.arg(arg!(--shell "Drop into an interactive testbed shell after the run").required(false))
|
||||
.arg(
|
||||
clap::Arg::new("shell_fail")
|
||||
.long("shell-fail")
|
||||
.action(clap::ArgAction::SetTrue)
|
||||
.help("Drop into an interactive testbed shell when a test failed"),
|
||||
)
|
||||
.arg(arg!(--timeout <secs> "Per-test timeout in seconds (default: 10000)").required(false).value_parser(clap::value_parser!(u64)))
|
||||
.arg(
|
||||
clap::Arg::new("timeout_factor")
|
||||
.long("timeout-factor")
|
||||
.value_name("FACTOR")
|
||||
.value_parser(clap::value_parser!(f64))
|
||||
.help("Scale the default timeouts (an explicit --timeout stands as given)"),
|
||||
)
|
||||
.arg(
|
||||
clap::Arg::new("fail_on")
|
||||
.long("fail-on")
|
||||
.value_name("TRIGGERS")
|
||||
.help("Comma-separated extra failure triggers: skip, no-tests (default: none)"),
|
||||
)
|
||||
.arg(
|
||||
clap::Arg::new("list_tests")
|
||||
.long("list-tests")
|
||||
.action(clap::ArgAction::SetTrue)
|
||||
.help("Print the tests declared in debian/tests/control and exit"),
|
||||
)
|
||||
.arg(arg!(--json "Emit the report as JSON").required(false))
|
||||
.arg(arg!(--verbose "Show raw test output instead of the summarized live view").required(false)),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("lint")
|
||||
.about("Lint the package (lintian wrapper + pkh-native checks)")
|
||||
@@ -902,6 +978,143 @@ fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(("test", sub_matches)) => {
|
||||
let path = sub_matches
|
||||
.get_one::<String>("path")
|
||||
.map(std::path::PathBuf::from);
|
||||
|
||||
// Read-only listing of the declared tests; never boots a
|
||||
// testbed.
|
||||
if sub_matches.get_flag("list_tests") {
|
||||
let tree = path.unwrap_or_else(current_dir_or_exit);
|
||||
match pkh::test::list_tests(&tree) {
|
||||
Ok(listing) => print!("{listing}"),
|
||||
Err(e) => {
|
||||
error!("{e}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+C must release the ephemeral chroot instead of dying
|
||||
// on the default disposition (the deb pattern).
|
||||
interrupt::install();
|
||||
|
||||
let mode = match sub_matches.get_one::<String>("mode").map(String::as_str) {
|
||||
None => None,
|
||||
Some(value) => match pkh::test::TestMode::parse(value) {
|
||||
Some(mode) => Some(mode),
|
||||
None => {
|
||||
error!("Invalid --mode value '{value}': expected chroot or local");
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
};
|
||||
let (fail_on_skip, fail_on_no_tests) =
|
||||
match sub_matches.get_one::<String>("fail_on").map(String::as_str) {
|
||||
None => (false, false),
|
||||
Some(list) => {
|
||||
let mut skip = false;
|
||||
let mut no_tests = false;
|
||||
for trigger in list.split(',').map(str::trim).filter(|t| !t.is_empty()) {
|
||||
match trigger {
|
||||
"skip" => skip = true,
|
||||
"no-tests" => no_tests = true,
|
||||
other => {
|
||||
error!(
|
||||
"Unknown --fail-on trigger '{other}': \
|
||||
expected skip or no-tests"
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
(skip, no_tests)
|
||||
}
|
||||
};
|
||||
|
||||
// Live build view, unless --verbose (DebUi additionally
|
||||
// disables itself when stdout is not a terminal).
|
||||
let quiet = pkh::report::Quiet;
|
||||
let live = if sub_matches.get_flag("verbose") {
|
||||
None
|
||||
} else {
|
||||
Some(pkh::ui::deb::DebUi::new(&multi))
|
||||
};
|
||||
let view: &dyn pkh::report::BuildView = live
|
||||
.as_ref()
|
||||
.map(|v| v as &dyn pkh::report::BuildView)
|
||||
.unwrap_or(&quiet);
|
||||
|
||||
let options = pkh::test::TestOptions {
|
||||
tree: path,
|
||||
series: sub_matches.get_one::<String>("series").cloned(),
|
||||
dist: sub_matches.get_one::<String>("dist").cloned(),
|
||||
arch: sub_matches.get_one::<String>("arch").cloned(),
|
||||
pocket: sub_matches.get_one::<String>("pocket").cloned(),
|
||||
ppa: sub_matches
|
||||
.get_many::<String>("ppa")
|
||||
.map(|v| v.cloned().collect())
|
||||
.unwrap_or_default(),
|
||||
inject: sub_matches
|
||||
.get_many::<String>("inject")
|
||||
.map(|v| v.cloned().collect())
|
||||
.unwrap_or_default(),
|
||||
mode,
|
||||
debs: sub_matches
|
||||
.get_many::<String>("debs")
|
||||
.map(|v| v.map(std::path::PathBuf::from).collect())
|
||||
.unwrap_or_default(),
|
||||
no_build: sub_matches.get_flag("no_build"),
|
||||
build: sub_matches.get_flag("build"),
|
||||
test_names: sub_matches
|
||||
.get_many::<String>("test_name")
|
||||
.map(|v| v.cloned().collect())
|
||||
.unwrap_or_default(),
|
||||
skip_tests: sub_matches
|
||||
.get_many::<String>("skip_test")
|
||||
.map(|v| v.cloned().collect())
|
||||
.unwrap_or_default(),
|
||||
setup_commands: sub_matches
|
||||
.get_many::<String>("setup_commands")
|
||||
.map(|v| v.cloned().collect())
|
||||
.unwrap_or_default(),
|
||||
apt_upgrade: sub_matches.get_flag("apt_upgrade"),
|
||||
shell: sub_matches.get_flag("shell"),
|
||||
shell_fail: sub_matches.get_flag("shell_fail"),
|
||||
timeout: sub_matches.get_one::<u64>("timeout").copied(),
|
||||
timeout_factor: sub_matches.get_one::<f64>("timeout_factor").copied(),
|
||||
fail_on_skip,
|
||||
fail_on_no_tests,
|
||||
json: sub_matches.get_flag("json"),
|
||||
verbose: sub_matches.get_flag("verbose"),
|
||||
view,
|
||||
};
|
||||
let json = options.json;
|
||||
|
||||
match rt.block_on(pkh::test::run(options)) {
|
||||
Ok(report) => {
|
||||
// The per-test report is the deliverable; the live
|
||||
// view carries phases and the outcome banner only.
|
||||
if json {
|
||||
println!("{}", report.render_json());
|
||||
} else {
|
||||
print!("{}", report.render_text());
|
||||
}
|
||||
std::process::exit(report.exit_code(fail_on_skip, fail_on_no_tests));
|
||||
}
|
||||
Err(e) => {
|
||||
// On Ctrl+C the interrupt watchdog owns the shutdown
|
||||
// (see `pkh deb`): park here instead of racing it.
|
||||
if pkh::interrupt::interrupted() {
|
||||
interrupt::wait_for_shutdown();
|
||||
}
|
||||
error!("{e}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(("prune", sub_matches)) => {
|
||||
let dry_run = sub_matches
|
||||
.get_one::<bool>("dry_run")
|
||||
|
||||
@@ -536,6 +536,34 @@ fn create_run_dir(package: &str, series: &str, arch: &str) -> std::io::Result<Pa
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
/// Render the tests declared in the tree's `debian/tests/control`, one
|
||||
/// block per test (`pkh test --list-tests`).
|
||||
pub fn list_tests(tree: &Path) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let tests_control = TestsControl::parse(&tree.join("debian/tests/control"))
|
||||
.map_err(|e| format!("cannot parse debian/tests/control: {e}"))?;
|
||||
let mut out = String::new();
|
||||
for test in &tests_control.tests {
|
||||
out.push_str(&test.name);
|
||||
out.push('\n');
|
||||
if let Some(command) = &test.command {
|
||||
out.push_str(&format!(" Test-Command: {command}\n"));
|
||||
}
|
||||
if !test.restrictions.is_empty() {
|
||||
out.push_str(&format!(
|
||||
" Restrictions: {}\n",
|
||||
test.restrictions.join(" ")
|
||||
));
|
||||
}
|
||||
if let Some(depends) = &test.depends {
|
||||
out.push_str(&format!(" Depends: {depends}\n"));
|
||||
}
|
||||
if let Some(directory) = &test.tests_directory {
|
||||
out.push_str(&format!(" Tests-Directory: {directory}\n"));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The binary package name of a staged .deb file name.
|
||||
fn deb_binary_name(path: &Path) -> Option<String> {
|
||||
let name = path.file_name()?.to_string_lossy();
|
||||
|
||||
Reference in New Issue
Block a user