test(harness): close hemx-test mutation gate

Prove canonical wire inspection diagnostics and selector failures, classify only OS-process and invariant-only mutation seams, and make the package-native mutation command pass for the complete hemx-test crate.

req: test/001

req: test/019

req: test/020

req: test/021

req: test/022

req: wire/009
This commit is contained in:
slhx agent
2026-07-16 23:10:12 +02:00
parent 612a66725f
commit 2705099aed
4 changed files with 101 additions and 19 deletions
+20 -5
View File
@@ -4,7 +4,7 @@ use hemx_core::{
};
use std::io;
use std::net::TcpStream;
use std::process::{Child, Command, Stdio};
use std::process::{Child, Command, ExitStatus, Stdio};
use std::time::{Duration, Instant};
/// A child process owned by an integration test and proven ready over TCP.
@@ -16,6 +16,14 @@ pub struct TestProcess {
child: Child,
}
fn test_process_try_wait(child: &mut Child) -> io::Result<Option<ExitStatus>> {
child.try_wait()
}
fn test_process_poll_delay() {
std::thread::sleep(Duration::from_millis(25));
}
impl TestProcess {
pub fn start(
mut command: Command,
@@ -37,7 +45,7 @@ impl TestProcess {
if TcpStream::connect(addr).is_ok() {
return Ok(process);
}
if let Some(status) = process.child.try_wait()? {
if let Some(status) = test_process_try_wait(&mut process.child)? {
return Err(io::Error::other(format!(
"{label} exited with {status} before listening on {addr}"
)));
@@ -48,7 +56,7 @@ impl TestProcess {
format!("timed out after {timeout:?} waiting for {label} to listen on {addr}"),
));
}
std::thread::sleep(Duration::from_millis(25));
test_process_poll_delay();
}
}
}
@@ -68,8 +76,12 @@ where
inspect(handler(input))
}
fn inspection_fingerprint() -> BuildFingerprint {
BuildFingerprint(0)
}
pub fn inspect(effect: impl IntoEffect) -> EffectInspector {
inspect_batch(effect.into_batch(BuildFingerprint(0)))
inspect_batch(effect.into_batch(inspection_fingerprint()))
}
/// Inspect an already-dispatched batch without matching raw effect variants in tests.
@@ -81,7 +93,10 @@ pub fn inspect_batch(batch: EffectBatch) -> EffectInspector {
/// Decode and inspect an effect wire response without exposing `EffectBatch` in tests.
/// req: test/001 req: dx/006
pub fn inspect_wire(bytes: &[u8]) -> EffectInspector {
inspect_batch(EffectBatch::from_wire(bytes).expect("hemx effect wire response"))
inspect_batch(
EffectBatch::from_wire(bytes)
.unwrap_or_else(|error| panic!("invalid hemx effect wire response: {error:?}")),
)
}
/// Return the resource id behind a generated target for low-level test assertions.
+67 -12
View File
@@ -1,8 +1,22 @@
use hemx_core::{
Atom, Effect, Form, GeneratedTarget, KeyedSlot, NavigateMode, Payload, ResourceId,
ResourceKind, ResourceRef, ScopeKey, Slot,
Atom, BuildFingerprint, Effect, EffectBatch, Form, GeneratedTarget, KeyedSlot, NavigateMode,
Payload, ResourceId, ResourceKind, ResourceRef, ScopeKey, Slot,
};
fn panic_text<T>(result: std::thread::Result<T>) -> String {
let panic = match result {
Ok(_) => panic!("operation must panic"),
Err(panic) => panic,
};
if let Some(message) = panic.downcast_ref::<String>() {
message.clone()
} else if let Some(message) = panic.downcast_ref::<&str>() {
(*message).to_owned()
} else {
panic!("panic payload was not text")
}
}
#[test]
fn inspects_tuple_effects() {
let count = Slot::<u32>::new(1);
@@ -265,21 +279,62 @@ fn selector_helpers_validate_parts_and_cover_unscoped_variants() {
.is_err());
for invalid in ["", "two parts", ".class", "#id", "a>b", "a[b]"] {
assert!(std::panic::catch_unwind(|| hemx_test::class_selector(invalid)).is_err());
assert!(panic_text(std::panic::catch_unwind(|| {
hemx_test::class_selector(invalid)
}))
.contains("class selector part"));
}
for call in [
std::panic::catch_unwind(|| hemx_test::element_class_selector("bad tag", "ok")),
std::panic::catch_unwind(|| hemx_test::element_class_selector("span", "bad class")),
std::panic::catch_unwind(|| hemx_test::class_child_selector("bad parent", "li", "row")),
std::panic::catch_unwind(|| hemx_test::class_child_selector("list", "bad tag", "row")),
std::panic::catch_unwind(|| hemx_test::class_child_selector("list", "li", "bad class")),
std::panic::catch_unwind(|| hemx_test::class_descendant_selector("bad parent", "i")),
std::panic::catch_unwind(|| hemx_test::class_descendant_selector("note", "bad tag")),
for (call, label) in [
(
std::panic::catch_unwind(|| hemx_test::element_class_selector("bad tag", "ok")),
"element selector part",
),
(
std::panic::catch_unwind(|| hemx_test::element_class_selector("span", "bad class")),
"class selector part",
),
(
std::panic::catch_unwind(|| hemx_test::class_child_selector("bad parent", "li", "row")),
"parent class selector part",
),
(
std::panic::catch_unwind(|| hemx_test::class_child_selector("list", "bad tag", "row")),
"element selector part",
),
(
std::panic::catch_unwind(|| hemx_test::class_child_selector("list", "li", "bad class")),
"class selector part",
),
(
std::panic::catch_unwind(|| hemx_test::class_descendant_selector("bad parent", "i")),
"parent class selector part",
),
(
std::panic::catch_unwind(|| hemx_test::class_descendant_selector("note", "bad tag")),
"element selector part",
),
] {
assert!(call.is_err());
assert!(panic_text(call).contains(label));
}
}
#[test]
fn inspect_wire_reports_the_decode_failure_and_accepts_canonical_batches() {
let batch = EffectBatch {
abi_version: hemx_core::EFFECT_BATCH_ABI_VERSION,
fingerprint: BuildFingerprint(9),
ops: vec![Effect::Emit {
name: "saved".into(),
payload: "ok".into(),
}],
};
assert!(hemx_test::inspect_wire(&batch.to_wire()).emits("saved", "ok"));
let message = panic_text(std::panic::catch_unwind(|| hemx_test::inspect_wire(b"bad")));
assert!(message.contains("invalid hemx effect wire response: Truncated"));
// req: test/001 test req: wire/009 test
}
#[test]
fn builds_generated_handle_form_bodies() {
let handle = hemx_core::Handle::<()>::new(7);