use super::*;
use crate::core::ir::ParamDef;
fn get_language_fn() -> FunctionDef {
FunctionDef {
name: "get_language".to_string(),
rust_path: "sample::get_language".to_string(),
original_rust_path: String::new(),
params: vec![ParamDef {
name: "name".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::Named("Language".to_string()),
is_async: false,
error_type: None,
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}
}
fn make_cfg(host_type: &str, construct_expr: &str) -> HostCapsuleTypeConfig {
HostCapsuleTypeConfig {
host_type: host_type.to_string(),
package: String::new(),
package_version: String::new(),
construct_expr: construct_expr.to_string(),
..Default::default()
}
}
#[test]
fn capsule_method_emits_configured_host_type_and_construct_expr() {
let func = get_language_fn();
let cfg = make_cfg(
"io.github.example.jtreesitter.Language",
"new io.github.example.jtreesitter.Language({ptr})",
);
let mut out = String::new();
gen_capsule_function_method(
&mut out,
&func,
"tsp",
"LanguagePack",
&AHashSet::new(),
&HashSet::new(),
&HashSet::new(),
&cfg,
);
assert!(
out.contains("io.github.example.jtreesitter.Language"),
"must use configured host_type. Got:\n{out}"
);
assert!(
out.contains("new io.github.example.jtreesitter.Language(resultPtr)"),
"must use configured construct_expr with ptr substituted. Got:\n{out}"
);
}
#[test]
fn capsule_method_registers_named_temporary_before_invocation() {
let mut func = get_language_fn();
func.params = vec![ParamDef {
name: "config".to_string(),
ty: TypeRef::Named("Config".to_string()),
..Default::default()
}];
let cfg = make_cfg("Language", "new Language({ptr})");
let mut out = String::new();
gen_capsule_function_method(
&mut out,
&func,
"tsp",
"LanguagePack",
&AHashSet::new(),
&HashSet::new(),
&HashSet::new(),
&cfg,
);
let registration = out
.find("nativeResources.register(cconfig, handle -> NativeLib.TSP_CONFIG_FREE.invoke(handle))")
.expect("temporary registration");
let invocation = out
.find("NativeLib.TSP_GET_LANGUAGE.invoke")
.expect("capsule invocation");
assert!(out.contains("var nativeResources = new NativeResources()"), "{out}");
assert!(registration < invocation, "{out}");
}
#[test]
fn capsule_method_errors_when_host_type_empty() {
let func = get_language_fn();
let cfg = make_cfg("", "new MyLanguage({ptr})");
let mut out = String::new();
gen_capsule_function_method(
&mut out,
&func,
"tsp",
"LanguagePack",
&AHashSet::new(),
&HashSet::new(),
&HashSet::new(),
&cfg,
);
assert!(
out.contains("ALEF ERROR"),
"empty host_type must produce an ALEF ERROR comment. Got:\n{out}"
);
assert!(
out.contains("host_type"),
"error must mention the missing field. Got:\n{out}"
);
}
#[test]
fn capsule_method_errors_when_construct_expr_empty() {
let func = get_language_fn();
let cfg = make_cfg("io.github.example.jtreesitter.Language", "");
let mut out = String::new();
gen_capsule_function_method(
&mut out,
&func,
"tsp",
"LanguagePack",
&AHashSet::new(),
&HashSet::new(),
&HashSet::new(),
&cfg,
);
assert!(
out.contains("ALEF ERROR"),
"empty construct_expr must produce an ALEF ERROR comment. Got:\n{out}"
);
assert!(
out.contains("construct_expr"),
"error must mention the missing field. Got:\n{out}"
);
}
fn mut_dto_param(name: &str, type_name: &str) -> ParamDef {
ParamDef {
name: name.to_string(),
ty: TypeRef::Named(type_name.to_string()),
is_ref: true,
is_mut: true,
..Default::default()
}
}
fn tag_record_fn(return_type: TypeRef) -> FunctionDef {
FunctionDef {
name: "tag_record".to_string(),
params: vec![mut_dto_param("record", "Record")],
return_type,
error_type: None,
..Default::default()
}
}
#[test]
fn mut_dto_param_writes_back_the_mutated_value() {
let func = tag_record_fn(TypeRef::Unit);
let mut out = String::new();
gen_sync_function_method(
&mut out,
&func,
"krz",
"SampleClient",
&AHashSet::new(),
&HashSet::new(),
&HashSet::new(),
false,
&AHashMap::new(),
&HashMap::new(),
);
assert!(
out.contains("public static Record tagRecord(final Record record) throws SampleClientException {"),
"expected the write-back signature (Record in, Record out), got:\n{out}"
);
assert!(
out.contains("nativeResources.register(crecord, handle -> NativeLib.KRZ_RECORD_FREE.invoke(handle));"),
"must still marshal and register the free for the parameter handle, got:\n{out}"
);
assert!(
out.contains("NativeLib.KRZ_TAG_RECORD.invoke(crecord);"),
"must still call the FFI mutator, got:\n{out}"
);
assert!(
out.contains("var jsonPtr = (MemorySegment) NativeLib.KRZ_RECORD_TO_JSON.invoke(crecord);"),
"must read the mutated handle back out via the FFI's _to_json helper, got:\n{out}"
);
assert!(
out.contains("return MAPPER.readValue(json, Record.class);"),
"must decode the read-back JSON into a fresh Record and return it, got:\n{out}"
);
assert_eq!(
out.matches("nativeResources.register(crecord,").count(),
1,
"crecord must be registered for free exactly once, got:\n{out}"
);
let marshal_pos = out
.find("nativeResources.register(crecord,")
.expect("marshal registration");
let invoke_pos = out
.find("NativeLib.KRZ_TAG_RECORD.invoke(crecord);")
.expect("mutator call");
let readback_pos = out
.find("NativeLib.KRZ_RECORD_TO_JSON.invoke(crecord);")
.expect("read-back call");
assert!(
marshal_pos < invoke_pos && invoke_pos < readback_pos,
"must marshal, then mutate, then read back, in that order, got:\n{out}"
);
assert!(
!out.contains("static void tagRecord"),
"must not regress to a void return, got:\n{out}"
);
}
#[test]
fn immutable_dto_param_gets_no_writeback() {
let func = FunctionDef {
params: vec![ParamDef {
is_ref: true,
is_mut: false,
..mut_dto_param("record", "Record")
}],
..tag_record_fn(TypeRef::Unit)
};
let mut out = String::new();
gen_sync_function_method(
&mut out,
&func,
"krz",
"SampleClient",
&AHashSet::new(),
&HashSet::new(),
&HashSet::new(),
false,
&AHashMap::new(),
&HashMap::new(),
);
assert!(
out.contains("public static void tagRecord(final Record record) throws SampleClientException {"),
"an immutable DTO param must keep the plain void signature, got:\n{out}"
);
assert!(
!out.contains("_TO_JSON"),
"an immutable DTO param must not read anything back, got:\n{out}"
);
}
#[test]
fn owned_dto_param_is_unchanged() {
let func = FunctionDef {
params: vec![ParamDef {
is_ref: false,
is_mut: false,
..mut_dto_param("record", "Record")
}],
..tag_record_fn(TypeRef::Unit)
};
let mut out = String::new();
gen_sync_function_method(
&mut out,
&func,
"krz",
"SampleClient",
&AHashSet::new(),
&HashSet::new(),
&HashSet::new(),
false,
&AHashMap::new(),
&HashMap::new(),
);
assert!(
out.contains("public static void tagRecord(final Record record) throws SampleClientException {"),
"an owned DTO param must keep the plain void signature, got:\n{out}"
);
assert!(
!out.contains("_TO_JSON"),
"an owned DTO param must not read anything back, got:\n{out}"
);
}