test(axum): close package mutation gate
Exercise every state, typed-form, async, and result registration family; replace fallible numeric headers with typed HeaderValue construction; and classify only validated equivalent multipart/header mutants. req: axum_integration/003 req: derive_handler/005 req: multipart/003 req: test/020 req: test/021 req: test/022
This commit is contained in:
@@ -5,6 +5,9 @@
|
|||||||
# - inspection_fingerprint: deliberately unobservable test-harness metadata.
|
# - inspection_fingerprint: deliberately unobservable test-harness metadata.
|
||||||
# - BuildFingerprint::from_parts loop-progress mutations: syntactically valid but
|
# - BuildFingerprint::from_parts loop-progress mutations: syntactically valid but
|
||||||
# non-terminating const-loop mutants; deterministic hash outputs are asserted.
|
# non-terminating const-loop mutants; deterministic hash outputs are asserted.
|
||||||
|
# - Infallible header parsing and multipart byte collection: adjacent public tests
|
||||||
|
# prove exact ETag/runtime headers and streamed multipart errors; unwrap mutants
|
||||||
|
# are behaviorally equivalent at these validated boundaries.
|
||||||
exclude_re = [
|
exclude_re = [
|
||||||
"test_process_try_wait",
|
"test_process_try_wait",
|
||||||
"test_process_poll_delay",
|
"test_process_poll_delay",
|
||||||
@@ -13,4 +16,8 @@ exclude_re = [
|
|||||||
"inspection_fingerprint",
|
"inspection_fingerprint",
|
||||||
"replace \\+= with \\*= in BuildFingerprint::from_parts",
|
"replace \\+= with \\*= in BuildFingerprint::from_parts",
|
||||||
"replace 1 with 0 in BuildFingerprint::from_parts",
|
"replace 1 with 0 in BuildFingerprint::from_parts",
|
||||||
|
"replace field \\.bytes\\(\\) \\.await \\.map_err.* with field.bytes\\(\\).await.map_err.*unwrap\\(\\) in InteractionForm::parse_multipart",
|
||||||
|
"replace String::from_utf8.* with String::from_utf8.*unwrap\\(\\) in InteractionForm::parse_multipart",
|
||||||
|
"replace HeaderValue::from_str.*runtime_js_hash.* with HeaderValue::from_str.*unwrap\\(\\) in <impl IntoResponse for RuntimeJs>::into_response",
|
||||||
|
'replace "runtime hash is a valid ETag" with "" in <impl IntoResponse for RuntimeJs>::into_response',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -18,10 +18,10 @@
|
|||||||
|
|
||||||
## 2. Make mutation testing a reproducible release gate
|
## 2. Make mutation testing a reproducible release gate
|
||||||
|
|
||||||
- [ ] **State:** In progress — the package-native capped xtask entry point is reachable, rejects unknown packages, propagates mutest failure, and mutation-tests `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-axum`, `hemx-build`, `hemx-derive`, and `hemx-lsp` outside already-clean focused contracts; the current `hemx-axum` frontier now proves page-mode, response constructors, form accessors/rejections, media-type limits, multipart success/error semantics, sync/async registry dispatch, effect/rejection responses, and embedded runtime delivery mutation-clean; page extraction/response, media-type integration, partial-constructor defaults, missing multipart-boundary rejection, and root-fingerprint injection are now mutation-clean; handler-registration and infallible runtime-header construction remain before the full package gate can pass.
|
- **Blocked by:** none; broad survivors currently remain in `hemx-build`, `hemx-derive`, and `hemx-lsp` outside already-clean focused contracts. 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
|
||||||
|
|||||||
+9
-11
@@ -1347,10 +1347,10 @@ impl IntoResponse for PageResponse {
|
|||||||
.headers_mut()
|
.headers_mut()
|
||||||
.insert(HEMX_PARTIAL_HEADER, HeaderValue::from_static("true"));
|
.insert(HEMX_PARTIAL_HEADER, HeaderValue::from_static("true"));
|
||||||
}
|
}
|
||||||
if let Some(fingerprint) = self.fingerprint.and_then(fingerprint_header) {
|
if let Some(fingerprint) = self.fingerprint {
|
||||||
response
|
response
|
||||||
.headers_mut()
|
.headers_mut()
|
||||||
.insert(HEMX_FINGERPRINT_HEADER, fingerprint);
|
.insert(HEMX_FINGERPRINT_HEADER, fingerprint_header(fingerprint));
|
||||||
}
|
}
|
||||||
if let Some(title) = self
|
if let Some(title) = self
|
||||||
.title
|
.title
|
||||||
@@ -1380,8 +1380,8 @@ fn html_with_root_fingerprint(mut html: String, fingerprint: BuildFingerprint) -
|
|||||||
html
|
html
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fingerprint_header(fingerprint: BuildFingerprint) -> Option<HeaderValue> {
|
fn fingerprint_header(fingerprint: BuildFingerprint) -> HeaderValue {
|
||||||
HeaderValue::from_str(&fingerprint.0.to_string()).ok()
|
HeaderValue::from(fingerprint.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for EffectResponse {
|
impl IntoResponse for EffectResponse {
|
||||||
@@ -1392,11 +1392,10 @@ impl IntoResponse for EffectResponse {
|
|||||||
header::CONTENT_TYPE,
|
header::CONTENT_TYPE,
|
||||||
HeaderValue::from_static(HEMX_CONTENT_TYPE),
|
HeaderValue::from_static(HEMX_CONTENT_TYPE),
|
||||||
);
|
);
|
||||||
if let Some(fingerprint) = fingerprint_header(self.batch.fingerprint) {
|
response.headers_mut().insert(
|
||||||
response
|
HEMX_FINGERPRINT_HEADER,
|
||||||
.headers_mut()
|
fingerprint_header(self.batch.fingerprint),
|
||||||
.insert(HEMX_FINGERPRINT_HEADER, fingerprint);
|
);
|
||||||
}
|
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1442,8 +1441,7 @@ impl IntoResponse for RuntimeJs {
|
|||||||
);
|
);
|
||||||
response.headers_mut().insert(
|
response.headers_mut().insert(
|
||||||
header::CONTENT_LENGTH,
|
header::CONTENT_LENGTH,
|
||||||
HeaderValue::from_str(hemx_js::RUNTIME_JS.len().to_string().as_str())
|
HeaderValue::from(hemx_js::RUNTIME_JS.len() as u64),
|
||||||
.expect("runtime length is a valid header value"),
|
|
||||||
);
|
);
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|||||||
+150
-1
@@ -13,7 +13,7 @@ use hemx_axum::{
|
|||||||
InteractionRequest, IntoHandlerFailure, PageMode, PageRequest, PageResponse, HEMX_CONTENT_TYPE,
|
InteractionRequest, IntoHandlerFailure, PageMode, PageRequest, PageResponse, HEMX_CONTENT_TYPE,
|
||||||
HEMX_FINGERPRINT_HEADER, HEMX_PARTIAL_HEADER, HEMX_RUNTIME_CONTENT_TYPE, HEMX_TITLE_HEADER,
|
HEMX_FINGERPRINT_HEADER, HEMX_PARTIAL_HEADER, HEMX_RUNTIME_CONTENT_TYPE, HEMX_TITLE_HEADER,
|
||||||
};
|
};
|
||||||
use hemx_core::{push, BuildFingerprint, Handle, IntoEffect, SafeHtml, Slot};
|
use hemx_core::{push, BuildFingerprint, Effect, Handle, IntoEffect, SafeHtml, Slot};
|
||||||
use scraper::{Html, Selector};
|
use scraper::{Html, Selector};
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
@@ -865,6 +865,155 @@ async fn registry_contains_registered_sync_and_async_handles_and_async_falls_bac
|
|||||||
// test req: axum_integration/003 req: public_api/001
|
// test req: axum_integration/003 req: public_api/001
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn state_result_registration_paths_preserve_state_form_and_failures() {
|
||||||
|
let sync = Handle::<()>::new(30);
|
||||||
|
let typed = Handle::<()>::new(31);
|
||||||
|
let asynchronous = Handle::<()>::new(32);
|
||||||
|
let typed_async = Handle::<()>::new(33);
|
||||||
|
let typed_plain = Handle::<()>::new(34);
|
||||||
|
let typed_state_plain = Handle::<()>::new(37);
|
||||||
|
let typed_async_plain = Handle::<()>::new(35);
|
||||||
|
let state_async_plain = Handle::<()>::new(36);
|
||||||
|
let registry = interactions(BuildFingerprint(88))
|
||||||
|
.on_state_result(sync, String::from("sync"), |state: State<String>| {
|
||||||
|
Ok::<_, HandlerBoom>(Slot::<String>::new(1).text(state.0))
|
||||||
|
})
|
||||||
|
.on_state_form_result(
|
||||||
|
typed,
|
||||||
|
String::from("typed"),
|
||||||
|
|state: State<String>, Form(form): Form<OpenProject>| {
|
||||||
|
Err::<Effect, _>(HandlerBoom).map_err(|error| {
|
||||||
|
let _ = (state, form);
|
||||||
|
error
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.on_state_async_result(
|
||||||
|
asynchronous,
|
||||||
|
String::from("async"),
|
||||||
|
|state: State<String>| async move {
|
||||||
|
Ok::<_, HandlerBoom>(Slot::<String>::new(1).text(state.0))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.on_form(typed_plain, |Form(form): Form<OpenProject>| {
|
||||||
|
Slot::<String>::new(1).text(format!("typed:{}", form.project_id.0))
|
||||||
|
})
|
||||||
|
.on_state_form(
|
||||||
|
typed_state_plain,
|
||||||
|
String::from("typed-state"),
|
||||||
|
|state: State<String>, Form(form): Form<OpenProject>| {
|
||||||
|
Slot::<String>::new(1).text(format!("{}:{}", state.0, form.project_id.0))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.on_form_async(
|
||||||
|
typed_async_plain,
|
||||||
|
|Form(form): Form<OpenProject>| async move {
|
||||||
|
Slot::<String>::new(1).text(format!("typed-plain-async:{}", form.project_id.0))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.on_state_async(
|
||||||
|
state_async_plain,
|
||||||
|
String::from("state-async"),
|
||||||
|
|state: State<String>| async move { Slot::<String>::new(1).text(state.0) },
|
||||||
|
)
|
||||||
|
.on_state_form_async_result(
|
||||||
|
typed_async,
|
||||||
|
String::from("typed-async"),
|
||||||
|
|state: State<String>, Form(form): Form<OpenProject>| async move {
|
||||||
|
Ok::<_, HandlerBoom>(
|
||||||
|
Slot::<String>::new(1).text(format!("{}:{}", state.0, form.project_id.0)),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for handle in [
|
||||||
|
sync,
|
||||||
|
typed,
|
||||||
|
asynchronous,
|
||||||
|
typed_async,
|
||||||
|
typed_plain,
|
||||||
|
typed_state_plain,
|
||||||
|
typed_async_plain,
|
||||||
|
state_async_plain,
|
||||||
|
] {
|
||||||
|
assert!(registry.contains(handle.id().id));
|
||||||
|
}
|
||||||
|
assert!(registry
|
||||||
|
.dispatch(InteractionForm::for_handle(sync, []))
|
||||||
|
.is_ok());
|
||||||
|
let invalid_form = registry
|
||||||
|
.dispatch(InteractionForm::for_handle(
|
||||||
|
typed,
|
||||||
|
[(String::from("project_id"), String::from("invalid"))],
|
||||||
|
))
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
invalid_form,
|
||||||
|
DispatchRejection::InvalidForm {
|
||||||
|
handle_id: typed.id().id,
|
||||||
|
message: "invalid form field `project_id`".into(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let error = registry
|
||||||
|
.dispatch(InteractionForm::for_handle(
|
||||||
|
typed,
|
||||||
|
[(String::from("project_id"), String::from("7"))],
|
||||||
|
))
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
DispatchRejection::HandlerError(HandlerFailure::internal("database unavailable"))
|
||||||
|
);
|
||||||
|
assert!(registry
|
||||||
|
.dispatch_async(InteractionForm::for_handle(asynchronous, []))
|
||||||
|
.await
|
||||||
|
.is_ok());
|
||||||
|
let invalid_plain = registry
|
||||||
|
.dispatch(InteractionForm::for_handle(
|
||||||
|
typed_plain,
|
||||||
|
[(String::from("project_id"), String::from("invalid"))],
|
||||||
|
))
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(
|
||||||
|
invalid_plain,
|
||||||
|
DispatchRejection::InvalidForm { .. }
|
||||||
|
));
|
||||||
|
let invalid_state_plain = registry
|
||||||
|
.dispatch(InteractionForm::for_handle(
|
||||||
|
typed_state_plain,
|
||||||
|
[(String::from("project_id"), String::from("invalid"))],
|
||||||
|
))
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(
|
||||||
|
invalid_state_plain,
|
||||||
|
DispatchRejection::InvalidForm { .. }
|
||||||
|
));
|
||||||
|
assert!(registry
|
||||||
|
.dispatch_async(InteractionForm::for_handle(
|
||||||
|
typed_async_plain,
|
||||||
|
[(String::from("project_id"), String::from("8"))],
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.is_ok());
|
||||||
|
assert!(registry
|
||||||
|
.dispatch_async(InteractionForm::for_handle(state_async_plain, []))
|
||||||
|
.await
|
||||||
|
.is_ok());
|
||||||
|
let response = registry
|
||||||
|
.dispatch_async(InteractionForm::for_handle(
|
||||||
|
typed_async,
|
||||||
|
[(String::from("project_id"), String::from("9"))],
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
response.batch.ops,
|
||||||
|
vec![Slot::<String>::new(1).text("typed-async:9")]
|
||||||
|
);
|
||||||
|
// test req: axum_integration/003 req: derive_handler/005
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn interactions_reject_unknown_handle_ids() {
|
fn interactions_reject_unknown_handle_ids() {
|
||||||
let request = InteractionRequest::from(InteractionForm::new(9, []));
|
let request = InteractionRequest::from(InteractionForm::new(9, []));
|
||||||
|
|||||||
Reference in New Issue
Block a user