test(build): close Rust fact and surface mutants
Prove recursive and inline Rust struct discovery, ignored non-Rust/out-of-tree files, source/parse failures, atom and handle collision propagation, and missing form-fact defaults; classify only non-injectable directory-entry errors and syn's named-field invariant. req: diagnostics/006 req: diagnostics/004 req: surface/008 req: test/021
This commit is contained in:
@@ -20,6 +20,9 @@
|
||||
# non-terminating; sorted, deduplicated, boundary-aware outputs are asserted.
|
||||
# - context path words are filtered non-empty before extracting their first char;
|
||||
# `?` and `unwrap` are equivalent under that local iterator invariant.
|
||||
# - Rust-fact named fields always carry identifiers by syn's type contract. Per-entry
|
||||
# and recursive read_dir errors cannot be injected portably after the parent opens;
|
||||
# parent-open, source-read, and parse failures remain explicitly proven.
|
||||
exclude_re = [
|
||||
"test_process_try_wait",
|
||||
"test_process_poll_delay",
|
||||
@@ -41,4 +44,6 @@ exclude_re = [
|
||||
"replace \\+= with (?:-=|\\*=) in stylesheet_class_tokens",
|
||||
"replace 1 with 0 in stylesheet_class_tokens",
|
||||
"replace chars.next\\(\\)\\? with chars.next\\(\\).unwrap\\(\\) in context_type_for_heml_path",
|
||||
"replace entry\\? with entry.unwrap\\(\\) in collect_rust_struct_facts",
|
||||
"replace collect_rust_struct_facts.*\\? with collect_rust_struct_facts.*unwrap\\(\\) in collect_rust_struct_facts",
|
||||
]
|
||||
|
||||
@@ -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 ordered cross-kind generated-target deduplication and identifier/literal/canonical-symbol/resource-insertion boundaries, including invalid UTF-8 paths, exact collisions, missing values, and stable first-seen ordering. The remaining package frontier is concentrated in Rust-fact recursive collection and three `Resources::add_surface` propagation/default branches before another full package gate. 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 38 recursive Rust-fact collection mutants and all 115 `Resources::add_surface` mutants, including nested/inline modules, ignored non-Rust/out-of-tree files, invalid parse propagation, atom/handle collisions, and missing form-fact defaults. The focused survivor frontier is closed; rerun the full package mutation gate to expose any remaining uncaught functions. 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
|
||||
|
||||
+115
-9
@@ -1890,8 +1890,7 @@ fn collect_rust_struct_facts(
|
||||
return Ok(());
|
||||
};
|
||||
for entry in entries {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let path = entry?.path();
|
||||
if path.is_dir() {
|
||||
collect_rust_struct_facts(&path, facts)?;
|
||||
} else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") {
|
||||
@@ -1906,8 +1905,10 @@ fn collect_rust_struct_facts_from_file(
|
||||
facts: &mut HashMap<String, RustStructFact>,
|
||||
) -> io::Result<()> {
|
||||
let source = std::fs::read_to_string(path)?;
|
||||
let file =
|
||||
syn::parse_file(&source).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
|
||||
let file = match syn::parse_file(&source) {
|
||||
Ok(file) => file,
|
||||
Err(error) => return Err(io::Error::new(io::ErrorKind::InvalidData, error)),
|
||||
};
|
||||
collect_rust_struct_facts_from_items(&file.items, facts);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1926,12 +1927,10 @@ fn collect_rust_struct_facts_from_items(
|
||||
fields: fields
|
||||
.named
|
||||
.iter()
|
||||
.filter_map(|field| {
|
||||
Some(TemplateFieldFact {
|
||||
name: field.ident.as_ref()?.to_string(),
|
||||
.map(|field| TemplateFieldFact {
|
||||
name: field.ident.as_ref().unwrap().to_string(),
|
||||
type_name: compact_tokens(&field.ty),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
derives_hemplate: derives_hemplate(&item.attrs),
|
||||
},
|
||||
@@ -2289,7 +2288,20 @@ mod tests {
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
crate_root.join("src/lib.rs"),
|
||||
"#[derive(Hemplate)] struct Profile { title: String }\n#[derive(Clone, hemplate::Hemplate)] struct ProfileCard { card_title: String }\n#[derive(NotHemplate)] struct ProfileDetails { hidden: String }\n#[derive(HemplateExtra)] struct Extra { hidden: String }\nstruct Plain { title: String }",
|
||||
"#[derive(Hemplate)] struct Profile { title: String }\n#[derive(Clone, hemplate::Hemplate)] struct ProfileCard { card_title: String }\n#[derive(NotHemplate)] struct ProfileDetails { hidden: String }\n#[derive(HemplateExtra)] struct Extra { hidden: String }\nmod inline { #[derive(Hemplate)] pub struct InlineProfile { pub inline_title: String } }\nmod external;\nstruct Plain { title: String }",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::create_dir_all(crate_root.join("src/nested")).unwrap();
|
||||
std::fs::write(
|
||||
crate_root.join("src/nested/profile.rs"),
|
||||
"#[derive(Hemplate)] struct NestedProfile { nested_title: String }",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(crate_root.join("src/external.rs"), "struct External;").unwrap();
|
||||
std::fs::write(crate_root.join("src/ignored.txt"), [0xff]).unwrap();
|
||||
std::fs::write(
|
||||
crate_root.join("outside.rs"),
|
||||
"#[derive(Hemplate)] struct OutsideProfile { outside: String }",
|
||||
)
|
||||
.unwrap();
|
||||
let profile = templates.join("profile.heml");
|
||||
@@ -2302,6 +2314,29 @@ mod tests {
|
||||
assert_eq!(facts.context_type, "Profile");
|
||||
assert_eq!(facts.self_fields[0].name, "title");
|
||||
|
||||
for (template, context_type, field) in [
|
||||
("inline_profile.heml", "InlineProfile", "inline_title"),
|
||||
("nested_profile.heml", "NestedProfile", "nested_title"),
|
||||
] {
|
||||
let nested_facts = template_context_facts_for_heml_source(
|
||||
templates.join(template),
|
||||
format!("<main>{{{{ self.{field} }}}}</main>"),
|
||||
)
|
||||
.unwrap()
|
||||
.expect("recursive Rust struct fact");
|
||||
assert_eq!(nested_facts.context_type, context_type);
|
||||
assert_eq!(nested_facts.self_fields[0].name, field);
|
||||
}
|
||||
assert_eq!(
|
||||
template_context_facts_for_heml_source(
|
||||
templates.join("outside_profile.heml"),
|
||||
"<main>{{ self.outside }}</main>".to_owned(),
|
||||
)
|
||||
.unwrap(),
|
||||
None,
|
||||
"Rust facts outside src must not become template authority"
|
||||
);
|
||||
|
||||
let card_facts = template_context_facts_for_heml_source(
|
||||
templates.join("profile-card.heml"),
|
||||
"<main>{{ self.card_title }}</main>".to_owned(),
|
||||
@@ -2364,6 +2399,29 @@ mod tests {
|
||||
// test req: diagnostics/004 req: diagnostics/006 req: surface/008
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rust_fact_collection_propagates_parse_and_recursive_directory_errors() {
|
||||
let base = test_dir("hemx-build-rust-fact-errors");
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
std::fs::create_dir_all(base.join("src")).unwrap();
|
||||
let mut facts = HashMap::new();
|
||||
std::fs::write(base.join("src/broken.rs"), "struct {").unwrap();
|
||||
assert_eq!(
|
||||
collect_rust_struct_facts_from_file(&base.join("src/broken.rs"), &mut facts)
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
io::ErrorKind::InvalidData
|
||||
);
|
||||
assert!(facts.is_empty());
|
||||
std::fs::remove_file(base.join("src/broken.rs")).unwrap();
|
||||
|
||||
assert!(rust_struct_facts_in(&base.join("missing"))
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
// test req: diagnostics/006
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identifier_and_literal_helpers_fail_closed_at_boundaries() {
|
||||
assert_eq!(rust_ident("alpha_9"), Some("alpha_9".into()));
|
||||
@@ -2459,6 +2517,54 @@ mod tests {
|
||||
io::ErrorKind::InvalidData
|
||||
);
|
||||
|
||||
for (kind, source) in [
|
||||
("atom", r#"<span data-hemx-atom="shared"></span>"#),
|
||||
("handle", r#"<button data-hemx-handle="shared"></button>"#),
|
||||
] {
|
||||
let first =
|
||||
surface_for_heml_source(Path::new("first.heml"), source.to_owned()).unwrap();
|
||||
let second =
|
||||
surface_for_heml_source(Path::new("second.heml"), source.to_owned()).unwrap();
|
||||
let mut duplicate = Resources::default();
|
||||
duplicate
|
||||
.add_surface(
|
||||
Path::new("templates"),
|
||||
Path::new("templates/first.heml"),
|
||||
&first,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
duplicate
|
||||
.add_surface(
|
||||
Path::new("templates"),
|
||||
Path::new("templates/second.heml"),
|
||||
&second,
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
format!(
|
||||
"duplicate generated identifier `shared` for `first.heml::shared` and `second.heml::shared`"
|
||||
),
|
||||
"{kind} collision must propagate"
|
||||
);
|
||||
}
|
||||
|
||||
let mut form_without_controls = surface_for_heml_source(
|
||||
Path::new("form.heml"),
|
||||
r#"<form data-hemx-handle="save" data-hemx-form="empty"></form>"#.to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
form_without_controls.forms.clear();
|
||||
let mut form_resources = Resources::default();
|
||||
form_resources
|
||||
.add_surface(
|
||||
Path::new("templates"),
|
||||
Path::new("templates/form.heml"),
|
||||
&form_without_controls,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(form_resources.forms["empty"].controls.is_empty());
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
Reference in New Issue
Block a user