test(build): close value and keyed-scope mutants
Assert every static attribute validator boundary, exact missing/empty/invalid diagnostics, and valid AppBuilder lowering. Prove nested keyed ancestry, sibling/invalid scope rejection, keyed resource lowering, and unkeyed nested resource failures. req: convention/002 req: convention/003 req: convention/004 req: convention/005 req: convention/006 req: list/005 req: diagnostics/004 req: test/021
This commit is contained in:
@@ -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.
|
- [ ] **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.
|
- **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.
|
- **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 59 placement-diagnostic mutants with exact `InvalidData` paths, attributes, and guidance for navigation, boost containers, client modules, and SSE roots, plus their valid placements. The remaining package frontier starts at static attribute-value and keyed-scope diagnostics. 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 current `hemx-build` frontier now also mutation-proves all 112 static attribute-value mutants and all 21 keyed-scope ancestry mutants: exact `InvalidData` diagnostics cover missing/empty/invalid values, valid boundaries reach `AppBuilder::run`, nested keyed resources lower as keyed, and invalid/sibling scopes stay unkeyed or fail closed. The remaining package frontier starts at unknown/selector attributes and unkeyed-loop diagnostic formatting. 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
|
- **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. Elect and enforce the release license policy
|
||||||
|
|||||||
+218
-12
@@ -3639,6 +3639,18 @@ fn main() {{
|
|||||||
"data-hemx-handle=\"delete\"",
|
"data-hemx-handle=\"delete\"",
|
||||||
"ui::delete",
|
"ui::delete",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"nested-handle",
|
||||||
|
r#"<template h-for="todo in &self.todos"><template h-if="todo.visible"><button data-hemx-handle="delete">Delete</button></template></template>"#,
|
||||||
|
"data-hemx-handle=\"delete\"",
|
||||||
|
"ui::delete",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"atom",
|
||||||
|
r#"<template h-for="todo in &self.todos"><span data-hemx-atom="selected">Selected</span></template>"#,
|
||||||
|
"data-hemx-atom=\"selected\"",
|
||||||
|
"ui::selected",
|
||||||
|
),
|
||||||
] {
|
] {
|
||||||
let base = test_dir(&format!("hemx-build-unkeyed-for-{case}-test"));
|
let base = test_dir(&format!("hemx-build-unkeyed-for-{case}-test"));
|
||||||
let templates = base.join("templates");
|
let templates = base.join("templates");
|
||||||
@@ -3662,6 +3674,87 @@ fn main() {{
|
|||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&base);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let base = test_dir("hemx-build-keyed-nested-resources-test");
|
||||||
|
let templates = base.join("templates");
|
||||||
|
let out = base.join("out");
|
||||||
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
std::fs::create_dir_all(&templates).unwrap();
|
||||||
|
let keyed_source = r#"
|
||||||
|
<main>
|
||||||
|
<ul data-hemx-slot="todos">
|
||||||
|
<template h-for="todo in &self.todos" h-key="todo.id">
|
||||||
|
<li data-hemx-slot="todo_row">
|
||||||
|
<template h-for="child in &todo.children" h-key="child.id">
|
||||||
|
<button data-hemx-handle="delete">Delete</button>
|
||||||
|
<span data-hemx-atom="selected">Selected</span>
|
||||||
|
</template>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<template h-for="other in &self.others"><li>Unaddressed sibling</li></template>
|
||||||
|
</ul>
|
||||||
|
<aside data-hemx-slot="summary"></aside>
|
||||||
|
</main>
|
||||||
|
"#;
|
||||||
|
std::fs::write(templates.join("todo.heml"), keyed_source).unwrap();
|
||||||
|
app().template_dir(&templates).out_dir(&out).run().unwrap();
|
||||||
|
let syms = std::fs::read_to_string(out.join("hemx.syms")).unwrap();
|
||||||
|
assert!(syms.contains("slot\ttodo.heml::todos\ttodos\t"));
|
||||||
|
assert!(syms.contains("slot\ttodo.heml::todo_row\ttodo_row\t"));
|
||||||
|
assert!(syms.contains("handle\ttodo.heml::delete\tdelete\t"));
|
||||||
|
assert!(syms.contains("atom\ttodo.heml::selected\tselected\t"));
|
||||||
|
assert!(syms.contains("slot\ttodo.heml::summary\tsummary\t"));
|
||||||
|
let generated = std::fs::read_to_string(out.join("hemx.generated.rs")).unwrap();
|
||||||
|
assert!(generated.contains("pub const todo_row: ::hemx::advanced::KeyedSlot"));
|
||||||
|
assert!(generated.contains("pub const todos: ::hemx::advanced::KeyedSlot"));
|
||||||
|
assert!(generated.contains("pub const summary: ::hemx::advanced::Slot"));
|
||||||
|
assert!(!generated.contains("pub const summary: ::hemx::advanced::KeyedSlot"));
|
||||||
|
|
||||||
|
let surface =
|
||||||
|
surface_for_heml_source(Path::new("todo.heml"), keyed_source.to_owned()).unwrap();
|
||||||
|
let keyed_scope = surface
|
||||||
|
.scopes
|
||||||
|
.iter()
|
||||||
|
.position(|scope| {
|
||||||
|
matches!(
|
||||||
|
scope.kind,
|
||||||
|
ScopeKind::For {
|
||||||
|
key_expr: Some(_),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|index| ScopeId(index as u32))
|
||||||
|
.unwrap();
|
||||||
|
let nested_scope = surface
|
||||||
|
.scopes
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, scope)| scope.parent == Some(keyed_scope))
|
||||||
|
.map(|(index, _)| ScopeId(index as u32))
|
||||||
|
.unwrap();
|
||||||
|
let sibling_scope = surface
|
||||||
|
.scopes
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, scope)| {
|
||||||
|
matches!(scope.kind, ScopeKind::For { key_expr: None, .. })
|
||||||
|
&& scope.parent != Some(keyed_scope)
|
||||||
|
})
|
||||||
|
.map(|(index, _)| ScopeId(index as u32))
|
||||||
|
.unwrap();
|
||||||
|
assert!(is_descendant_scope(&surface, nested_scope, keyed_scope));
|
||||||
|
assert!(!is_descendant_scope(&surface, keyed_scope, keyed_scope));
|
||||||
|
assert!(!is_descendant_scope(&surface, sibling_scope, keyed_scope));
|
||||||
|
assert!(!is_descendant_scope(
|
||||||
|
&surface,
|
||||||
|
ScopeId(u32::MAX),
|
||||||
|
keyed_scope
|
||||||
|
));
|
||||||
|
assert!(is_inside_keyed_for(&surface, nested_scope));
|
||||||
|
assert!(!is_inside_keyed_for(&surface, sibling_scope));
|
||||||
|
assert!(!is_inside_keyed_for(&surface, ScopeId(u32::MAX)));
|
||||||
|
let _ = std::fs::remove_dir_all(base);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3757,6 +3850,12 @@ fn main() {{
|
|||||||
"data-hemx-policy",
|
"data-hemx-policy",
|
||||||
"latest",
|
"latest",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"policy-missing",
|
||||||
|
r#"<button data-hemx-handle="save" data-hemx-policy>Save</button>"#,
|
||||||
|
"data-hemx-policy",
|
||||||
|
"latest",
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"empty-client",
|
"empty-client",
|
||||||
r#"<button data-hemx-client="">Save</button>"#,
|
r#"<button data-hemx-client="">Save</button>"#,
|
||||||
@@ -3895,30 +3994,137 @@ fn main() {{
|
|||||||
let out = base.join("out");
|
let out = base.join("out");
|
||||||
let _ = std::fs::remove_dir_all(&base);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
std::fs::create_dir_all(&templates).unwrap();
|
std::fs::create_dir_all(&templates).unwrap();
|
||||||
std::fs::write(templates.join("invalid.heml"), template).unwrap();
|
let template_path = templates.join("invalid.heml");
|
||||||
|
std::fs::write(&template_path, template).unwrap();
|
||||||
|
|
||||||
let err = app()
|
let err = app()
|
||||||
.template_dir(&templates)
|
.template_dir(&templates)
|
||||||
.out_dir(&out)
|
.out_dir(&out)
|
||||||
.run()
|
.run()
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(
|
let surface = surface_for_heml_source(&template_path, template.to_owned()).unwrap();
|
||||||
err.to_string().contains(attr),
|
let value = surface
|
||||||
"missing attr in diagnostic: {err}"
|
.nodes
|
||||||
);
|
.iter()
|
||||||
assert!(
|
.flat_map(|node| &node.attrs)
|
||||||
err.to_string().contains("invalid"),
|
.find(|candidate| candidate.name == attr)
|
||||||
"missing invalid-value diagnostic: {err}"
|
.and_then(|candidate| candidate.value.as_deref())
|
||||||
);
|
.unwrap_or("");
|
||||||
assert!(
|
let expectation = match attr {
|
||||||
err.to_string().contains(guidance),
|
"data-hemx-policy" => {
|
||||||
"missing fix guidance {guidance:?}: {err}"
|
"expected one of `latest`, `queue`, `drop`, or `parallel`"
|
||||||
|
}
|
||||||
|
"data-hemx-history" => {
|
||||||
|
"expected `push`, `replace`, or empty for the default push behavior"
|
||||||
|
}
|
||||||
|
"data-hemx-client" => "expected a non-empty client handler name",
|
||||||
|
"data-hemx-client-policy" => "expected `latest` or `drop`",
|
||||||
|
"data-hemx-client-module" => {
|
||||||
|
"expected a same-origin module specifier beginning with `/`, `./`, or `../`"
|
||||||
|
}
|
||||||
|
"data-hemx-client-event" => "expected a runtime-supported event",
|
||||||
|
"data-hemx-client-state-version" => {
|
||||||
|
"expected a positive client state ABI version"
|
||||||
|
}
|
||||||
|
"data-hemx-on" => "expected runtime-supported events: `click`, `submit`, `input`, `change`, `keydown`, `dragstart`, `dragover`, or `drop`",
|
||||||
|
"data-hemx-confirm" => "expected a non-empty confirmation message",
|
||||||
|
"data-hemx-sse" => "expected a non-empty same-origin SSE URL",
|
||||||
|
"data-hemx-debounce" | "data-hemx-delay" | "data-hemx-throttle"
|
||||||
|
| "data-hemx-every" | "data-hemx-interval" => {
|
||||||
|
"expected milliseconds like `250`/`250ms` or seconds like `1s`"
|
||||||
|
}
|
||||||
|
_ => panic!("missing expectation for {attr}"),
|
||||||
|
};
|
||||||
|
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
||||||
|
assert_eq!(
|
||||||
|
err.to_string(),
|
||||||
|
format!(
|
||||||
|
"{}: invalid {attr} value `{value}`; {expectation}",
|
||||||
|
template_path.display()
|
||||||
|
),
|
||||||
|
"guidance anchor {guidance:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&base);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn static_attribute_value_validators_cover_every_boundary() {
|
||||||
|
for valid in ["latest", " queue ", "drop", "parallel"] {
|
||||||
|
assert!(valid_policy(valid), "rejected policy {valid:?}");
|
||||||
|
}
|
||||||
|
for invalid in ["", "newest", "latest queue", "LATEST"] {
|
||||||
|
assert!(!valid_policy(invalid), "accepted policy {invalid:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
for valid in ["/app.js", " ./app.js ", "../app.js"] {
|
||||||
|
assert!(valid_client_module(valid), "rejected module {valid:?}");
|
||||||
|
}
|
||||||
|
for invalid in [
|
||||||
|
"",
|
||||||
|
" ",
|
||||||
|
"//cdn/app.js",
|
||||||
|
"https://example.com/app.js",
|
||||||
|
"app.js",
|
||||||
|
] {
|
||||||
|
assert!(!valid_client_module(invalid), "accepted module {invalid:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
for valid in [
|
||||||
|
"click",
|
||||||
|
"submit input change keydown dragstart dragover drop",
|
||||||
|
" click change ",
|
||||||
|
] {
|
||||||
|
assert!(valid_event_list(valid), "rejected event list {valid:?}");
|
||||||
|
}
|
||||||
|
for invalid in ["", " ", "blur", "click blur"] {
|
||||||
|
assert!(
|
||||||
|
!valid_event_list(invalid),
|
||||||
|
"accepted event list {invalid:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for valid in [
|
||||||
|
"click",
|
||||||
|
"submit",
|
||||||
|
"input",
|
||||||
|
"change",
|
||||||
|
"keydown",
|
||||||
|
"dragstart",
|
||||||
|
"dragover",
|
||||||
|
"drop",
|
||||||
|
] {
|
||||||
|
assert!(valid_runtime_event(valid), "rejected event {valid:?}");
|
||||||
|
}
|
||||||
|
for invalid in ["", "blur", "Click"] {
|
||||||
|
assert!(!valid_runtime_event(invalid), "accepted event {invalid:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
for valid in ["0", "250", "250ms", "1s", " 5s "] {
|
||||||
|
assert!(valid_duration(valid), "rejected duration {valid:?}");
|
||||||
|
}
|
||||||
|
for invalid in ["", " ", "ms", "s", "-1", "1sec", "1.5s", "1 ms"] {
|
||||||
|
assert!(!valid_duration(invalid), "accepted duration {invalid:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let valid_source = r#"
|
||||||
|
<main data-hemx-root="app" data-hemx-client-module="/app.js" data-hemx-sse="/events">
|
||||||
|
<button data-hemx-handle="save" data-hemx-policy="latest" data-hemx-on="click change" data-hemx-confirm="Save?" data-hemx-delay="250ms" data-hemx-throttle="1s">Save</button>
|
||||||
|
<button data-hemx-client="save" data-hemx-client-event="click" data-hemx-client-policy="drop" data-hemx-client-state-version="1">Client</button>
|
||||||
|
<form method="get" action="/search" data-hemx-history="replace"><input name="q"></form>
|
||||||
|
</main>
|
||||||
|
"#;
|
||||||
|
let base = test_dir("hemx-build-valid-static-values");
|
||||||
|
let templates = base.join("templates");
|
||||||
|
let out = base.join("out");
|
||||||
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
std::fs::create_dir_all(&templates).unwrap();
|
||||||
|
std::fs::write(templates.join("valid.heml"), valid_source).unwrap();
|
||||||
|
app().template_dir(&templates).out_dir(&out).run().unwrap();
|
||||||
|
let _ = std::fs::remove_dir_all(base);
|
||||||
|
// test req: convention/002 req: convention/003 req: convention/004 req: convention/005 req: convention/006
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn placement_contracts_fail_closed_with_exact_diagnostics() {
|
fn placement_contracts_fail_closed_with_exact_diagnostics() {
|
||||||
// req: page_swap/001 req: page_swap/007 req: push/006 req: diagnostics/002
|
// req: page_swap/001 req: page_swap/007 req: push/006 req: diagnostics/002
|
||||||
|
|||||||
Reference in New Issue
Block a user