feat(xtask): add package-native mutation gate
Expose a capped mutation command that selects only elected packages, rejects unknown input, propagates mutest failures, and keeps output under the workspace target directory. req: test/020 req: test/021 req: test/022
This commit is contained in:
+107
-4
@@ -12,6 +12,7 @@ fn main() -> ExitCode {
|
||||
Some("test") | None => run_test_plan(),
|
||||
Some("html-examples-smoke") => run_html_examples_smoke(),
|
||||
Some("bench") => run_bench_plan(),
|
||||
Some("mutation") => run_mutation_plan(args.next().as_deref()),
|
||||
Some("workout") => {
|
||||
let subcommand = args.next();
|
||||
let operand = args.next();
|
||||
@@ -37,7 +38,7 @@ fn main() -> ExitCode {
|
||||
|
||||
fn print_help() {
|
||||
println!(
|
||||
"hemx-ci — resource-aware project checks\n\n cargo run -p hemx-xtask -- test\n cargo run -p hemx-xtask -- html-examples-smoke\n cargo run -p hemx-xtask -- bench\n cargo run -p hemx-xtask -- app new PATH\n cargo run -p hemx-xtask -- app new --mobile PATH\n cargo run -p hemx-xtask -- workout new PATH\n cargo run -p hemx-xtask -- workout dev\n cargo run -p hemx-xtask -- workout test\n cargo run -p hemx-xtask -- workout build\n cargo run -p hemx-xtask -- workout mobile-release\n cargo run -p hemx-xtask -- workout mobile-verify\n cargo run -p hemx-xtask -- workout doctor\n\nEnvironment overrides:\n HEMX_CI_JOBS=N compile jobs, capped by detected resources\n HEMX_CI_TEST_THREADS=N Rust test threads, capped by detected resources\n HEMX_CI_SKIP_BROWSER=1 skip browser E2E\n HEMX_WORKOUT_ORIGIN=https://app.example.com\n HEMX_WORKOUT_MOBILE_OUT=target/hemx-mobile/workout"
|
||||
"hemx-ci — resource-aware project checks\n\n cargo run -p hemx-xtask -- test\n cargo run -p hemx-xtask -- html-examples-smoke\n cargo run -p hemx-xtask -- bench\n cargo run -p hemx-xtask -- mutation [PACKAGE]\n cargo run -p hemx-xtask -- app new PATH\n cargo run -p hemx-xtask -- app new --mobile PATH\n cargo run -p hemx-xtask -- workout new PATH\n cargo run -p hemx-xtask -- workout dev\n cargo run -p hemx-xtask -- workout test\n cargo run -p hemx-xtask -- workout build\n cargo run -p hemx-xtask -- workout mobile-release\n cargo run -p hemx-xtask -- workout mobile-verify\n cargo run -p hemx-xtask -- workout doctor\n\nEnvironment overrides:\n HEMX_CI_JOBS=N compile jobs, capped by detected resources\n HEMX_CI_TEST_THREADS=N Rust test threads, capped by detected resources\n HEMX_MUTEST_BIN=PATH mutest executable (default: mutest)\n HEMX_CI_SKIP_BROWSER=1 skip browser E2E\n HEMX_WORKOUT_ORIGIN=https://app.example.com\n HEMX_WORKOUT_MOBILE_OUT=target/hemx-mobile/workout"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1248,6 +1249,97 @@ fn run_bench_plan() -> ExitCode {
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
const MUTATION_PACKAGES: &[&str] = &[
|
||||
"hemx",
|
||||
"hemx-axum",
|
||||
"hemx-build",
|
||||
"hemx-core",
|
||||
"hemx-derive",
|
||||
"hemx-host",
|
||||
"hemx-js",
|
||||
"hemx-sync",
|
||||
"hemx-sync-macros",
|
||||
"hemx-test",
|
||||
"hemx-wasm",
|
||||
];
|
||||
|
||||
fn mutation_packages(package: Option<&str>) -> Result<Vec<&'static str>, String> {
|
||||
match package {
|
||||
None | Some("all") => Ok(MUTATION_PACKAGES.to_vec()),
|
||||
Some(package) => MUTATION_PACKAGES
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|candidate| *candidate == package)
|
||||
.map(|package| vec![package])
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"unknown mutation package `{package}`; expected `all` or one of: {}",
|
||||
MUTATION_PACKAGES.join(", ")
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_mutation_plan(package: Option<&str>) -> ExitCode {
|
||||
let packages = match mutation_packages(package) {
|
||||
Ok(packages) => packages,
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let budget = Budget::detect().with_jobs(4);
|
||||
budget.report();
|
||||
let mutest = env::var_os("HEMX_MUTEST_BIN").unwrap_or_else(|| "mutest".into());
|
||||
|
||||
let output_root = workspace_root().join("target/mutest");
|
||||
if let Err(error) = fs::create_dir_all(&output_root) {
|
||||
eprintln!(
|
||||
"failed to create mutation output directory {}: {error}",
|
||||
output_root.display()
|
||||
);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
for package in packages {
|
||||
eprintln!("\n==> mutation: {package}");
|
||||
let output = output_root.join(package);
|
||||
let jobs = budget.jobs.to_string();
|
||||
let status = Command::new(&mutest)
|
||||
.current_dir(workspace_root())
|
||||
.args([
|
||||
"-p",
|
||||
package,
|
||||
"-j",
|
||||
&jobs,
|
||||
"--jobserver-tasks",
|
||||
&jobs,
|
||||
"--colors",
|
||||
"never",
|
||||
"--annotations",
|
||||
"none",
|
||||
"--no-times",
|
||||
"--exhaustive",
|
||||
"-o",
|
||||
])
|
||||
.arg(output)
|
||||
.status();
|
||||
match status {
|
||||
Ok(status) if status.success() => {}
|
||||
Ok(status) => {
|
||||
eprintln!("mutation: {package} failed with {status}");
|
||||
return ExitCode::from(status.code().unwrap_or(1) as u8);
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("failed to run mutest for {package}: {error}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Budget {
|
||||
cpus: usize,
|
||||
@@ -1473,13 +1565,24 @@ fn is_executable(path: impl AsRef<Path>) -> bool {
|
||||
mod tests {
|
||||
use super::{
|
||||
android_twa_release_json, create_app_scaffold, create_mobile_app_scaffold,
|
||||
create_workout_app, mobile_external_blockers, origin_host, verify_workout_mobile_release,
|
||||
workout_mobile_manifest, workspace_root, write_workout_mobile_release, Budget,
|
||||
WorkoutMobileConfig,
|
||||
create_workout_app, mobile_external_blockers, mutation_packages, origin_host,
|
||||
verify_workout_mobile_release, workout_mobile_manifest, workspace_root,
|
||||
write_workout_mobile_release, Budget, WorkoutMobileConfig, MUTATION_PACKAGES,
|
||||
};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn mutation_plan_selects_only_elected_packages() {
|
||||
// req: test/020 test req: test/021 test req: test/022 test
|
||||
assert_eq!(mutation_packages(None).unwrap(), MUTATION_PACKAGES);
|
||||
assert_eq!(mutation_packages(Some("all")).unwrap(), MUTATION_PACKAGES);
|
||||
assert_eq!(mutation_packages(Some("hemx-js")).unwrap(), ["hemx-js"]);
|
||||
let error = mutation_packages(Some("example-app")).unwrap_err();
|
||||
assert!(error.contains("unknown mutation package `example-app`"));
|
||||
assert!(error.contains("hemx-core"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_steps_resolve_the_workspace_independent_of_caller_directory() {
|
||||
assert!(workspace_root().join("Cargo.toml").is_file()); // req: test/004
|
||||
|
||||
Reference in New Issue
Block a user