test(derive): close first mutation shard
Create shard output parents, exercise exact handler placement/signature/codegen contracts, preserve generated-file and form-bound diagnostics, and fail closed on malformed form symbol manifests. Public shard 1/8 passes through both 16-way partitions. req: derive_handler/001 req: client_local/001 req: form/004 req: diagnostics/003 req: test/022 req: test/023
This commit is contained in:
@@ -21,7 +21,7 @@
|
|||||||
- [ ] **State:** In progress — the package-native capped xtask entry point is reachable, rejects unknown packages and invalid shards, propagates mutest failure, and mutation-tests `hemx-axum`, `hemx-build`, `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 and invalid shards, propagates mutest failure, and mutation-tests `hemx-axum`, `hemx-build`, `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-derive` and `hemx-lsp` outside already-clean focused contracts. The mutation entry point accepts validated one-based `SHARD/TOTAL` operands, maps them to native zero-based shards, uses shard-specific output directories, grants repo-owned compiler probes a 120-second floor, and preserves the unsharded gate. All eight deterministic `hemx-build` shards now pass: 1,304 mutants total, 1,070 caught and 234 unviable, including final shard `8/8` with 159 mutants (139 caught, 20 unviable). The complete 470-mutant `hemx-axum` package gate 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-derive` and `hemx-lsp` outside already-clean focused contracts. The xtask mutation runner now creates shard output parents before invoking mutest, fixing first-use failure for newly sharded packages. `hemx-derive` public shard `1/8` is clean through deterministic partitions `1/16` (31 mutants: 24 caught, 7 unviable) and `9/16` (31 mutants: 28 caught, 3 unviable) after exact client-handler mode/signature/codegen, generated-file diagnostics, form generic bounds, and malformed symbol-manifest coverage; shards `2/8` through `8/8` remain. The mutation entry point accepts validated one-based `SHARD/TOTAL` operands, maps them to native zero-based shards, uses shard-specific output directories, grants repo-owned compiler probes a 120-second floor, and preserves the unsharded gate. All eight deterministic `hemx-build` shards now pass: 1,304 mutants total, 1,070 caught and 234 unviable, including final shard `8/8` with 159 mutants (139 caught, 20 unviable). The complete 470-mutant `hemx-axum` package gate 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
|
||||||
|
|||||||
+271
-65
@@ -12,37 +12,23 @@ use syn::{
|
|||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
let placement = attr.to_string();
|
let placement = attr.to_string();
|
||||||
let is_client = match placement.as_str() {
|
let placement = match handler_placement(&placement) {
|
||||||
"" => false,
|
Ok(placement) => placement,
|
||||||
"client" => true,
|
Err(error) => return error.into_compile_error().into(),
|
||||||
_ => {
|
|
||||||
return syn::Error::new(
|
|
||||||
proc_macro2::Span::call_site(),
|
|
||||||
"unsupported hemx handler placement; expected #[hemx::handler] or #[hemx::handler(client)]",
|
|
||||||
)
|
|
||||||
.into_compile_error()
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let function = parse_macro_input!(item as ItemFn);
|
let function = parse_macro_input!(item as ItemFn);
|
||||||
let name = function.sig.ident.to_string();
|
let name = function.sig.ident.to_string();
|
||||||
|
|
||||||
let Some(syms_path) = syms_path() else {
|
let syms_path = match handler_syms_path(syms_path()) {
|
||||||
let message = "#[hemx::handler] requires generated hemx files; add hemx_build::app().run()? to build.rs or run inside a Cargo crate";
|
Ok(path) => path,
|
||||||
return quote!(
|
Err(message) => {
|
||||||
#function
|
return quote!(
|
||||||
compile_error!(#message);
|
#function
|
||||||
)
|
compile_error!(#message);
|
||||||
.into();
|
)
|
||||||
|
.into();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if !syms_path.exists() {
|
|
||||||
let message = "#[hemx::handler] could not find generated hemx symbols; add hemx_build::app().run()? to build.rs or check template generation";
|
|
||||||
return quote!(
|
|
||||||
#function
|
|
||||||
compile_error!(#message);
|
|
||||||
)
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
if !syms_contains_handle(&syms_path, &name) {
|
if !syms_contains_handle(&syms_path, &name) {
|
||||||
let message = format!(
|
let message = format!(
|
||||||
"unknown hemx handle `{name}`; add `data-hemx-handle=\"{name}\"` to a template or rename this handler"
|
"unknown hemx handle `{name}`; add `data-hemx-handle=\"{name}\"` to a template or rename this handler"
|
||||||
@@ -86,33 +72,34 @@ pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
.into();
|
.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
if !is_client {
|
expand_handler_function(function, placement).into()
|
||||||
return quote!(#function).into();
|
}
|
||||||
}
|
|
||||||
let input_count = function.sig.inputs.len();
|
fn expand_handler_function(
|
||||||
if !matches!(input_count, 0 | 2)
|
function: ItemFn,
|
||||||
|| function.sig.asyncness.is_some()
|
placement: HandlerPlacement,
|
||||||
|| function.sig.unsafety.is_some()
|
) -> proc_macro2::TokenStream {
|
||||||
|| function.sig.constness.is_some()
|
if placement == HandlerPlacement::Server {
|
||||||
|| !function.sig.generics.params.is_empty()
|
return quote!(#function);
|
||||||
{
|
|
||||||
let message = format!(
|
|
||||||
"client-local hemx handler `{name}` must be safe, synchronous, non-generic, and accept either no parameters or `(hemx::wasm::ClientEvent, hemx::wasm::ClientState)`"
|
|
||||||
);
|
|
||||||
return quote!(
|
|
||||||
#function
|
|
||||||
compile_error!(#message);
|
|
||||||
)
|
|
||||||
.into();
|
|
||||||
}
|
}
|
||||||
|
let has_inputs = match client_handler_has_inputs(&function) {
|
||||||
|
Ok(has_inputs) => has_inputs,
|
||||||
|
Err(error) => {
|
||||||
|
let message = error.to_string();
|
||||||
|
return quote!(
|
||||||
|
#function
|
||||||
|
compile_error!(#message);
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let function_name = &function.sig.ident;
|
let function_name = &function.sig.ident;
|
||||||
let export_name = format_ident!("__hemx_client_{function_name}");
|
let export_name = format_ident!("__hemx_client_{function_name}");
|
||||||
let export_module = format_ident!("__hemx_client_export_{function_name}");
|
let export_module = format_ident!("__hemx_client_export_{function_name}");
|
||||||
let invoke_handler = if input_count == 0 {
|
let invoke_handler = if has_inputs {
|
||||||
quote!(super::#function_name())
|
|
||||||
} else {
|
|
||||||
quote!(super::#function_name(event, state))
|
quote!(super::#function_name(event, state))
|
||||||
|
} else {
|
||||||
|
quote!(super::#function_name())
|
||||||
};
|
};
|
||||||
quote!(
|
quote!(
|
||||||
#function
|
#function
|
||||||
@@ -149,7 +136,6 @@ pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
@@ -162,7 +148,7 @@ pub fn form(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
let form_name = parse_macro_input!(attr as LitStr).value();
|
let form_name = parse_macro_input!(attr as LitStr).value();
|
||||||
let form_struct = parse_macro_input!(item as ItemStruct);
|
let form_struct = parse_macro_input!(item as ItemStruct);
|
||||||
let Some(syms_path) = syms_path() else {
|
let Some(syms_path) = syms_path() else {
|
||||||
let message = "#[hemx::form] requires generated hemx files; add hemx_build::app().run()? to build.rs or run inside a Cargo crate";
|
let message = missing_form_generated_files_message();
|
||||||
return quote!(
|
return quote!(
|
||||||
#form_struct
|
#form_struct
|
||||||
compile_error!(#message);
|
compile_error!(#message);
|
||||||
@@ -174,13 +160,7 @@ pub fn form(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
let ident = &form_struct.ident;
|
let ident = &form_struct.ident;
|
||||||
let resource_id =
|
let resource_id =
|
||||||
form_resource_id(&syms_path, &form_name).expect("checked form exists in hemx.syms");
|
form_resource_id(&syms_path, &form_name).expect("checked form exists in hemx.syms");
|
||||||
let mut generics = form_struct.generics.clone();
|
let generics = form_impl_generics(&form_struct);
|
||||||
for ty in form_parser_types(&form_struct) {
|
|
||||||
generics
|
|
||||||
.make_where_clause()
|
|
||||||
.predicates
|
|
||||||
.push(parse_quote!(#ty: ::hemx::FormValue));
|
|
||||||
}
|
|
||||||
let decode_fields = form_decode_fields(&syms_path, &form_name, &form_struct);
|
let decode_fields = form_decode_fields(&syms_path, &form_name, &form_struct);
|
||||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||||
quote!(
|
quote!(
|
||||||
@@ -201,7 +181,7 @@ pub fn form(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
let message = errors.join("; ");
|
let message = join_contract_errors(&errors);
|
||||||
quote!(
|
quote!(
|
||||||
#form_struct
|
#form_struct
|
||||||
compile_error!(#message);
|
compile_error!(#message);
|
||||||
@@ -228,9 +208,6 @@ pub fn component(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
let Some(syms_path) = syms_path() else {
|
let Some(syms_path) = syms_path() else {
|
||||||
return quote!(#module).into();
|
return quote!(#module).into();
|
||||||
};
|
};
|
||||||
if !syms_path.exists() {
|
|
||||||
return quote!(#module).into();
|
|
||||||
}
|
|
||||||
let component_filter = component_name.as_deref();
|
let component_filter = component_name.as_deref();
|
||||||
let errors = component_contract_errors(&syms_path, component_filter, items);
|
let errors = component_contract_errors(&syms_path, component_filter, items);
|
||||||
if !errors.is_empty() {
|
if !errors.is_empty() {
|
||||||
@@ -336,6 +313,76 @@ fn form_model_type(ty: &Type) -> Option<Type> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn missing_handler_generated_files_message() -> &'static str {
|
||||||
|
"#[hemx::handler] requires generated hemx files; add hemx_build::app().run()? to build.rs or run inside a Cargo crate"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn missing_form_generated_files_message() -> &'static str {
|
||||||
|
"#[hemx::form] requires generated hemx files; add hemx_build::app().run()? to build.rs or run inside a Cargo crate"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handler_syms_path(syms_path: Option<PathBuf>) -> Result<PathBuf, &'static str> {
|
||||||
|
match syms_path {
|
||||||
|
None => Err(missing_handler_generated_files_message()),
|
||||||
|
Some(path) if !path.exists() => Err(
|
||||||
|
"#[hemx::handler] could not find generated hemx symbols; add hemx_build::app().run()? to build.rs or check template generation",
|
||||||
|
),
|
||||||
|
Some(path) => Ok(path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join_contract_errors(errors: &[String]) -> String {
|
||||||
|
errors.join("; ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn form_impl_generics(form_struct: &ItemStruct) -> syn::Generics {
|
||||||
|
let mut generics = form_struct.generics.clone();
|
||||||
|
for ty in form_parser_types(form_struct) {
|
||||||
|
generics
|
||||||
|
.make_where_clause()
|
||||||
|
.predicates
|
||||||
|
.push(parse_quote!(#ty: ::hemx::FormValue));
|
||||||
|
}
|
||||||
|
generics
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
enum HandlerPlacement {
|
||||||
|
Server,
|
||||||
|
Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handler_placement(placement: &str) -> syn::Result<HandlerPlacement> {
|
||||||
|
match placement {
|
||||||
|
"" => Ok(HandlerPlacement::Server),
|
||||||
|
"client" => Ok(HandlerPlacement::Client),
|
||||||
|
_ => Err(syn::Error::new(
|
||||||
|
proc_macro2::Span::call_site(),
|
||||||
|
"unsupported hemx handler placement; expected #[hemx::handler] or #[hemx::handler(client)]",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn client_handler_has_inputs(function: &ItemFn) -> syn::Result<bool> {
|
||||||
|
let input_count = function.sig.inputs.len();
|
||||||
|
if matches!(input_count, 0 | 2)
|
||||||
|
&& function.sig.asyncness.is_none()
|
||||||
|
&& function.sig.unsafety.is_none()
|
||||||
|
&& function.sig.constness.is_none()
|
||||||
|
&& function.sig.generics.params.is_empty()
|
||||||
|
{
|
||||||
|
Ok(input_count == 2)
|
||||||
|
} else {
|
||||||
|
Err(syn::Error::new_spanned(
|
||||||
|
&function.sig,
|
||||||
|
format!(
|
||||||
|
"client-local hemx handler `{}` must be safe, synchronous, non-generic, and accept either no parameters or `(hemx::wasm::ClientEvent, hemx::wasm::ClientState)`",
|
||||||
|
function.sig.ident
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handler_form_model_type(function: &ItemFn) -> Option<Type> {
|
fn handler_form_model_type(function: &ItemFn) -> Option<Type> {
|
||||||
function.sig.inputs.iter().rev().find_map(|arg| match arg {
|
function.sig.inputs.iter().rev().find_map(|arg| match arg {
|
||||||
FnArg::Typed(arg) => form_model_type(&arg.ty),
|
FnArg::Typed(arg) => form_model_type(&arg.ty),
|
||||||
@@ -984,14 +1031,173 @@ fn compile_error(message: &str) -> TokenStream {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
add_app_registry_helper, add_component_register_helper, component_handler_names,
|
add_app_registry_helper, add_component_register_helper, client_handler_has_inputs,
|
||||||
form_model_type, handle_params, handle_requires_form, handler_form_model_type,
|
component_handler_names, expand_handler_function, form_fields, form_impl_generics,
|
||||||
has_form_param, has_non_unit_return, missing_component_handlers, missing_handle_params,
|
form_model_type, form_resource_id, handle_params, handle_requires_form,
|
||||||
parser_type, returns_result, syms_contains_handle,
|
handler_form_model_type, handler_placement, handler_syms_path, has_form_param,
|
||||||
|
has_non_unit_return, is_type_named, join_contract_errors, missing_component_handlers,
|
||||||
|
missing_form_generated_files_message, missing_handle_params,
|
||||||
|
missing_handler_generated_files_message, parser_type, returns_result, syms_contains_handle,
|
||||||
|
HandlerPlacement,
|
||||||
};
|
};
|
||||||
use quote::quote;
|
use quote::{quote, ToTokens};
|
||||||
use syn::{parse_quote, ItemFn, Type};
|
use syn::{parse_quote, ItemFn, Type};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handler_attribute_parses_server_and_client_modes_exactly() {
|
||||||
|
assert_eq!(handler_placement("").unwrap(), HandlerPlacement::Server);
|
||||||
|
assert_eq!(
|
||||||
|
handler_placement("client").unwrap(),
|
||||||
|
HandlerPlacement::Client
|
||||||
|
);
|
||||||
|
for invalid in ["server", " client", "client ", "CLIENT"] {
|
||||||
|
assert_eq!(
|
||||||
|
handler_placement(invalid).unwrap_err().to_string(),
|
||||||
|
"unsupported hemx handler placement; expected #[hemx::handler] or #[hemx::handler(client)]"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let no_inputs: ItemFn = parse_quote!(
|
||||||
|
fn save() {}
|
||||||
|
);
|
||||||
|
let two_inputs: ItemFn = parse_quote!(
|
||||||
|
fn save(event: Event, state: State) {}
|
||||||
|
);
|
||||||
|
assert!(!client_handler_has_inputs(&no_inputs).unwrap());
|
||||||
|
assert!(client_handler_has_inputs(&two_inputs).unwrap());
|
||||||
|
|
||||||
|
let server =
|
||||||
|
expand_handler_function(no_inputs.clone(), HandlerPlacement::Server).to_string();
|
||||||
|
assert_eq!(
|
||||||
|
server,
|
||||||
|
quote!(
|
||||||
|
fn save() {}
|
||||||
|
)
|
||||||
|
.to_string()
|
||||||
|
);
|
||||||
|
let no_input_client =
|
||||||
|
expand_handler_function(no_inputs.clone(), HandlerPlacement::Client).to_string();
|
||||||
|
assert!(no_input_client.contains("super :: save ()"));
|
||||||
|
assert!(!no_input_client.contains("super :: save (event , state)"));
|
||||||
|
let input_client =
|
||||||
|
expand_handler_function(two_inputs.clone(), HandlerPlacement::Client).to_string();
|
||||||
|
assert!(input_client.contains("super :: save (event , state)"));
|
||||||
|
assert!(!input_client.contains("super :: save ()"));
|
||||||
|
|
||||||
|
for invalid in [
|
||||||
|
parse_quote!(
|
||||||
|
fn save(event: Event) {}
|
||||||
|
),
|
||||||
|
parse_quote!(
|
||||||
|
fn save(a: A, b: B, c: C) {}
|
||||||
|
),
|
||||||
|
parse_quote!(
|
||||||
|
async fn save() {}
|
||||||
|
),
|
||||||
|
parse_quote!(
|
||||||
|
unsafe fn save() {}
|
||||||
|
),
|
||||||
|
parse_quote!(
|
||||||
|
const fn save() {}
|
||||||
|
),
|
||||||
|
parse_quote!(
|
||||||
|
fn save<T>() {}
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
client_handler_has_inputs(&invalid).unwrap_err().to_string(),
|
||||||
|
"client-local hemx handler `save` must be safe, synchronous, non-generic, and accept either no parameters or `(hemx::wasm::ClientEvent, hemx::wasm::ClientState)`"
|
||||||
|
);
|
||||||
|
let expanded = expand_handler_function(invalid, HandlerPlacement::Client).to_string();
|
||||||
|
assert!(expanded.contains("compile_error !"));
|
||||||
|
assert!(expanded.contains("client-local hemx handler"));
|
||||||
|
}
|
||||||
|
// test req: derive_handler/001 req: client_local/001
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generated_file_and_form_helpers_preserve_exact_contracts() {
|
||||||
|
assert_eq!(
|
||||||
|
missing_handler_generated_files_message(),
|
||||||
|
"#[hemx::handler] requires generated hemx files; add hemx_build::app().run()? to build.rs or run inside a Cargo crate"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
missing_form_generated_files_message(),
|
||||||
|
"#[hemx::form] requires generated hemx files; add hemx_build::app().run()? to build.rs or run inside a Cargo crate"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
handler_syms_path(None).unwrap_err(),
|
||||||
|
missing_handler_generated_files_message()
|
||||||
|
);
|
||||||
|
let missing = std::env::temp_dir().join("hemx-derive-missing-symbols");
|
||||||
|
assert_eq!(
|
||||||
|
handler_syms_path(Some(missing)).unwrap_err(),
|
||||||
|
"#[hemx::handler] could not find generated hemx symbols; add hemx_build::app().run()? to build.rs or check template generation"
|
||||||
|
);
|
||||||
|
let existing = std::env::current_exe().unwrap();
|
||||||
|
assert_eq!(handler_syms_path(Some(existing.clone())).unwrap(), existing);
|
||||||
|
assert_eq!(
|
||||||
|
join_contract_errors(&["first".into(), "second".into()]),
|
||||||
|
"first; second"
|
||||||
|
);
|
||||||
|
|
||||||
|
let form: syn::ItemStruct = parse_quote!(
|
||||||
|
struct Profile<T> {
|
||||||
|
name: String,
|
||||||
|
tags: Vec<T>,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let generics = form_impl_generics(&form);
|
||||||
|
let where_clause = generics
|
||||||
|
.where_clause
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.to_token_stream()
|
||||||
|
.to_string();
|
||||||
|
assert!(where_clause.contains("String : :: hemx :: FormValue"));
|
||||||
|
assert!(where_clause.contains("T : :: hemx :: FormValue"));
|
||||||
|
// test req: derive_handler/001 req: form/004
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generated_form_symbol_lookup_is_exact_and_fail_closed() {
|
||||||
|
let path =
|
||||||
|
std::env::temp_dir().join(format!("hemx-derive-form-symbols-{}", std::process::id()));
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
"hemx-syms-v1\nform\tprofile.heml::profile\tprofile\t42\nform\nform\tbroken\nform\tmissing-id\tmissing-id\nform\tother.heml::other\tother\tbad\nform_field\tprofile\tname\ttrue\tfalse\nform_field\nform_field\tprofile\nform_field\tprofile\tbad name\tfalse\tfalse\nform_field\tprofile\ttags\tfalse\ttrue\nform_field\tprofile\tbad-name\tfalse\tfalse\nform_field\tother\tignored\tfalse\tfalse\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(form_resource_id(&path, "profile"), Some(42));
|
||||||
|
assert_eq!(form_resource_id(&path, "missing"), None);
|
||||||
|
assert_eq!(form_resource_id(&path, "broken"), None);
|
||||||
|
assert_eq!(form_resource_id(&path, "missing-id"), None);
|
||||||
|
assert_eq!(form_resource_id(&path, "other"), None);
|
||||||
|
|
||||||
|
let fields = form_fields(&path, "profile");
|
||||||
|
assert_eq!(fields.len(), 3);
|
||||||
|
assert_eq!(fields[0].ident, "name");
|
||||||
|
assert!(fields[0].required);
|
||||||
|
assert!(!fields[0].multiple);
|
||||||
|
assert_eq!(fields[1].ident, "tags");
|
||||||
|
assert!(!fields[1].required);
|
||||||
|
assert!(fields[1].multiple);
|
||||||
|
assert_eq!(fields[2].ident, "bad_name");
|
||||||
|
assert!(form_fields(&path, "missing").is_empty());
|
||||||
|
std::fs::remove_file(&path).unwrap();
|
||||||
|
assert_eq!(form_resource_id(&path, "profile"), None);
|
||||||
|
assert!(form_fields(&path, "profile").is_empty());
|
||||||
|
|
||||||
|
let string: Type = parse_quote!(String);
|
||||||
|
let qualified: Type = parse_quote!(std::string::String);
|
||||||
|
let reference: Type = parse_quote!(&String);
|
||||||
|
assert!(is_type_named(&string, "String"));
|
||||||
|
assert!(is_type_named(&qualified, "String"));
|
||||||
|
assert!(!is_type_named(&string, "Vec"));
|
||||||
|
assert!(!is_type_named(&reference, "String"));
|
||||||
|
// test req: form/004 req: diagnostics/003
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn handler_type_helpers_recognize_only_the_public_form_and_result_shapes() {
|
fn handler_type_helpers_recognize_only_the_public_form_and_result_shapes() {
|
||||||
let bare: Type = parse_quote!(Form<String>);
|
let bare: Type = parse_quote!(Form<String>);
|
||||||
|
|||||||
@@ -1347,6 +1347,15 @@ fn run_mutation_plan(package: Option<&str>, shard: Option<&str>) -> ExitCode {
|
|||||||
.join(format!("shard-{}", shard.replace('/', "-of-")))
|
.join(format!("shard-{}", shard.replace('/', "-of-")))
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
if let Some(parent) = output.parent() {
|
||||||
|
if let Err(error) = fs::create_dir_all(parent) {
|
||||||
|
eprintln!(
|
||||||
|
"failed to create mutation output parent {}: {error}",
|
||||||
|
parent.display()
|
||||||
|
);
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
let jobs = budget.jobs.to_string();
|
let jobs = budget.jobs.to_string();
|
||||||
let mut command = Command::new(&mutest);
|
let mut command = Command::new(&mutest);
|
||||||
command
|
command
|
||||||
|
|||||||
Reference in New Issue
Block a user