use super::*;
#[test]
fn bug1_excluded_type_is_fully_qualified_in_trait_impl() {
let internal_doc_method = MethodDef {
name: "render".to_string(),
params: vec![crate::core::ir::ParamDef {
name: "doc".to_string(),
ty: TypeRef::Named("HiddenDoc".to_string()),
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: true,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: crate::core::ir::CoreWrapper::None,
}],
return_type: TypeRef::String,
is_async: false,
is_static: false,
error_type: Some("Box<dyn std::error::Error + Send + Sync>".to_string()),
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
};
let trait_def = make_trait_def("Renderer", vec![internal_doc_method]);
let bridge_cfg = sample_bridge_cfg("Renderer");
let api = ApiSurface {
crate_name: "my-lib".to_string(),
version: "1.0.0".to_string(),
types: vec![],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: {
let mut m = ::std::collections::HashMap::new();
m.insert("HiddenDoc".to_string(), "my_lib::internal::HiddenDoc".to_string());
m
},
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: Vec::new(),
..Default::default()
};
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"ml",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("&my_lib::internal::HiddenDoc"),
"excluded type must be fully-qualified, not bare;\n\
actual code:\n{code}"
);
assert!(
!code.contains("&HiddenDoc"),
"bare type reference must not appear in generated trait impl;\n\
actual code:\n{code}"
);
}
#[test]
fn bug2_sync_method_body_uses_trait_error_type_not_box_from() {
use crate::codegen::generators::trait_bridge::TraitBridgeSpec;
let method = MethodDef {
name: "run".to_string(),
params: vec![],
return_type: TypeRef::String,
is_async: false,
is_static: false,
error_type: Some("MyError".to_string()),
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
};
let trait_def = make_trait_def("Backend", vec![method.clone()]);
let bridge_cfg = sample_bridge_cfg("Backend");
let spec = TraitBridgeSpec {
trait_def: &trait_def,
bridge_config: &bridge_cfg,
core_import: "my_lib",
wrapper_prefix: "Ml",
type_paths: ::std::collections::HashMap::new(),
lifetime_type_names: std::collections::HashSet::new(),
error_type: "MyError".to_string(),
error_constructor: "MyError::from({msg})".to_string(),
};
let generator = FfiBridgeGenerator {
prefix: "ml".to_string(),
core_import: "my_lib".to_string(),
type_paths: ::std::collections::HashMap::new(),
error_type: "MyError".to_string(),
plugin_error_constructor: None,
lifetime_type_names: ::std::collections::HashSet::new(),
};
let sync_body = generator.gen_vtable_call_body(&method, &spec, false);
assert!(
sync_body.contains("MyError::from("),
"sync method body must use the trait's error constructor;\n\
actual body:\n{sync_body}"
);
assert!(
!sync_body.contains("Err(Box::from("),
"sync method body must NOT use Box::from (that's for the async closure);\n\
actual body:\n{sync_body}"
);
let closure_body = generator.gen_vtable_call_body(&method, &spec, true);
assert!(
closure_body.contains("Err(Box::from("),
"async closure body must use Box::from;\n\
actual body:\n{closure_body}"
);
}
#[test]
fn bug3_returns_ref_vec_string_emits_slice_ref_and_cache_field() {
let method = MethodDef {
name: "supported_mime_types".to_string(),
params: vec![],
return_type: TypeRef::Vec(Box::new(TypeRef::String)),
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: true, returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
};
let trait_def = make_trait_def("DocumentExtractor", vec![method]);
let bridge_cfg = sample_bridge_cfg("DocumentExtractor");
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"kr",
"sample_crate",
"SampleCrateError",
"SampleCrateError::from({msg})",
None,
&api,
);
assert!(
code.contains("fn supported_mime_types(&self) -> &[&str]"),
"returns_ref Vec<String> must produce &[&str] in trait impl;\n\
actual code:\n{code}"
);
assert!(
code.contains("supported_mime_types_strs: &'static [&'static str]"),
"bridge struct must have supported_mime_types_strs cache field;\n\
actual code:\n{code}"
);
assert!(
code.contains("self.supported_mime_types_strs"),
"trait impl body must return from the cached field;\n\
actual code:\n{code}"
);
assert!(
code.contains("Box::leak"),
"constructor must use Box::leak to build &'static [&'static str];\n\
actual code:\n{code}"
);
}
#[test]
fn bug4_ffi_skip_methods_opts_out_of_trait_impl() {
let required = make_method("run", TypeRef::String, true, false);
let optional = make_method("shutdown", TypeRef::Unit, false, true);
let trait_def = make_trait_def("Backend", vec![required, optional]);
let mut bridge_cfg = sample_bridge_cfg("Backend");
bridge_cfg.ffi_skip_methods = vec!["shutdown".to_string()];
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"ml",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("fn run("),
"required method must appear in trait impl;\n\
actual code:\n{code}"
);
assert!(
!code.contains("fn shutdown("),
"method listed in ffi_skip_methods must NOT get a generated body;\n\
actual code:\n{code}"
);
}
#[test]
fn bug5_async_str_param_uses_to_string_not_clone() {
let method = MethodDef {
name: "process".to_string(),
params: vec![ParamDef {
name: "mime_type".to_string(),
ty: TypeRef::String,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: true, is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: crate::core::ir::CoreWrapper::None,
}],
return_type: TypeRef::Unit,
is_async: true, is_static: false,
error_type: Some("Box<dyn std::error::Error + Send + Sync>".to_string()),
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
};
let trait_def = make_trait_def("Backend", vec![method]);
let bridge_cfg = sample_bridge_cfg("Backend");
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"ml",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("let mime_type = mime_type.to_string()"),
"async &str param must be captured via .to_string() to avoid E0521;\n\
actual code:\n{code}"
);
assert!(
!code.contains("let mime_type = mime_type.clone()"),
"async &str param must NOT use .clone() (returns &str, still borrows);\n\
actual code:\n{code}"
);
}
#[test]
fn bug6_async_excluded_type_return_signature_and_deserialization() {
let method = MethodDef {
name: "extract_bytes".to_string(),
params: vec![ParamDef {
name: "content".to_string(),
ty: TypeRef::Bytes,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: true,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: crate::core::ir::CoreWrapper::None,
}],
return_type: TypeRef::Named("HiddenDoc".to_string()),
is_async: true,
is_static: false,
error_type: Some("Box<dyn std::error::Error + Send + Sync>".to_string()),
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
};
let trait_def = make_trait_def("Extractor", vec![method]);
let bridge_cfg = sample_bridge_cfg("Extractor");
let api = ApiSurface {
crate_name: "my-lib".to_string(),
version: "1.0.0".to_string(),
types: vec![],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: {
let mut m = ::std::collections::HashMap::new();
m.insert("HiddenDoc".to_string(), "my_lib::internal::HiddenDoc".to_string());
m
},
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: Vec::new(),
..Default::default()
};
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"ml",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("-> std::result::Result<my_lib::internal::HiddenDoc,"),
"async method return type must be qualified excluded type in signature;\n\
actual code:\n{code}"
);
assert!(
!code.contains("-> std::result::Result<String,"),
"async method return type must NOT be String for Named return types;\n\
actual code:\n{code}"
);
assert!(
code.contains("serde_json::from_str::<my_lib::internal::HiddenDoc>"),
"async closure body must deserialize JSON to HiddenDoc;\n\
actual code:\n{code}"
);
assert!(
!code.contains("Ok(cs.to_string_lossy().into_owned())"),
"async closure body must NOT return raw String for Named return types;\n\
actual code:\n{code}"
);
}
#[test]
fn bug_ffi1_trait_impl_param_types_respect_is_ref() {
let method = MethodDef {
name: "process".to_string(),
params: vec![
ParamDef {
name: "content".to_string(),
ty: TypeRef::Bytes,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: true, is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: crate::core::ir::CoreWrapper::None,
},
ParamDef {
name: "mime_type".to_string(),
ty: TypeRef::String,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: true, is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: crate::core::ir::CoreWrapper::None,
},
ParamDef {
name: "path".to_string(),
ty: TypeRef::Path,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: true, is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: crate::core::ir::CoreWrapper::None,
},
],
return_type: TypeRef::Unit,
is_async: false,
is_static: false,
error_type: Some("Box<dyn std::error::Error + Send + Sync>".to_string()),
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
};
let trait_def = make_trait_def("Extractor", vec![method]);
let bridge_cfg = sample_bridge_cfg("Extractor");
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"ml",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("content: &[u8]"),
"is_ref Bytes param must be &[u8] in trait impl, not Vec<u8>;\n\
actual code:\n{code}"
);
assert!(
code.contains("mime_type: &str"),
"is_ref String param must be &str in trait impl, not String;\n\
actual code:\n{code}"
);
assert!(
code.contains("path: &std::path::Path"),
"is_ref Path param must be &std::path::Path in trait impl, not PathBuf;\n\
actual code:\n{code}"
);
}
#[test]
fn lifetime_param_named_type_emits_angle_lifetime_placeholder() {
use crate::core::ir::{ParamDef, TypeDef};
let method = MethodDef {
name: "visit_element".to_string(),
params: vec![ParamDef {
name: "context".to_string(),
ty: TypeRef::Named("NodeContext".to_string()),
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: true, is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: crate::core::ir::CoreWrapper::None,
}],
return_type: TypeRef::Unit,
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: true, binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
};
let trait_def = make_trait_def("HtmlVisitor", vec![method]);
let bridge_cfg = sample_bridge_cfg("HtmlVisitor");
let api = ApiSurface {
crate_name: "my-lib".to_string(),
version: "1.0.0".to_string(),
types: vec![TypeDef {
name: "NodeContext".to_string(),
rust_path: "my_lib::NodeContext".to_string(),
original_rust_path: String::new(),
fields: vec![],
methods: vec![],
is_opaque: false,
is_clone: false,
is_copy: false,
is_trait: false,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: false,
super_traits: vec![],
doc: String::new(),
cfg: None,
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: true, version: Default::default(),
}],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: Vec::new(),
..Default::default()
};
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"htm",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("context: &my_lib::NodeContext<'_>"),
"lifetime-parameterized Named type must be &Type<'_> in trait impl signature;\n\
actual code:\n{code}"
);
assert!(
!code.contains("context: &my_lib::NodeContext,") && !code.contains("context: &my_lib::NodeContext\n"),
"bare &NodeContext without lifetime placeholder must NOT appear in trait impl;\n\
actual code:\n{code}"
);
}
#[test]
fn vtable_registration_signature_takes_const_pointer() {
let trait_def = make_trait_def(
"TestBackend",
vec![make_method("process", TypeRef::String, true, false)],
);
let bridge_cfg = crate::core::config::TraitBridgeConfig {
trait_name: "TestBackend".to_string(),
register_fn: Some("register_backend".to_string()),
registry_getter: Some("get_registry".to_string()),
..Default::default()
};
let api = ApiSurface {
crate_name: "test_lib".to_string(),
version: "0.1.0".to_string(),
types: vec![trait_def.clone()],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: Vec::new(),
..Default::default()
};
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"test",
"test_lib",
"TestError",
"TestError::from({msg})",
None,
&api,
);
assert!(
code.contains("vtable: *const TestTestBackendVTable"),
"FFI registration function must take vtable as `*const VTableName` pointer;\n\
actual code:\n{code}"
);
assert!(
!code.contains(
"pub unsafe extern \"C\" fn test_register_backend(\n name: *const std::ffi::c_char,\n vtable: TestTestBackendVTable,"
),
"FFI registration function must NOT take vtable as value type (bare struct);\n\
actual code:\n{code}"
);
assert!(
code.contains("let vtable_ref = &*vtable;"),
"FFI registration function body must dereference vtable pointer;\n\
actual code:\n{code}"
);
}