feat(xtask): shard package mutation gates
Accept validated native SHARD/TOTAL operands, isolate shard output directories, preserve the unsharded package gate, and prove hemx-build shard 1/4 through the public xtask entry point. req: test/022 req: test/023
This commit is contained in:
@@ -52,7 +52,7 @@ Keep it stable. Prefer pointers to canonical sources over copied structure, file
|
||||
- Add only durable style, ownership, gotchas, and at most a few stable commands agents should actually run.
|
||||
- Prefer links or pointers to canonical sources over copied lists.
|
||||
- Avoid project trees, architecture maps, generated inventories, current file sizes, issue lists, TODO inventories, and other snapshots that will rot.
|
||||
- Stable commands: `cargo run -p hemx-xtask -- test`, `cargo run -p hemx-xtask -- mutation [PACKAGE]`, `cargo run -p hemx-xtask -- html-examples-smoke`, `cargo check --workspace`, `redgate refs`. Use the xtask runner for full verification so jobs are capped from local CPU and memory and commands resolve the workspace independently of the caller's directory; use the html_examples smoke for focused repo-owned browser verification of the HTML pattern gallery, no-reload dynamic interactions, and no `/tmp` scripts. Keep fast crate tests, focused browser smoke, and full xtask authority distinct; the full path should stay within a documented 10 minute local timeout or be split into deterministic shards under the same wrapper. req: test/004 req: test/006 req: test/012 req: test/013 req: test/014 req: test/015 req: test/016
|
||||
- Stable commands: `cargo run -p hemx-xtask -- test`, `cargo run -p hemx-xtask -- mutation [PACKAGE] [SHARD/TOTAL]`, `cargo run -p hemx-xtask -- html-examples-smoke`, `cargo check --workspace`, `redgate refs`. Use the xtask runner for full verification so jobs are capped from local CPU and memory and commands resolve the workspace independently of the caller's directory; use the html_examples smoke for focused repo-owned browser verification of the HTML pattern gallery, no-reload dynamic interactions, and no `/tmp` scripts. Keep fast crate tests, focused browser smoke, and full xtask authority distinct; the full path should stay within a documented 10 minute local timeout or use every deterministic native mutation shard under the same wrapper. req: test/004 req: test/006 req: test/012 req: test/013 req: test/014 req: test/015 req: test/016 req: test/022 req: test/023
|
||||
- Example behavior tests should prefer `hemx_test` generated-resource assertion methods over raw slot constants, raw effect/payload matching, or boolean predicates wrapped in opaque `assert!`; failures should include the expectation and actual effects, while rendered target/handle assertions should name the generated resource. Keep browser selector helpers as test adapters only, not authoring APIs. Process-backed tests use the RAII `TestProcess` harness rather than duplicating readiness loops and child cleanup. req: test/008 req: test/009 req: test/010 req: test/017 req: test/018 req: test/019
|
||||
- Run the workout product exemplar with `cargo run -p hemx-xtask -- workout dev` and open `http://127.0.0.1:3028`; set `HEMX_WORKOUT_ADDR=127.0.0.1:3030` if the default port is busy. Its durable visual direction and recovery expectations live in `examples/workout/DESIGN.md`. req: examples/008
|
||||
- Use the same Workout command surface for tests, production build, and mobile release: `cargo run -p hemx-xtask -- workout test`, `cargo run -p hemx-xtask -- workout build`, `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-release`, and `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-verify`; Android/iOS SDKs, store submission targets, and signing remain external blockers, not repo-owned secrets, and do not imply a broad `hemx-mobile` framework. req: examples/006 req: examples/011 req: examples/013
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
- [ ] **State:** In progress — the package-native capped xtask entry point is reachable, rejects unknown packages, propagates mutest failure, and mutation-tests `hemx-axum`, `hemx-core`, `hemx-js`, and the full `hemx-test` package cleanly; full package closure remains.
|
||||
- **User value:** maintainers can run one bounded repository command and trust that meaningful Rust logic across every mutation-applicable library is either killed or explicitly justified.
|
||||
- **Build:** add a capped `hemx-xtask` mutation command that invokes `/opt/repositories/mutest`/`mutest` through package-native test targets rather than the broken workspace-wide example path; enumerate only current mutation-applicable library/proc-macro packages; finish adversarial tests or simplify code until every survivor is classified; keep equivalent, invariant-only, and infrastructure-inapplicable classifications inspectable and minimal; document the exact local release command in the existing readiness surface.
|
||||
- **Blocked by:** none; broad survivors currently remain in `hemx-build`, `hemx-derive`, and `hemx-lsp` outside already-clean focused contracts. The current `hemx-build` frontier now also mutation-proves all compact Rust-type/vector-fact and residual default/alias/parameter/keyed-descendant/Cargo-root helper mutants exposed during the full gate. That gate produced no missed-mutant report before the 30-minute command timeout, so the next slice must either complete it with deterministic package shards or expose and close the next shard's survivors. The complete 470-mutant `hemx-axum` package gate now passes with 262 caught and 208 unviable after public page/form/multipart/registry/response/runtime proofs and narrow classification of infallible header parsing and streamed multipart unwrap-equivalent mutants.
|
||||
- **Blocked by:** none; broad survivors currently remain in `hemx-build`, `hemx-derive`, and `hemx-lsp` outside already-clean focused contracts. The mutation entry point now accepts validated native `SHARD/TOTAL` operands with shard-specific output directories while preserving the unsharded gate. `hemx-build` shard `1/4` passed through xtask with 325 mutants (280 caught, 45 unviable); shards `2/4` through `4/4` remain before the package gate is complete. The complete 470-mutant `hemx-axum` package gate now passes with 262 caught and 208 unviable after public page/form/multipart/registry/response/runtime proofs and narrow classification of infallible header parsing and streamed multipart unwrap-equivalent mutants.
|
||||
- **Proof:** the new xtask mutation command exits zero within its documented bound, covers each applicable package, emits no unexplained missed mutant, and a deliberate adjacent mutation makes it fail. `cargo run -p hemx-xtask -- test` remains green. req: test/020 req: test/021
|
||||
|
||||
## 3. Elect and enforce the release license policy
|
||||
|
||||
+3
-1
@@ -704,7 +704,9 @@ what a valid business email is. [north_star]
|
||||
|
||||
021 Unexplained missed mutants must block release; equivalent, invariant-only, and infrastructure-inapplicable mutants must be explicitly classified. [north_star]
|
||||
|
||||
022 `cargo run -p hemx-xtask -- mutation [PACKAGE]` must run capped exhaustive mutation tests through package-native test targets, reject unknown packages, and fail when any selected package has an unexplained survivor. [north_star]
|
||||
022 `cargo run -p hemx-xtask -- mutation [PACKAGE] [SHARD/TOTAL]` must run capped exhaustive package-native mutation tests and fail on unexplained survivors. [north_star]
|
||||
|
||||
023 A supplied mutation `SHARD/TOTAL` must select a deterministic native shard and reject invalid bounds; omission preserves the full-package gate. [north_star]
|
||||
|
||||
---
|
||||
|
||||
|
||||
+65
-9
@@ -12,7 +12,11 @@ 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("mutation") => {
|
||||
let package = args.next();
|
||||
let shard = args.next();
|
||||
run_mutation_plan(package.as_deref(), shard.as_deref())
|
||||
}
|
||||
Some("workout") => {
|
||||
let subcommand = args.next();
|
||||
let operand = args.next();
|
||||
@@ -38,7 +42,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 -- 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"
|
||||
"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] [SHARD/TOTAL]\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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1280,7 +1284,30 @@ fn mutation_packages(package: Option<&str>) -> Result<Vec<&'static str>, String>
|
||||
}
|
||||
}
|
||||
|
||||
fn run_mutation_plan(package: Option<&str>) -> ExitCode {
|
||||
fn mutation_shard(shard: Option<&str>) -> Result<Option<String>, String> {
|
||||
let Some(shard) = shard else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some((index, total)) = shard.split_once('/') else {
|
||||
return Err(format!(
|
||||
"invalid mutation shard `{shard}`; expected SHARD/TOTAL such as `1/4`"
|
||||
));
|
||||
};
|
||||
let index = index
|
||||
.parse::<usize>()
|
||||
.map_err(|_| format!("invalid mutation shard `{shard}`; SHARD must be an integer"))?;
|
||||
let total = total
|
||||
.parse::<usize>()
|
||||
.map_err(|_| format!("invalid mutation shard `{shard}`; TOTAL must be an integer"))?;
|
||||
if total < 2 || index == 0 || index > total {
|
||||
return Err(format!(
|
||||
"invalid mutation shard `{shard}`; require TOTAL >= 2 and 1 <= SHARD <= TOTAL"
|
||||
));
|
||||
}
|
||||
Ok(Some(format!("{index}/{total}")))
|
||||
}
|
||||
|
||||
fn run_mutation_plan(package: Option<&str>, shard: Option<&str>) -> ExitCode {
|
||||
let packages = match mutation_packages(package) {
|
||||
Ok(packages) => packages,
|
||||
Err(error) => {
|
||||
@@ -1288,6 +1315,13 @@ fn run_mutation_plan(package: Option<&str>) -> ExitCode {
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let shard = match mutation_shard(shard) {
|
||||
Ok(shard) => shard,
|
||||
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());
|
||||
@@ -1303,9 +1337,17 @@ fn run_mutation_plan(package: Option<&str>) -> ExitCode {
|
||||
|
||||
for package in packages {
|
||||
eprintln!("\n==> mutation: {package}");
|
||||
let output = output_root.join(package);
|
||||
let output = shard.as_ref().map_or_else(
|
||||
|| output_root.join(package),
|
||||
|shard| {
|
||||
output_root
|
||||
.join(package)
|
||||
.join(format!("shard-{}", shard.replace('/', "-of-")))
|
||||
},
|
||||
);
|
||||
let jobs = budget.jobs.to_string();
|
||||
let status = Command::new(&mutest)
|
||||
let mut command = Command::new(&mutest);
|
||||
command
|
||||
.current_dir(workspace_root())
|
||||
.args([
|
||||
"-p",
|
||||
@@ -1322,8 +1364,11 @@ fn run_mutation_plan(package: Option<&str>) -> ExitCode {
|
||||
"--exhaustive",
|
||||
"-o",
|
||||
])
|
||||
.arg(output)
|
||||
.status();
|
||||
.arg(output);
|
||||
if let Some(shard) = &shard {
|
||||
command.args(["--shard", shard]);
|
||||
}
|
||||
let status = command.status();
|
||||
match status {
|
||||
Ok(status) if status.success() => {}
|
||||
Ok(status) => {
|
||||
@@ -1565,8 +1610,8 @@ 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, mutation_packages, origin_host,
|
||||
verify_workout_mobile_release, workout_mobile_manifest, workspace_root,
|
||||
create_workout_app, mobile_external_blockers, mutation_packages, mutation_shard,
|
||||
origin_host, verify_workout_mobile_release, workout_mobile_manifest, workspace_root,
|
||||
write_workout_mobile_release, Budget, WorkoutMobileConfig, MUTATION_PACKAGES,
|
||||
};
|
||||
use std::fs;
|
||||
@@ -1581,6 +1626,17 @@ mod tests {
|
||||
let error = mutation_packages(Some("example-app")).unwrap_err();
|
||||
assert!(error.contains("unknown mutation package `example-app`"));
|
||||
assert!(error.contains("hemx-core"));
|
||||
|
||||
assert_eq!(mutation_shard(None).unwrap(), None);
|
||||
assert_eq!(mutation_shard(Some("1/4")).unwrap().as_deref(), Some("1/4"));
|
||||
assert_eq!(mutation_shard(Some("4/4")).unwrap().as_deref(), Some("4/4"));
|
||||
for invalid in ["1", "a/4", "1/a", "0/4", "5/4", "1/1"] {
|
||||
assert!(
|
||||
mutation_shard(Some(invalid)).unwrap_err().contains(invalid),
|
||||
"missing invalid shard in diagnostic"
|
||||
);
|
||||
}
|
||||
// test req: test/022 req: test/023
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user