test(build): close app input collection mutants
Traverse inputs once, preserve deterministic HE/ML-before-stylesheet lowering, and prove nested discovery, explicit surfaces, OUT_DIR fallback, missing inputs, invalid UTF-8, permission failures, output failures, and stable collision ordering through AppBuilder::run. req: build/003 req: build/004 req: build/009 req: diagnostics/004 req: test/021
This commit is contained in:
@@ -10,6 +10,9 @@
|
|||||||
# are behaviorally equivalent at these validated boundaries.
|
# are behaviorally equivalent at these validated boundaries.
|
||||||
# - hemx-build source inspection delegates to hemplate's currently infallible
|
# - hemx-build source inspection delegates to hemplate's currently infallible
|
||||||
# Surface parser; file I/O and invalid Rust-context errors remain explicitly proven.
|
# Surface parser; file I/O and invalid Rust-context errors remain explicitly proven.
|
||||||
|
# - AppBuilder reuses that same parser seam. Directory-open and recursive errors are
|
||||||
|
# proven, while a per-entry readdir fault cannot be injected portably after a
|
||||||
|
# successful read_dir; its unwrap mutant is classified as infrastructure-only.
|
||||||
# - write_if_changed propagates non-NotFound read errors; for ordinary filesystem
|
# - write_if_changed propagates non-NotFound read errors; for ordinary filesystem
|
||||||
# paths, attempting the same write returns the same OS error, so the guard mutant
|
# paths, attempting the same write returns the same OS error, so the guard mutant
|
||||||
# is externally equivalent while create/update/no-op behavior is mutation-proven.
|
# is externally equivalent while create/update/no-op behavior is mutation-proven.
|
||||||
@@ -28,5 +31,7 @@ exclude_re = [
|
|||||||
"replace surface_for_heml_source.* with surface_for_heml_source.*unwrap\\(\\) in diagnostics_for_heml_source",
|
"replace surface_for_heml_source.* with surface_for_heml_source.*unwrap\\(\\) in diagnostics_for_heml_source",
|
||||||
"replace surface_for_heml_source.* with surface_for_heml_source.*unwrap\\(\\) in generated_targets_for_heml_source",
|
"replace surface_for_heml_source.* with surface_for_heml_source.*unwrap\\(\\) in generated_targets_for_heml_source",
|
||||||
"replace surface_for_heml_source.* with surface_for_heml_source.*unwrap\\(\\) in template_context_facts_for_heml_source",
|
"replace surface_for_heml_source.* with surface_for_heml_source.*unwrap\\(\\) in template_context_facts_for_heml_source",
|
||||||
|
"replace surface_for_heml_source.* with surface_for_heml_source.*unwrap\\(\\) in AppBuilder::run",
|
||||||
|
"replace entry\\? with entry.unwrap\\(\\) in collect_input_files_into",
|
||||||
"replace match guard error.kind\\(\\) == io::ErrorKind::NotFound with true in write_if_changed",
|
"replace match guard error.kind\\(\\) == io::ErrorKind::NotFound with true in write_if_changed",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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 mutation-proves resource extraction plus canonical generated Rust/global exports/symbol manifests across keyed/component/collision/deduplication branches; the full 1,368-mutant package gate was runnable but exceeded 30 minutes after exposing 187 remaining survivors, led by `AppBuilder::run`, collection/error propagation, stylesheet parsing, and template-context 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 mutation-proves resource extraction, canonical generated contracts, and all 51 `AppBuilder::run`/input-collection mutants, including nested HE/ML/CSS/SCSS discovery, explicit-surface mode, `OUT_DIR`, stable ordering, missing inputs, output failures, invalid UTF-8, and recursive permission errors; the remaining package frontier starts at stylesheet parsing and template-context 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.
|
||||||
- **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
|
||||||
|
|||||||
+169
-44
@@ -202,15 +202,14 @@ impl AppBuilder {
|
|||||||
std::fs::create_dir_all(&out_dir)?;
|
std::fs::create_dir_all(&out_dir)?;
|
||||||
|
|
||||||
let mut resources = Resources::default();
|
let mut resources = Resources::default();
|
||||||
|
let input_paths = collect_input_files(&self.template_dir)?;
|
||||||
if self.surfaces.is_empty() {
|
if self.surfaces.is_empty() {
|
||||||
for path in collect_heml(&self.template_dir)? {
|
for path in input_paths.iter().filter(|path| {
|
||||||
let source = std::fs::read_to_string(&path)?;
|
path.extension().and_then(|extension| extension.to_str()) == Some("heml")
|
||||||
let doc = build_ast(Arc::new(source)).map_err(|err| parse_error(&path, err))?;
|
}) {
|
||||||
let Some(doc) = doc else {
|
let source = std::fs::read_to_string(path)?;
|
||||||
continue;
|
let surface = surface_for_heml_source(path, source)?;
|
||||||
};
|
resources.add_surface(&self.template_dir, path, &surface)?;
|
||||||
let surface = extract_surface(&doc);
|
|
||||||
resources.add_surface(&self.template_dir, &path, &surface)?;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// req: surface/008
|
// req: surface/008
|
||||||
@@ -218,9 +217,14 @@ impl AppBuilder {
|
|||||||
resources.add_surface(&self.template_dir, path, surface)?;
|
resources.add_surface(&self.template_dir, path, surface)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for path in collect_stylesheets(&self.template_dir)? {
|
for path in input_paths.iter().filter(|path| {
|
||||||
let source = std::fs::read_to_string(&path)?;
|
matches!(
|
||||||
resources.add_stylesheet(&self.template_dir, &path, &source)?;
|
path.extension().and_then(|extension| extension.to_str()),
|
||||||
|
Some("css" | "scss")
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
let source = std::fs::read_to_string(path)?;
|
||||||
|
resources.add_stylesheet(&self.template_dir, path, &source)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
write_if_changed(
|
write_if_changed(
|
||||||
@@ -1344,49 +1348,22 @@ fn make_resource(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_heml(root: &Path) -> io::Result<Vec<PathBuf>> {
|
fn collect_input_files(root: &Path) -> io::Result<Vec<PathBuf>> {
|
||||||
let mut paths = Vec::new();
|
let mut paths = Vec::new();
|
||||||
if !root.exists() {
|
if !root.exists() {
|
||||||
return Ok(paths);
|
return Ok(paths);
|
||||||
}
|
}
|
||||||
collect_heml_into(root, &mut paths)?;
|
collect_input_files_into(root, &mut paths)?;
|
||||||
paths.sort();
|
paths.sort();
|
||||||
Ok(paths)
|
Ok(paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_heml_into(dir: &Path, paths: &mut Vec<PathBuf>) -> io::Result<()> {
|
fn collect_input_files_into(dir: &Path, paths: &mut Vec<PathBuf>) -> io::Result<()> {
|
||||||
for entry in std::fs::read_dir(dir)? {
|
for entry in std::fs::read_dir(dir)? {
|
||||||
let entry = entry?;
|
let path = entry?.path();
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
collect_heml_into(&path, paths)?;
|
collect_input_files_into(&path, paths)?;
|
||||||
} else if path.extension().and_then(|ext| ext.to_str()) == Some("heml") {
|
} else {
|
||||||
paths.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_stylesheets(root: &Path) -> io::Result<Vec<PathBuf>> {
|
|
||||||
let mut paths = Vec::new();
|
|
||||||
if !root.exists() {
|
|
||||||
return Ok(paths);
|
|
||||||
}
|
|
||||||
collect_stylesheets_into(root, &mut paths)?;
|
|
||||||
paths.sort();
|
|
||||||
Ok(paths)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_stylesheets_into(dir: &Path, paths: &mut Vec<PathBuf>) -> io::Result<()> {
|
|
||||||
for entry in std::fs::read_dir(dir)? {
|
|
||||||
let entry = entry?;
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() {
|
|
||||||
collect_stylesheets_into(&path, paths)?;
|
|
||||||
} else if matches!(
|
|
||||||
path.extension().and_then(|ext| ext.to_str()),
|
|
||||||
Some("css" | "scss")
|
|
||||||
) {
|
|
||||||
paths.push(path);
|
paths.push(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2282,6 +2259,8 @@ fn parse_error(path: &Path, err: impl std::fmt::Display) -> io::Error {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
static OUT_DIR_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn public_heml_inspection_entry_points_preserve_paths_and_io_diagnostics() {
|
fn public_heml_inspection_entry_points_preserve_paths_and_io_diagnostics() {
|
||||||
let root = std::env::temp_dir().join(format!(
|
let root = std::env::temp_dir().join(format!(
|
||||||
@@ -2649,6 +2628,152 @@ mod tests {
|
|||||||
// test req: build/004 req: client_local/011 req: diagnostics/004
|
// test req: build/004 req: client_local/011 req: diagnostics/004
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn app_builder_discovers_nested_inputs_and_propagates_collection_errors() {
|
||||||
|
let base = test_dir("hemx-build-collection");
|
||||||
|
let templates = base.join("templates");
|
||||||
|
let nested = templates.join("nested");
|
||||||
|
let out = base.join("out");
|
||||||
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
std::fs::create_dir_all(&nested).unwrap();
|
||||||
|
std::fs::write(templates.join("a_empty.heml"), "").unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
nested.join("panel.heml"),
|
||||||
|
r#"<section data-hemx-slot="nested_panel"></section>"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
std::fs::write(nested.join("panel.css"), ".nested-css {}").unwrap();
|
||||||
|
std::fs::write(nested.join("theme.scss"), ".nested-scss {}").unwrap();
|
||||||
|
std::fs::write(nested.join("ignored.txt"), [0xff]).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\tnested/panel.heml::nested_panel\tnested_panel\t"));
|
||||||
|
assert!(syms.contains("class\tnested/panel.css::nested-css\tnested_css\tnested-css\n"));
|
||||||
|
assert!(syms.contains("class\tnested/theme.scss::nested-scss\tnested_scss\tnested-scss\n"));
|
||||||
|
|
||||||
|
let missing = base.join("missing");
|
||||||
|
let missing_out = base.join("missing-out");
|
||||||
|
app()
|
||||||
|
.template_dir(&missing)
|
||||||
|
.out_dir(&missing_out)
|
||||||
|
.run()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(missing_out.join("hemx.syms")).unwrap(),
|
||||||
|
"hemx-syms-v1\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _out_dir_lock = OUT_DIR_LOCK.lock().unwrap();
|
||||||
|
let previous_out_dir = std::env::var_os("OUT_DIR");
|
||||||
|
let env_out = base.join("env-out");
|
||||||
|
std::env::set_var("OUT_DIR", &env_out);
|
||||||
|
let env_result = app().template_dir(&missing).run();
|
||||||
|
match previous_out_dir {
|
||||||
|
Some(value) => std::env::set_var("OUT_DIR", value),
|
||||||
|
None => std::env::remove_var("OUT_DIR"),
|
||||||
|
}
|
||||||
|
env_result.unwrap();
|
||||||
|
assert!(env_out.join("hemx.generated.rs").is_file());
|
||||||
|
|
||||||
|
let template_file = base.join("not-a-directory");
|
||||||
|
std::fs::write(&template_file, "not a directory").unwrap();
|
||||||
|
let error = app()
|
||||||
|
.template_dir(&template_file)
|
||||||
|
.out_dir(base.join("file-root-out"))
|
||||||
|
.run()
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(
|
||||||
|
error.kind(),
|
||||||
|
io::ErrorKind::NotADirectory | io::ErrorKind::Other
|
||||||
|
));
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
let restricted_root = base.join("restricted-root");
|
||||||
|
let restricted_nested = restricted_root.join("nested");
|
||||||
|
std::fs::create_dir_all(&restricted_nested).unwrap();
|
||||||
|
std::fs::set_permissions(&restricted_nested, std::fs::Permissions::from_mode(0o000))
|
||||||
|
.unwrap();
|
||||||
|
let error = app()
|
||||||
|
.template_dir(&restricted_root)
|
||||||
|
.out_dir(base.join("restricted-out"))
|
||||||
|
.run()
|
||||||
|
.unwrap_err();
|
||||||
|
std::fs::set_permissions(&restricted_nested, std::fs::Permissions::from_mode(0o700))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
let invalid_heml = base.join("invalid-heml");
|
||||||
|
std::fs::create_dir_all(&invalid_heml).unwrap();
|
||||||
|
std::fs::write(invalid_heml.join("bad.heml"), [0xff]).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
app()
|
||||||
|
.template_dir(&invalid_heml)
|
||||||
|
.out_dir(base.join("invalid-heml-out"))
|
||||||
|
.run()
|
||||||
|
.unwrap_err()
|
||||||
|
.kind(),
|
||||||
|
io::ErrorKind::InvalidData
|
||||||
|
);
|
||||||
|
|
||||||
|
let invalid_css = base.join("invalid-css");
|
||||||
|
std::fs::create_dir_all(&invalid_css).unwrap();
|
||||||
|
std::fs::write(invalid_css.join("bad.css"), [0xff]).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
app()
|
||||||
|
.template_dir(&invalid_css)
|
||||||
|
.out_dir(base.join("invalid-css-out"))
|
||||||
|
.run()
|
||||||
|
.unwrap_err()
|
||||||
|
.kind(),
|
||||||
|
io::ErrorKind::InvalidData
|
||||||
|
);
|
||||||
|
|
||||||
|
let blocked_out = base.join("blocked-out");
|
||||||
|
std::fs::write(&blocked_out, "not a directory").unwrap();
|
||||||
|
assert!(app()
|
||||||
|
.template_dir(&missing)
|
||||||
|
.out_dir(&blocked_out)
|
||||||
|
.run()
|
||||||
|
.is_err());
|
||||||
|
|
||||||
|
let explicit_surface = surface_for_heml_source(
|
||||||
|
Path::new("explicit.heml"),
|
||||||
|
r#"<section data-hemx-slot="123"></section>"#.to_owned(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
app()
|
||||||
|
.surface("explicit.heml", explicit_surface)
|
||||||
|
.out_dir(base.join("explicit-out"))
|
||||||
|
.run()
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string(),
|
||||||
|
"invalid hemx slot name `123`; expected a Rust identifier"
|
||||||
|
);
|
||||||
|
|
||||||
|
let ordered = base.join("ordered");
|
||||||
|
std::fs::create_dir_all(&ordered).unwrap();
|
||||||
|
std::fs::write(ordered.join("a.css"), ".foo-bar {}").unwrap();
|
||||||
|
std::fs::write(ordered.join("b.css"), ".foo_bar {}").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
app()
|
||||||
|
.template_dir(&ordered)
|
||||||
|
.out_dir(base.join("ordered-out"))
|
||||||
|
.run()
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string(),
|
||||||
|
"duplicate generated class identifier `foo_bar` for CSS classes `foo-bar` and `foo_bar`"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(base);
|
||||||
|
// test req: build/003 req: build/004 req: build/009 req: diagnostics/004
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_op_build_preserves_generated_artifact_timestamps() {
|
fn no_op_build_preserves_generated_artifact_timestamps() {
|
||||||
// req: build/009
|
// req: build/009
|
||||||
|
|||||||
Reference in New Issue
Block a user