use super::*;
#[test]
fn bounded_duration_option_uses_u64_niche_without_boxing() {
let loc = myflat_loc();
let items: Vec<(syn::Item, SourceLocation)> = [
"#[prebindgen] pub type Duration = std::time::Duration;",
"pub fn duration_from_millis(v: u64) -> Duration { unimplemented!() }",
"pub fn duration_to_millis(v: &Duration) -> u64 { unimplemented!() }",
"pub fn duration_echo(v: Option<Duration>) -> Option<Duration> { unimplemented!() }",
]
.into_iter()
.map(|source| {
let item: syn::Item = syn::parse_str(source).unwrap();
(item, loc.clone())
})
.collect();
let registry = crate::test_util::reg_from_items(declare_referenced(items)).unwrap();
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.convert(
prebindgen_registry::convert!(Duration)
.input(prebindgen_registry::fun!(duration_from_millis))
.output(prebindgen_registry::fun!(duration_to_millis))
.valid_range(0u64..=1_000_000u64),
)
.package(crate::package!("time").fun(prebindgen_registry::fun!(duration_echo)));
let dir = unique_test_dir("jnigen_bounded_duration");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let generation = jni.build_with(registry).unwrap();
let rust_path = generation.write_rust(dir.join("gen.rs")).unwrap();
let rust = std::fs::read_to_string(rust_path).unwrap();
let paths = generation.write_kotlin(&dir.join("kotlin")).unwrap();
let kotlin = paths
.iter()
.map(|path| std::fs::read_to_string(path).unwrap())
.collect::<Vec<_>>()
.join("\n");
let rc: String = rust.split_whitespace().collect();
let kc: String = kotlin.split_whitespace().collect();
assert!(rc.contains("outsideitsdeclareddomain"), "{rust}");
assert!(
rc.contains("None=>-1i64") || rc.contains("None=>-1"),
"{rust}"
);
assert!(
rc.contains("Some({let__inner_s0=jlong_to_u64_")
&& rc.contains("let__inner_s1=u64_to_Duration_"),
"Option input must compose the raw u64 decoder with the Duration stage:\n{rust}"
);
assert!(
rc.contains("Some(value)=>{let__inner_s0=")
&& rc.contains("Duration_to_u64_")
&& rc.contains("u64_to_jlong_"),
"Option output must compose the Duration stage with the raw u64 encoder:\n{rust}"
);
assert!(!rc.contains("Optionbox:"), "{rust}");
assert!(kc.contains("v:ULong?"), "{kotlin}");
assert!(kc.contains("v?.toLong()?:-1L"), "{kotlin}");
assert!(kc.contains("v:Long"), "{kotlin}");
}
#[test]
fn flattened_field_composes_bounded_conversion_stages() {
let loc = myflat_loc();
let items = vec![
(
syn::Item::Struct(syn::parse_quote!(
pub struct Timed {
pub delay: Option<Duration>,
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn duration_from_millis(v: u64) -> Duration {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn duration_to_millis(v: &Duration) -> u64 {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn timed_use(value: &Timed) -> u64 {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn timed_echo(value: &Timed) -> Timed {
unimplemented!()
}
)),
loc,
),
];
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.convert(
prebindgen_registry::convert!(Duration)
.input(prebindgen_registry::fun!(duration_from_millis))
.output(prebindgen_registry::fun!(duration_to_millis))
.valid_range(0u64..=1_000_000u64),
)
.package(
crate::package!()
.class(crate::data_class!(Timed))
.fun(prebindgen_registry::fun!(timed_use))
.fun(prebindgen_registry::fun!(timed_echo)),
);
let dir = unique_test_dir("jnigen_flat_staged_field");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let generation = jni.build_with(registry).expect("resolve");
let rust = std::fs::read_to_string(generation.write_rust(dir.join("gen.rs")).unwrap()).unwrap();
let kotlin = generation
.write_kotlin(&dir.join("kotlin"))
.unwrap()
.iter()
.map(|path| std::fs::read_to_string(path).unwrap())
.collect::<Vec<_>>()
.join("\n");
let rc: String = rust.split_whitespace().collect();
let kc: String = kotlin.split_whitespace().collect();
assert!(kc.contains("valueDelay:Long"), "{kotlin}");
assert!(kc.contains("value.delay?.toLong()?:-1L"), "{kotlin}");
assert!(
kc.contains("funfromParts(delay:Long):Timed=Timed(if(delay==-1L)nullelsedelay.toULong())"),
"the struct factory must receive the niche as a primitive Long:\n{kotlin}"
);
assert!(
kc.contains("TimedBuilderRaw<outR>{publicfunrun(delay:Long):R}"),
"{kotlin}"
);
assert!(
kc.contains("if(delay==-1L)nullelsedelay.toULong()"),
"the raw builder adapter must restore the optional niche:\n{kotlin}"
);
assert!(rc.contains("jlong_to_u64"), "{rust}");
assert!(rc.contains("u64_to_Duration"), "{rust}");
assert!(
rc.contains("jlong_to_Option_Duration") && rc.contains("env,&__delay_raw)?"),
"whole-JObject input must invoke the complete optional Duration converter:\n{rust}"
);
assert!(
rc.contains("let___delay:jni::sys::jlong=Option_Duration_to_jlong")
&& rc.contains("\"(J)Lio/test/jni/Timed;\""),
"whole-struct output must pass the niche as primitive jlong:\n{rust}"
);
assert!(!rc.contains("let___delay:jni::objects::JObject"), "{rust}");
assert!(
rc.contains("myflat::Timed{delay:__flat_value_delay"),
"{rust}"
);
}
#[test]
fn a_bounded_leaf_takes_its_sentinel_from_its_own_type_not_its_ancestor() {
let loc = myflat_loc();
let items = vec![
(
syn::Item::Struct(syn::parse_quote!(
pub struct SpanStruct {
pub required: Duration,
pub delay: Option<Duration>,
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn duration_from_millis(v: u64) -> Duration {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn duration_to_millis(v: &Duration) -> u64 {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn span_to_struct(s: &Span) -> SpanStruct {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn holder_span(h: &Holder) -> Option<&Span> {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn holder_each(cb: impl Fn(Holder) + Send + Sync + 'static) {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn span_each(cb: impl Fn(Span) + Send + Sync + 'static) {
unimplemented!()
}
)),
loc,
),
];
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.convert(
prebindgen_registry::convert!(Duration)
.input(prebindgen_registry::fun!(duration_from_millis))
.output(prebindgen_registry::fun!(duration_to_millis))
.valid_range(0u64..=1_000_000u64),
)
.package(
crate::package!()
.class(crate::ptr_class!(Span))
.class(crate::ptr_class!(Holder))
.fun(prebindgen_registry::fun!(span_each))
.fun(prebindgen_registry::fun!(holder_each)),
)
.expand(
prebindgen_registry::expand_return!(Span)
.fields(prebindgen_registry::fields!(span_to_struct)),
)
.expand(
prebindgen_registry::expand_return!(Holder)
.field(prebindgen_registry::fun!(holder_span)),
);
let dir = unique_test_dir("jnigen_niche_matrix");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let generation = jni.build_with(registry).expect("resolve");
let kotlin = generation
.write_kotlin(&dir.join("kotlin"))
.unwrap()
.iter()
.map(|path| std::fs::read_to_string(path).unwrap())
.collect::<Vec<_>>()
.join("\n");
let kc: String = kotlin.split_whitespace().collect();
assert!(
kc.contains("funrun(required:Long,delay:Long)"),
"with no optional ancestor both leaves keep the primitive wire:\n{kotlin}"
);
assert!(
kc.contains("required.toULong()"),
"row 1: a bare bounded leaf just converts:\n{kotlin}"
);
assert!(
kc.contains("if(delay==-1L)nullelsedelay.toULong()"),
"row 2: the leaf's own niche is tested, unguarded:\n{kotlin}"
);
assert!(
kc.contains("funrun(holderSpan__required:Long?,holderSpan__delay:Long?)"),
"under an optional ancestor both leaves box:\n{kotlin}"
);
assert!(
kc.contains("holderSpan__required?.toULong()"),
"row 3: a bare bounded leaf under an optional ancestor takes no \
sentinel:\n{kotlin}"
);
assert!(
!kc.contains("holderSpan__required?.let{if(it==-1L)"),
"row 3: …and specifically not the doubly-optional shape:\n{kotlin}"
);
assert!(
kc.contains("holderSpan__delay?.let{if(it==-1L)nullelseit.toULong()}"),
"row 4: an ancestor's `?` does not erase the leaf's own niche:\n{kotlin}"
);
}
#[test]
fn duration_requires_an_explicit_conversion() {
let alias: syn::Item =
syn::parse_str("#[prebindgen] pub type Duration = std::time::Duration;").unwrap();
let function: syn::ItemFn =
syn::parse_str("pub fn duration_echo(v: Duration) -> Duration { unimplemented!() }")
.unwrap();
let registry = crate::test_util::reg_from_items(declare_referenced([
(alias, myflat_loc()),
(syn::Item::Fn(function), myflat_loc()),
]))
.expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(crate::package!("time").fun(prebindgen_registry::fun!(duration_echo)));
let error = jni
.build_with(registry)
.expect_err("Duration must not have an implicit unchecked converter")
.to_string();
assert!(error.contains("Duration"), "{error}");
}
#[test]
#[should_panic(expected = "domain type i64 does not match input representation u64")]
fn conversion_domain_must_match_the_representation() {
let loc = myflat_loc();
let items: Vec<(syn::Item, SourceLocation)> = [
"pub fn duration_from_millis(v: u64) -> Duration { unimplemented!() }",
"pub fn duration_use(v: Duration) { unimplemented!() }",
]
.into_iter()
.map(|source| {
let item: syn::Item = syn::parse_str(source).unwrap();
(item, loc.clone())
})
.collect();
let registry = crate::test_util::reg_from_items(declare_referenced(items)).unwrap();
let jni = JniGenBuilder::new()
.convert(
prebindgen_registry::convert!(Duration)
.input(prebindgen_registry::fun!(duration_from_millis))
.valid_range(0i64..=1_000i64),
)
.package(crate::package!("time").fun(prebindgen_registry::fun!(duration_use)));
let _ = jni.build_with(registry);
}
#[test]
fn option_scalar_param_crosses_as_present_value_pair() {
let loc = myflat_loc();
let items: Vec<(syn::Item, SourceLocation)> = vec![
(
syn::Item::Enum(syn::parse_quote!(
pub enum Mode {
A,
B,
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn z_set_timeout(ms: Option<i64>, count: Option<i32>, mode: Option<Mode>) {
unimplemented!()
}
)),
loc.clone(),
),
];
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(crate::package!().class(crate::enum_class!(Mode)))
.package(crate::package!("cfg").fun(prebindgen_registry::fun!(z_set_timeout)));
let dir = unique_test_dir("jnigen_optscalar");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let gen = jni.build_with(registry).expect("resolve");
let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust");
let rust = std::fs::read_to_string(&rust_path).unwrap();
let rc: String = rust.split_whitespace().collect();
let kdir = dir.join("kotlin");
let paths = gen.write_kotlin(&kdir).expect("write_kotlin");
let kotlin: String = paths
.iter()
.map(|p| std::fs::read_to_string(p).unwrap())
.collect::<Vec<_>>()
.join("\n");
let kc: String = kotlin.split_whitespace().collect();
assert!(kc.contains("ms:Long?"), "{kotlin}");
assert!(kc.contains("count:Int?"), "{kotlin}");
assert!(kc.contains("mode:Mode?"), "{kotlin}");
assert!(kc.contains("msPresent:Boolean"), "{kotlin}");
assert!(kc.contains("msValue:Long"), "{kotlin}");
assert!(kc.contains("countPresent:Boolean"), "{kotlin}");
assert!(kc.contains("countValue:Int"), "{kotlin}");
assert!(kc.contains("modePresent:Boolean"), "{kotlin}");
assert!(kc.contains("modeValue:Int"), "{kotlin}");
assert!(kc.contains("ms!=null"), "{kotlin}");
assert!(kc.contains("ms?:0L"), "{kotlin}");
assert!(kc.contains("count?:0"), "{kotlin}");
assert!(kc.contains("mode?.value?:0"), "{kotlin}");
assert!(rc.contains("ms_present:jni::sys::jboolean"), "{rust}");
assert!(rc.contains("ms_value:jni::sys::jlong"), "{rust}");
assert!(rc.contains("count_value:jni::sys::jint"), "{rust}");
assert!(rc.contains("mode_value:jni::sys::jint"), "{rust}");
assert!(rc.contains("ifms_present!=0u8"), "{rust}");
assert!(
rc.contains("myflat::z_set_timeout(ms,count,mode)"),
"{rust}"
);
}
#[test]
fn vec_of_handle_output_folds_kotlin_side() {
let loc = myflat_loc();
let items: Vec<(syn::Item, SourceLocation)> = vec![
(
syn::Item::Struct(syn::parse_quote!(
pub struct ZThing {
_p: u8,
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn thing_list() -> Vec<ZThing> {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn thing_list_opt() -> Option<Vec<ZThing>> {
unimplemented!()
}
)),
loc.clone(),
),
];
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(
crate::package!("thing")
.class(crate::ptr_class!(ZThing))
.fun(prebindgen_registry::fun!(thing_list))
.fun(prebindgen_registry::fun!(thing_list_opt)),
);
let dir = unique_test_dir("jnigen_vec_handle_out");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let gen = jni.build_with(registry).expect("resolve");
let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust");
let rust = std::fs::read_to_string(&rust_path).unwrap();
let rc: String = rust.split_whitespace().collect();
let kdir = dir.join("kotlin");
let paths = gen.write_kotlin(&kdir).expect("write_kotlin");
let kotlin: String = paths
.iter()
.map(|p| std::fs::read_to_string(p).unwrap())
.collect::<Vec<_>>()
.join("\n");
let kc: String = kotlin.split_whitespace().collect();
assert!(kc.contains("interfaceZThingFolderRaw<A>"), "{kotlin}");
assert!(!kc.contains("interfaceZThingFolder<A>"), "{kotlin}");
assert!(!kc.contains("ZThingFolder<A>.asRaw"), "{kotlin}");
assert!(kc.contains("List<ZThing>"), "{kotlin}");
assert!(kc.contains("ArrayList<ZThing>()"), "{kotlin}");
assert!(
kc.contains("ZThing(element)") || kc.contains("acc.add(ZThing("),
"{kotlin}"
);
assert!(kc.contains("List<ZThing>?"), "{kotlin}");
assert!(rc.contains("jvalue{j:__enc}"), "{rust}");
assert!(
!rc.contains(r#"new_object("java/util/ArrayList""#),
"no Rust-side ArrayList for Vec<handle>: {rust}"
);
}
#[test]
fn option_scalar_struct_field_flattens() {
let loc = myflat_loc();
let items: Vec<(syn::Item, SourceLocation)> = vec![
(
syn::Item::Struct(syn::parse_quote!(
pub struct Opts {
pub id: i64,
pub ttl: Option<i64>,
pub flag: Option<bool>,
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn opts_put(o: &Opts) {
unimplemented!()
}
)),
loc.clone(),
),
];
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(
crate::package!()
.class(crate::data_class!(Opts))
.fun(prebindgen_registry::fun!(opts_put)),
);
let dir = unique_test_dir("jnigen_optfield");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let gen = jni.build_with(registry).expect("resolve");
let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust");
let rust = std::fs::read_to_string(&rust_path).unwrap();
let rc: String = rust.split_whitespace().collect();
let kdir = dir.join("kotlin");
let paths = gen.write_kotlin(&kdir).expect("write_kotlin");
let kotlin: String = paths
.iter()
.map(|p| std::fs::read_to_string(p).unwrap())
.collect::<Vec<_>>()
.join("\n");
let kc: String = kotlin.split_whitespace().collect();
assert!(kc.contains("oTtlPresent:Boolean"), "{kotlin}");
assert!(kc.contains("oTtlValue:Long"), "{kotlin}");
assert!(kc.contains("oFlagPresent:Boolean"), "{kotlin}");
assert!(kc.contains("oFlagValue:Boolean"), "{kotlin}");
assert!(kc.contains("o.ttl!=null"), "{kotlin}");
assert!(kc.contains("o.ttl?:0L"), "{kotlin}");
assert!(kc.contains("o.flag?:false"), "{kotlin}");
assert!(rc.contains("o_ttl_present:jni::sys::jboolean"), "{rust}");
assert!(rc.contains("o_ttl_value:jni::sys::jlong"), "{rust}");
assert!(rc.contains("ifo_ttl_present!=0u8"), "{rust}");
assert!(
rc.contains("myflat::Opts{id:__flat_o_id,ttl:__flat_o_ttl,flag:__flat_o_flag"),
"{rust}"
);
assert!(rc.contains("myflat::opts_put(&o)"), "{rust}");
}
#[test]
fn recursive_data_class_input_flattens_nested_and_optional_fields() {
let loc = myflat_loc();
let items: Vec<(syn::Item, SourceLocation)> = vec![
(
syn::Item::Enum(syn::parse_quote!(
pub enum Level {
Low = 0,
High = 1,
}
)),
loc.clone(),
),
(
syn::Item::Struct(syn::parse_quote!(
pub struct Inner {
pub id: i64,
}
)),
loc.clone(),
),
(
syn::Item::Struct(syn::parse_quote!(
pub struct Job {
pub inner: Inner,
pub level: Level,
pub ttl: Option<i64>,
pub mode: Option<Level>,
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn job_make(tag: i64) -> Job {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn job_mode(j: &Job) -> Option<Level> {
unimplemented!()
}
)),
loc.clone(),
),
];
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(
crate::package!("model")
.class(crate::enum_class!(Level))
.class(crate::data_class!(Inner))
.class(crate::data_class!(Job)),
)
.package(
crate::package!("job")
.fun(prebindgen_registry::fun!(job_make))
.fun(prebindgen_registry::fun!(job_mode)),
);
let dir = unique_test_dir("jnigen_fromparts_optbox");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let gen = jni.build_with(registry).expect("resolve");
let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust");
let rust = std::fs::read_to_string(&rust_path).unwrap();
let rc: String = rust.split_whitespace().collect();
let kdir = dir.join("kotlin");
let paths = gen.write_kotlin(&kdir).expect("write_kotlin");
let kotlin: String = paths
.iter()
.map(|p| std::fs::read_to_string(p).unwrap())
.collect::<Vec<_>>()
.join("\n");
let kc: String = kotlin.split_whitespace().collect();
assert!(
rc.contains(r#""(JILjava/lang/Long;Ljava/lang/Integer;)Lio/test/jni/model/Job;""#),
"{rust}"
);
assert!(kc.contains("ttl:Long?"), "{kotlin}");
assert!(kc.contains("mode:Int?"), "{kotlin}");
assert!(kc.contains("mode?.let{Level.fromInt(it)}"), "{kotlin}");
assert!(kc.contains("Inner.fromParts(inner_id)"), "{kotlin}");
assert!(kc.contains("jInnerId:Long"), "{kotlin}");
assert!(kc.contains("jLevel:Int"), "{kotlin}");
assert!(kc.contains("jTtlPresent:Boolean"), "{kotlin}");
assert!(kc.contains("jModeValue:Int"), "{kotlin}");
assert!(kc.contains("j.inner.id"), "{kotlin}");
assert!(rc.contains("myflat::Inner{id:__flat_j_inner_id"), "{rust}");
assert!(
rc.contains("myflat::Job{inner:__flat_j_inner,level:__flat_j_level"),
"{rust}"
);
assert!(kc.contains("jModeValue:Int"), "{kotlin}");
assert!(kc.contains("errorSink:Any"), "{kotlin}");
assert!(kc.contains("):Int?"), "{kotlin}");
assert!(kc.contains("?.let{Level.fromInt(it)}"), "{kotlin}");
}
#[test]
fn jobject_input_is_an_explicit_hybrid_leaf_escape_hatch() {
let loc = myflat_loc();
let items = vec![
(
syn::Item::Struct(syn::parse_quote!(
pub struct FlatChild {
pub id: i64,
}
)),
loc.clone(),
),
(
syn::Item::Struct(syn::parse_quote!(
pub struct ObjectChild {
pub name: String,
}
)),
loc.clone(),
),
(
syn::Item::Struct(syn::parse_quote!(
pub struct Hybrid {
pub flat: FlatChild,
pub maybe: Option<FlatChild>,
pub object: ObjectChild,
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn hybrid_use(h: Hybrid) -> i64 {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn hybrid_optional(h: Option<Hybrid>) -> i64 {
unimplemented!()
}
)),
loc,
),
];
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(
crate::package!()
.class(crate::data_class!(FlatChild))
.class(crate::data_class!(ObjectChild).jobject_input())
.class(crate::data_class!(Hybrid))
.fun(prebindgen_registry::fun!(hybrid_use))
.fun(prebindgen_registry::fun!(hybrid_optional)),
);
let dir = unique_test_dir("jnigen_hybrid_jobject_input");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let generation = jni.build_with(registry).expect("resolve");
let rust = std::fs::read_to_string(generation.write_rust(dir.join("gen.rs")).unwrap()).unwrap();
let kotlin = generation
.write_kotlin(&dir.join("kotlin"))
.unwrap()
.iter()
.map(|path| std::fs::read_to_string(path).unwrap())
.collect::<Vec<_>>()
.join("\n");
let rc: String = rust.split_whitespace().collect();
let kc: String = kotlin.split_whitespace().collect();
assert!(kc.contains("hFlatId:Long"), "{kotlin}");
assert!(kc.contains("hObject:ObjectChild"), "{kotlin}");
assert!(kc.contains("h.flat.id"), "{kotlin}");
assert!(kc.contains("hMaybePresent:Boolean"), "{kotlin}");
assert!(kc.contains("h.maybe?.id?:0L"), "{kotlin}");
assert!(kc.contains("h.object_"), "{kotlin}");
assert!(kc.contains("hPresent:Boolean"), "{kotlin}");
assert!(kc.contains("hObject:io.test.jni.ObjectChild?"), "{kotlin}");
assert!(kc.contains("h?.object_"), "{kotlin}");
assert!(rc.contains("JObject_to_ObjectChild"), "{rust}");
assert!(
rc.contains(
"myflat::Hybrid{flat:__flat_h_flat,maybe:__flat_h_maybe,object:__flat_h_object"
),
"{rust}"
);
assert!(generation.report().contains("input `JObject` opt-in"));
}
#[test]
fn empty_structs_keep_their_own_constructor_delimiters() {
let loc = myflat_loc();
let items = vec![
(
syn::Item::Struct(syn::parse_quote!(
pub struct Unit;
)),
loc.clone(),
),
(
syn::Item::Struct(syn::parse_quote!(
pub struct EmptyNamed {}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn take_empties(a: Unit, c: EmptyNamed) -> i64 {
unimplemented!()
}
)),
loc,
),
];
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(
crate::package!()
.class(crate::data_class!(Unit).jobject_input())
.class(crate::data_class!(EmptyNamed).jobject_input())
.fun(prebindgen_registry::fun!(take_empties)),
);
let dir = unique_test_dir("jnigen_empty_struct_delimiters");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let generation = jni.build_with(registry).expect("resolve");
let rust = std::fs::read_to_string(generation.write_rust(dir.join("gen.rs")).unwrap()).unwrap();
let rc: String = rust.split_whitespace().collect();
assert!(
rc.contains("myflat::Unit)") || rc.contains("myflat::Unit}"),
"unit struct must be constructed bare, got:\n{rust}"
);
assert!(
!rc.contains("myflat::Unit{}"),
"unit struct must not take braces:\n{rust}"
);
assert!(
rc.contains("myflat::EmptyNamed{}"),
"empty named struct keeps its braces:\n{rust}"
);
}
#[test]
fn recursive_flattened_owned_handles_join_lock_and_consume_scaffold() {
let loc = myflat_loc();
let items = vec![
(
syn::Item::Struct(syn::parse_quote!(
pub struct Token {
pub value: i64,
}
)),
loc.clone(),
),
(
syn::Item::Struct(syn::parse_quote!(
pub struct Envelope {
pub token: Token,
pub spare: Option<Token>,
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn envelope_use(e: Envelope) -> i64 {
unimplemented!()
}
)),
loc,
),
];
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(
crate::package!()
.class(crate::ptr_class!(Token))
.class(crate::data_class!(Envelope))
.fun(prebindgen_registry::fun!(envelope_use)),
);
let dir = unique_test_dir("jnigen_recursive_handles");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let generation = jni.build_with(registry).expect("resolve");
let rust = std::fs::read_to_string(generation.write_rust(dir.join("gen.rs")).unwrap()).unwrap();
let kotlin = generation
.write_kotlin(&dir.join("kotlin"))
.unwrap()
.iter()
.map(|path| std::fs::read_to_string(path).unwrap())
.collect::<Vec<_>>()
.join("\n");
let rc: String = rust.split_whitespace().collect();
let kc: String = kotlin.split_whitespace().collect();
assert!(kc.contains("withSortedHandleLocks(__locks)"), "{kotlin}");
assert!(kc.contains("__locks.add(e.token)"), "{kotlin}");
assert!(kc.contains("e.spare?.let{__locks.add(it)}"), "{kotlin}");
assert!(kc.contains("e.spare?.isClosed()==true"), "{kotlin}");
assert!(kc.contains("valeToken_ptr=e.token.ptr"), "{kotlin}");
assert!(kc.contains("valeSpare_ptr=e.spare?.ptr?:0L"), "{kotlin}");
assert!(kc.contains("e.token.markConsumed()"), "{kotlin}");
assert!(kc.contains("e.spare?.markConsumed()"), "{kotlin}");
assert!(
rc.contains("Box::from_raw(e_tokenas*mutmyflat::Token)"),
"{rust}"
);
assert!(
rc.contains(
"Option::Some(unsafe{*::std::boxed::Box::from_raw(e_spareas*mutmyflat::Token)})"
),
"{rust}"
);
}
#[test]
fn recursive_flattening_rejects_jvm_parameter_slot_overflow() {
let fields = (0..127)
.map(|index| format!("pub f{index}: i64"))
.collect::<Vec<_>>()
.join(",");
let wide: syn::ItemStruct =
syn::parse_str(&format!("pub struct Wide {{ {fields} }}")).expect("parse wide struct");
let use_wide: syn::ItemFn = syn::parse_quote!(
pub fn use_wide(value: Wide) -> i64 {
unimplemented!()
}
);
let loc = myflat_loc();
let registry = crate::test_util::reg_from_items(declare_referenced([
(syn::Item::Struct(wide.clone()), loc.clone()),
(syn::Item::Fn(use_wide.clone()), loc.clone()),
]))
.expect("index items");
let jni = JniGenBuilder::new().package(
crate::package!()
.class(crate::data_class!(Wide))
.fun(prebindgen_registry::fun!(use_wide)),
);
let error = jni
.build_with(registry)
.expect_err("256 JVM slots must fail")
.to_string();
assert!(error.contains("uses 256 JVM parameter slots"), "{error}");
assert!(error.contains("jobject_input"), "{error}");
let registry = crate::test_util::reg_from_items(declare_referenced([
(syn::Item::Struct(wide), loc.clone()),
(syn::Item::Fn(use_wide), loc),
]))
.expect("index marked items");
let jni = JniGenBuilder::new().package(
crate::package!()
.class(crate::data_class!(Wide).jobject_input())
.fun(prebindgen_registry::fun!(use_wide)),
);
let generation = jni
.build_with(registry)
.expect("JObject boundary must bypass the flattened slot limit");
assert!(generation.report().contains("input `JObject` opt-in"));
}
#[test]
fn output_only_convert_resolves_without_input_twin() {
let loc = myflat_loc();
let fns: &[&str] = &[
"pub fn len_of(s: &String) -> Len { unimplemented!() }",
"pub fn len_value(l: &Len) -> i64 { unimplemented!() }",
];
let items: Vec<(syn::Item, SourceLocation)> = fns
.iter()
.map(|src| {
let f: syn::ItemFn = syn::parse_str(src).expect("parse fn");
(syn::Item::Fn(f), loc.clone())
})
.collect();
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.convert(prebindgen_registry::convert!(Len).output(prebindgen_registry::fun!(len_value)))
.package(crate::package!("len").fun(prebindgen_registry::fun!(len_of)));
let dir = unique_test_dir("jnigen_outonly_convert");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let gen = jni
.build_with(registry)
.expect("an output-only convert type must not require an input twin");
let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust");
let rust = std::fs::read_to_string(&rust_path).unwrap();
let rc: String = rust.split_whitespace().collect();
assert!(rc.contains("myflat::len_value(&v)"), "{rust}");
assert!(rc.contains("myflat::len_of(&s)"), "{rust}");
}
#[test]
fn convert_fn_qualifies_with_origin_crate() {
let loc = |krate: &str| SourceLocation {
crate_name: Some(krate.to_string()),
..SourceLocation::default()
};
let item = |src: &str, krate: &str| -> (syn::Item, SourceLocation) {
let f: syn::ItemFn = syn::parse_str(src).expect("parse fn");
(syn::Item::Fn(f), loc(krate))
};
let flat = vec![item(
"pub fn len_of(s: &String) -> Len { unimplemented!() }",
"myflat",
)];
let helpers = vec![item(
"pub fn len_value(l: &Len) -> i64 { unimplemented!() }",
"my-helpers",
)];
let registry =
crate::test_util::reg_from_items(declare_referenced(flat.into_iter().chain(helpers)))
.expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.convert(prebindgen_registry::convert!(Len).output(prebindgen_registry::fun!(len_value)))
.package(crate::package!("len").fun(prebindgen_registry::fun!(len_of)));
let dir = unique_test_dir("jnigen_convert_origin");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let gen = jni.build_with(registry).expect("resolve");
let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust");
let rust = std::fs::read_to_string(&rust_path).unwrap();
let rc: String = rust.split_whitespace().collect();
assert!(rc.contains("my_helpers::len_value(&v)"), "{rust}");
assert!(rc.contains("myflat::len_of(&s)"), "{rust}");
}
#[test]
#[should_panic(expected = "produces `Other`, not `Len`")]
fn convert_input_target_mismatch_rejected() {
let loc = myflat_loc();
let fns: &[&str] = &[
"pub fn from_long(v: i64) -> Other { unimplemented!() }",
"pub fn use_len(l: Len) { unimplemented!() }",
];
let items: Vec<(syn::Item, SourceLocation)> = fns
.iter()
.map(|src| {
let f: syn::ItemFn = syn::parse_str(src).expect("parse fn");
(syn::Item::Fn(f), loc.clone())
})
.collect();
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.convert(prebindgen_registry::convert!(Len).input(prebindgen_registry::fun!(from_long)))
.package(crate::package!("len").fun(prebindgen_registry::fun!(use_len)));
let dir = unique_test_dir("jnigen_convert_mismatch");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let _ = jni
.build_with(registry)
.and_then(|gen| gen.write_rust(dir.join("gen.rs")));
}
#[test]
fn convert_via_trait_impls() {
let loc = myflat_loc();
let f: syn::ItemFn =
syn::parse_str("pub fn temp_double(c: Celsius) -> Celsius { unimplemented!() }").unwrap();
let registry =
crate::test_util::reg_from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)]))
.expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.convert(
prebindgen_registry::convert!(Celsius)
.input(prebindgen_registry::from!(i32))
.output(prebindgen_registry::into!(i32)),
)
.package(crate::package!("m").fun(prebindgen_registry::fun!(temp_double)));
let dir = unique_test_dir("jnigen_convert_trait");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let gen = jni.build_with(registry).expect("resolve");
let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust");
let rust = std::fs::read_to_string(&rust_path).unwrap();
let rc: String = rust.split_whitespace().collect();
assert!(
rc.contains("<i32as::core::convert::Into<myflat::Celsius>>::into(v)"),
"{rust}"
);
assert!(
rc.contains("<myflat::Celsiusas::core::convert::Into<i32>>::into(v)"),
"{rust}"
);
}
#[test]
fn convert_via_try_from_is_fallible() {
let loc = myflat_loc();
let f: syn::ItemFn =
syn::parse_str("pub fn pct_use(p: Percent) -> i32 { unimplemented!() }").unwrap();
let registry =
crate::test_util::reg_from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)]))
.expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.convert(prebindgen_registry::convert!(Percent).input(prebindgen_registry::try_from!(i32)))
.package(crate::package!("m").fun(prebindgen_registry::fun!(pct_use)));
let dir = unique_test_dir("jnigen_convert_tryfrom");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let gen = jni.build_with(registry).expect("resolve");
let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust");
let rust = std::fs::read_to_string(&rust_path).unwrap();
let rc: String = rust.split_whitespace().collect();
assert!(
rc.contains("<i32as::core::convert::TryInto<myflat::Percent>>::try_into(v)"),
"{rust}"
);
assert!(
rc.contains("<i32as::core::convert::TryInto<myflat::Percent>>::Error"),
"{rust}"
);
}
#[test]
fn option_composition_normalizes_fallible_stage_errors() {
let loc = myflat_loc();
let f: syn::ItemFn = syn::parse_str(
"pub fn pct_optional(p: Option<Percent>) -> Option<Percent> { unimplemented!() }",
)
.unwrap();
let registry =
crate::test_util::reg_from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)]))
.expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.convert(
prebindgen_registry::convert!(Percent)
.input(prebindgen_registry::try_from!(i32))
.output(
prebindgen_registry::fun!(crate::conv::pct_out)
.sig(prebindgen_registry::sig!((p: Percent) -> Result<i32, String>)),
),
)
.package(crate::package!("m").fun(prebindgen_registry::fun!(pct_optional)));
let dir = unique_test_dir("jnigen_option_fallible_stages");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let gen = jni.build_with(registry).expect("resolve");
let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust");
let rust = std::fs::read_to_string(&rust_path).unwrap();
let rc: String = rust.split_whitespace().collect();
assert!(
rc.matches("__e.to_string()").count() >= 2,
"input and output stages must both normalize their raw errors:\n{rust}"
);
assert!(rc.contains("JObject_to_Option_Percent"), "{rust}");
assert!(rc.contains("Option_Percent_to_JObject"), "{rust}");
}
#[test]
fn convert_via_local_fns() {
let loc = myflat_loc();
let f: syn::ItemFn =
syn::parse_str("pub fn label_id(l: Label) -> Label { unimplemented!() }").unwrap();
let registry =
crate::test_util::reg_from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)]))
.expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.convert(
prebindgen_registry::convert!(Label)
.input(
prebindgen_registry::fun!(crate::conv::label_in)
.sig(prebindgen_registry::sig!((s: String) -> Label)),
)
.output(
prebindgen_registry::fun!(crate::conv::label_out)
.sig(prebindgen_registry::sig!((l: Label) -> String)),
),
)
.package(crate::package!("m").fun(prebindgen_registry::fun!(label_id)));
let dir = unique_test_dir("jnigen_convert_local");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let gen = jni.build_with(registry).expect("resolve");
let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust");
let rust = std::fs::read_to_string(&rust_path).unwrap();
let rc: String = rust.split_whitespace().collect();
assert!(rc.contains("crate::conv::label_in("), "{rust}");
assert!(rc.contains("crate::conv::label_out("), "{rust}");
}
#[test]
#[should_panic(expected = "input conversion is already declared")]
fn convert_duplicate_input_rejected() {
let _ = prebindgen_registry::convert!(Widget)
.input(prebindgen_registry::from!(i32))
.input(
prebindgen_registry::fun!(crate::widget_in)
.sig(prebindgen_registry::sig!((v: String) -> Widget)),
);
}
#[test]
#[should_panic(expected = "an input conversion is built with from!/try_from!")]
fn convert_input_into_direction_rejected() {
let _ = prebindgen_registry::convert!(Widget).input(prebindgen_registry::into!(i32));
}
#[test]
#[should_panic(expected = "an output conversion is built with into!/try_into!")]
fn convert_output_from_direction_rejected() {
let _ = prebindgen_registry::convert!(Widget).output(prebindgen_registry::from!(i32));
}
#[test]
#[should_panic(expected = ".sig(sig!(")]
fn convert_local_source_missing_sig_rejected() {
let _ =
prebindgen_registry::convert!(Widget).input(prebindgen_registry::fun!(crate::widget_in));
}
#[test]
#[should_panic(expected = ".name()/expand overrides don't apply")]
fn convert_source_fun_with_decorations_rejected() {
let _ = prebindgen_registry::convert!(Widget)
.input(prebindgen_registry::fun!(widget_in).name("widgetIn"));
}
#[test]
fn convert_via_local_try_fn_is_fallible() {
let loc = myflat_loc();
let f: syn::ItemFn =
syn::parse_str("pub fn label_id(l: Label) -> Label { unimplemented!() }").unwrap();
let registry =
crate::test_util::reg_from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)]))
.expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.convert(
prebindgen_registry::convert!(Label)
.input(
prebindgen_registry::fun!(crate::conv::label_in)
.sig(prebindgen_registry::sig!((s: String) -> Result<Label, String>)),
)
.output(
prebindgen_registry::fun!(crate::conv::label_out)
.sig(prebindgen_registry::sig!((l: Label) -> String)),
),
)
.package(crate::package!("m").fun(prebindgen_registry::fun!(label_id)));
let dir = unique_test_dir("jnigen_convert_local_try");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let gen = jni.build_with(registry).expect("resolve");
let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust");
let rust = std::fs::read_to_string(&rust_path).unwrap();
let rc: String = rust.split_whitespace().collect();
assert!(rc.contains("crate::conv::label_in("), "{rust}");
assert!(rc.contains("Result<myflat::Label,String>"), "{rust}");
}
#[test]
fn data_class_members_reenter_as_field_leaves() {
let loc = myflat_loc();
let items: Vec<(syn::Item, SourceLocation)> = vec![
(
syn::Item::Struct(syn::parse_quote!(
pub struct Point {
pub x: i64,
pub y: i64,
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn point_norm(p: &Point) -> i64 {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn point_origin() -> Point {
unimplemented!()
}
)),
loc.clone(),
),
];
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(
crate::package!().class(
crate::data_class!(Point)
.method(prebindgen_registry::fun!(point_norm).name("norm"))
.constructor(prebindgen_registry::fun!(point_origin).name("origin")),
),
);
let dir = unique_test_dir("jnigen_data_members");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let gen = jni.build_with(registry).expect("resolve");
gen.write_rust(dir.join("gen.rs")).expect("write_rust");
let kdir = dir.join("kotlin");
let paths = gen.write_kotlin(&kdir).expect("write_kotlin");
let all: String = paths
.iter()
.filter_map(|p| std::fs::read_to_string(p).ok())
.collect::<Vec<_>>()
.join("\n");
let ac: String = all.split_whitespace().collect();
assert!(ac.contains("dataclassPoint("), "{all}");
assert!(ac.contains("funnorm("), "{all}");
assert!(ac.contains("this.x,this.y"), "{all}");
let point_block = all
.split("data class Point")
.nth(1)
.and_then(|rest| rest.split("fun interface").next())
.expect("Point class block");
assert_eq!(point_block.matches("companion object").count(), 1, "{all}");
let pb: String = point_block.split_whitespace().collect();
assert!(pb.contains("funorigin("), "{all}");
assert!(pb.contains("funfromParts("), "{all}");
}
#[test]
fn unsigned_scalars_use_lossless_kotlin_surface_and_raw_jni_wires() {
let loc = myflat_loc();
let items: Vec<(syn::Item, SourceLocation)> = vec![
(
syn::Item::Struct(syn::parse_quote!(
pub struct Unsigned {
pub byte: u8,
pub short: u16,
pub int: u32,
pub long: u64,
pub maybe_long: Option<u64>,
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn unsigned_round_trip(
byte: u8,
short: u16,
int: u32,
long: u64,
maybe_long: Option<u64>,
) -> Unsigned {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn unsigned_callback(f: impl Fn(u64) + Send + Sync + 'static) {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn unsigned_data_maybe(value: &Unsigned) -> Option<u64> {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn unsigned_result(value: u64) -> Result<u64, String> {
unimplemented!()
}
)),
loc,
),
];
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(
crate::package!()
.class(crate::data_class!(Unsigned))
.fun(prebindgen_registry::fun!(unsigned_round_trip))
.fun(prebindgen_registry::fun!(unsigned_data_maybe))
.fun(prebindgen_registry::fun!(unsigned_callback))
.fun(prebindgen_registry::fun!(unsigned_result)),
);
let dir = unique_test_dir("jnigen_unsigned");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let generation = jni.build_with(registry).expect("resolve");
let rust_path = generation
.write_rust(dir.join("gen.rs"))
.expect("write_rust");
let rust = std::fs::read_to_string(rust_path).unwrap();
let rc: String = rust.split_whitespace().collect();
assert!(rc.contains("u8::try_from(*v)"), "{rust}");
assert!(rc.contains("u16::try_from(*v)"), "{rust}");
assert!(rc.contains("u32::try_from(*v)"), "{rust}");
assert!(rc.contains("*vas::core::primitive::u64"), "{rust}");
let paths = generation
.write_kotlin(&dir.join("kotlin"))
.expect("write_kotlin");
let kotlin = paths
.iter()
.map(|p| std::fs::read_to_string(p).unwrap())
.collect::<Vec<_>>()
.join("\n");
let kc: String = kotlin.split_whitespace().collect();
assert!(kc.contains("byte:Int"), "{kotlin}");
assert!(kc.contains("short:Int"), "{kotlin}");
assert!(kc.contains("int:Long"), "{kotlin}");
assert!(kc.contains("long:ULong"), "{kotlin}");
assert!(kc.contains("maybeLong:ULong?"), "{kotlin}");
assert!(kc.contains("externalfununsignedRoundTrip("), "{kotlin}");
assert!(kc.contains("long:Long"), "{kotlin}");
assert!(kc.contains("maybeLong:Long?"), "{kotlin}");
assert!(kc.contains("long.toLong()"), "{kotlin}");
assert!(kc.contains("maybeLong?.toLong()"), "{kotlin}");
assert!(kc.contains(".toULong()"), "{kotlin}");
assert!(kc.contains("valueMaybeLongPresent:Boolean"), "{kotlin}");
assert!(kc.contains("valueMaybeLongValue:Long"), "{kotlin}");
assert!(kc.contains("value.maybeLong!=null"), "{kotlin}");
assert!(
rc.contains("value_maybe_long_present:jni::sys::jboolean"),
"{rust}"
);
assert!(kc.contains("funrun(u64:ULong)"), "{kotlin}");
assert!(kc.contains("funrun(u64:Long)"), "{kotlin}");
assert!(kc.contains("u64.toULong()"), "{kotlin}");
assert!(kc.contains("fununsignedResult(value:ULong"), "{kotlin}");
assert!(kc.contains("):ULong"), "{kotlin}");
}
#[test]
fn data_class_properties_match_their_from_parts_params() {
let loc = myflat_loc();
let items: Vec<(syn::Item, SourceLocation)> = vec![
(
syn::Item::Struct(syn::parse_quote!(
pub struct Child {
pub n: i64,
}
)),
loc.clone(),
),
(
syn::Item::Enum(syn::parse_quote!(
pub enum Level {
Low = 0,
High = 1,
}
)),
loc.clone(),
),
(
syn::Item::Struct(syn::parse_quote!(
pub struct Handle {
pub v: i64,
}
)),
loc.clone(),
),
(
syn::Item::Struct(syn::parse_quote!(
pub struct Bag {
pub handle: Handle,
pub child: Child,
pub level: Level,
pub note: Option<String>,
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn bag_make() -> Bag {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn bag_take(b: Bag) -> i64 {
unimplemented!()
}
)),
loc.clone(),
),
];
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(
crate::package!()
.class(crate::ptr_class!(Handle))
.class(crate::data_class!(Child))
.class(crate::enum_class!(Level))
.class(crate::data_class!(Bag))
.fun(prebindgen_registry::fun!(bag_make))
.fun(prebindgen_registry::fun!(bag_take)),
);
let dir = unique_test_dir("jnigen_data_class_props");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let generation = jni.build_with(registry).expect("resolve");
let kotlin = generation
.write_kotlin(&dir.join("kotlin"))
.unwrap()
.iter()
.map(|path| std::fs::read_to_string(path).unwrap())
.collect::<Vec<_>>()
.join("\n");
let kc: String = kotlin.split_whitespace().collect();
assert!(kc.contains("valhandle:Handle"), "{kotlin}");
assert!(kc.contains("valchild:Child"), "{kotlin}");
assert!(kc.contains("vallevel:Level"), "{kotlin}");
assert!(kc.contains("valnote:String?"), "{kotlin}");
assert!(kc.contains("AutoCloseable"), "{kotlin}");
assert!(kc.contains("funclose()"), "{kotlin}");
assert!(kc.contains("Bag(Handle(handle)"), "{kotlin}");
assert!(kc.contains("Child.fromParts(child_n)"), "{kotlin}");
assert!(kc.contains("Level.fromInt(level)"), "{kotlin}");
}
#[test]
fn array_length_const_is_qualified_without_touching_locals() {
check_array_length_qualification(myflat_loc(), "myflat");
}
#[test]
fn array_length_qualification_falls_back_to_crate_without_an_origin() {
check_array_length_qualification(SourceLocation::default(), "crate");
}
fn check_array_length_qualification(loc: SourceLocation, module: &str) {
let mut items: Vec<(syn::Item, SourceLocation)> = Vec::new();
items.push((
syn::Item::Const(syn::parse_quote!(
#[allow(non_upper_case_globals)]
pub const env: usize = 4;
)),
loc.clone(),
));
items.push((
syn::Item::Struct(syn::parse_quote!(
pub struct Blob {
pub bytes: [u8; env],
}
)),
loc.clone(),
));
items.push((
syn::Item::Fn(syn::parse_quote!(
pub fn blob_echo(b: Blob) -> Blob {
unimplemented!()
}
)),
loc.clone(),
));
let registry = crate::test_util::reg_from_items(declare_referenced(items)).unwrap();
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(
crate::package!("blob")
.class(crate::data_class!(Blob))
.fun(prebindgen_registry::fun!(blob_echo)),
);
let dir = unique_test_dir(&format!("jnigen_array_len_const_{module}"));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let generation = jni.build_with(registry).unwrap();
let rust_path = generation.write_rust(dir.join("gen.rs")).unwrap();
let rust = std::fs::read_to_string(rust_path).unwrap();
let rc: String = rust.split_whitespace().collect();
assert!(rc.contains(&format!("[u8;{module}::env]")), "{rust}");
assert!(rc.contains("env.byte_array_from_slice"), "{rust}");
assert!(
!rc.contains(&format!("{module}::env.byte_array_from_slice")),
"{rust}"
);
assert!(!rc.contains(&format!("{module}::env,")), "{rust}");
assert!(!rc.contains(&format!("&mut{module}::env")), "{rust}");
}
#[test]
fn a_borrowed_transparent_sequence_wrapper_is_not_decoded_as_a_vec() {
let loc = prebindgen::SourceLocation::default();
let items: Vec<(syn::Item, prebindgen::SourceLocation)> = vec![(
syn::Item::Fn(syn::parse_quote!(
pub fn z_take_boxed(v: &Box<Vec<i32>>) -> i64 {
v.len() as i64
}
)),
loc.clone(),
)];
let decls = crate::package!("ops").fun(crate::FunctionDecl::new(
syn::parse_str("z_take_boxed").unwrap(),
));
let registry = crate::test_util::reg_from_items(items).expect("index");
let err = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(decls)
.build_with(registry)
.expect_err("a borrowed transparent sequence wrapper has no conversion");
let msg = err.to_string();
assert!(
msg.contains("Box < Vec < i32 > >"),
"the refusal must name the wrapper spelling the binding cannot convert:\n{msg}"
);
}
#[test]
fn the_enum_probe_sees_through_wrappers_a_spelling_key_misses() {
use prebindgen_registry::flat;
let loc = myflat_loc();
let items: Vec<(syn::Item, SourceLocation)> = vec![
(
syn::Item::Enum(syn::parse_quote!(
pub enum Priority {
Low = 1,
High = 2,
}
)),
loc.clone(),
),
(
syn::Item::Struct(syn::parse_quote!(
pub struct Probe {
pub plain: Priority,
pub borrowed: Box<Priority>,
pub optional: Option<Priority>,
pub boxed_optional: Box<Option<Priority>>,
pub optional_borrow: Option<Box<Priority>>,
pub run: Vec<Priority>,
pub unrelated: i64,
}
)),
loc.clone(),
),
];
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let gen = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(crate::package!().class(crate::enum_class!(Priority)))
.build_with(registry)
.expect("resolve");
let (ext, registry) = (gen.declarations(), gen.registry());
let flat::Type::Struct(probe) = registry.flat().declared_type("Probe").expect("indexed") else {
panic!("Probe is a struct");
};
let field = |name: &str| {
&probe
.fields
.iter()
.find(|f| f.name.as_ref().is_some_and(|n| n == name))
.unwrap_or_else(|| panic!("field `{name}`"))
.ty
};
for name in [
"plain",
"borrowed",
"optional",
"boxed_optional",
"optional_borrow",
] {
let reading = field(name);
assert!(
ext.is_kotlin_enum_reading(reading),
"`{name}` holds a declared Kotlin enum, however it is wrapped — the \
probe peels the model's layers, so it must say so"
);
}
for name in ["run", "unrelated"] {
assert!(
!ext.is_kotlin_enum_reading(field(name)),
"`{name}` is not an enum value: a run of enums is a `List`, and the \
probe must not peel the sequence layer to reach the element"
);
}
assert!(
!ext.is_kotlin_enum_key(&field("borrowed").key()),
"if the spelling key ever started seeing through `Box`, this test would \
stop distinguishing the two probes"
);
assert!(ext.is_kotlin_enum_key(&field("plain").key()));
}
#[test]
fn a_transparently_wrapped_option_takes_the_present_value_pair_and_is_rebuilt() {
let loc = myflat_loc();
let items: Vec<(syn::Item, SourceLocation)> = vec![
(
syn::Item::Enum(syn::parse_quote!(
pub enum Mode {
A,
B,
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn z_bare(mode: Option<Mode>) {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn z_boxed(mode: Box<Option<Mode>>) {
unimplemented!()
}
)),
loc.clone(),
),
];
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(crate::package!().class(crate::enum_class!(Mode)))
.package(
crate::package!("cfg")
.fun(prebindgen_registry::fun!(z_bare))
.fun(prebindgen_registry::fun!(z_boxed)),
);
let dir = unique_test_dir("jnigen_wrapped_optscalar");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let Ok(gen) = jni.build_with(registry) else {
return;
};
let kdir = dir.join("kotlin");
let paths = gen.write_kotlin(&kdir).expect("write_kotlin");
let kotlin: String = paths
.iter()
.map(|p| std::fs::read_to_string(p).unwrap())
.collect::<Vec<_>>()
.join("\n");
let kc: String = kotlin.split_whitespace().collect();
assert!(
kc.contains("zBare(modePresent:Boolean,modeValue:Int"),
"the bare `Option<Mode>` must still cross as (present, value) — \
otherwise this test proves nothing about the wrapped one:\n{kotlin}"
);
assert!(
kc.contains("zBoxed(modePresent:Boolean,modeValue:Int"),
"`Box<Option<Mode>>` must take the same present/value lowering as its \
bare twin — the model erases the `Box`, and the emitter puts it back \
rather than declining the shape:\n{kotlin}"
);
let rust = std::fs::read_to_string(gen.write_rust(dir.join("g.rs")).expect("write_rust"))
.expect("read rust");
let rc: String = rust.split_whitespace().collect();
assert!(
rc.contains("letmode=::std::boxed::Box::new(if"),
"the rebuilt `Option` must be re-wrapped for the spelling:\n{rust}"
);
assert_eq!(
rc.matches("::std::boxed::Box::new(if").count(),
1,
"only the wrapped spelling gets a `Box::new`; the bare twin builds the \
`Option` and passes it as is:\n{rust}"
);
}
#[test]
fn an_outer_wrapper_around_a_reference_is_seen_before_the_layers_are_read() {
let loc = myflat_loc();
let items: Vec<(syn::Item, SourceLocation)> = vec![
(
syn::Item::Struct(syn::parse_quote!(
pub struct Foo {
pub id: i64,
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn put_bare(v: &[Foo]) {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn put_wrapped(v: Box<&Vec<Foo>>) {
unimplemented!()
}
)),
loc.clone(),
),
];
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(
crate::package!("foo")
.class(crate::data_class!(Foo))
.fun(prebindgen_registry::fun!(put_bare))
.fun(prebindgen_registry::fun!(put_wrapped)),
);
let dir = unique_test_dir("jnigen_outer_wrapper_ref");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let Ok(gen) = jni.build_with(registry) else {
return;
};
let kdir = dir.join("kotlin");
let paths = gen.write_kotlin(&kdir).expect("write_kotlin");
let kotlin: String = paths
.iter()
.map(|p| std::fs::read_to_string(p).unwrap())
.collect::<Vec<_>>()
.join("\n");
let kc: String = kotlin.split_whitespace().collect();
assert!(
kc.contains("fooVecNew"),
"the bare `&[Foo]` must still take the Vec-build path — otherwise this \
test proves nothing about the wrapped one:\n{kotlin}"
);
assert!(
kc.contains("val__vec_v=JNINative.fooVecNew(v.size)"),
"{kotlin}"
);
let wrapped_body = kc
.split("publicfunputWrapped(")
.nth(1)
.map(|s| s.split("publicfun").next().unwrap_or(s).to_string())
.unwrap_or_default();
assert!(
!wrapped_body.contains("fooVecNew"),
"`Box<&Vec<Foo>>` took the Vec-build path, which hands the source fn a \
`&[Foo]` built from a transient Vec while the parameter spells \
`Box<&Vec<Foo>>` — an E0308 in the generated crate:\n{kotlin}"
);
}
#[test]
fn both_spellings_of_a_borrowed_run_get_the_vec_borrow() {
let loc = myflat_loc();
let items: Vec<(syn::Item, SourceLocation)> = vec![
(
syn::Item::Struct(syn::parse_quote!(
pub struct Foo {
pub id: i64,
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn put_slice(v: &[Foo]) {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn put_ref_vec(v: &Vec<Foo>) {
unimplemented!()
}
)),
loc.clone(),
),
];
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(
crate::package!("foo")
.class(crate::data_class!(Foo))
.fun(prebindgen_registry::fun!(put_slice))
.fun(prebindgen_registry::fun!(put_ref_vec)),
);
let dir = unique_test_dir("jnigen_borrowed_run_spellings");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let gen = jni.build_with(registry).expect("resolve");
let kdir = dir.join("kotlin");
let paths = gen.write_kotlin(&kdir).expect("write_kotlin");
let kotlin: String = paths
.iter()
.map(|p| std::fs::read_to_string(p).unwrap())
.collect::<Vec<_>>()
.join("\n");
let kc: String = kotlin.split_whitespace().collect();
let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust");
let rust = std::fs::read_to_string(&rust_path).expect("read rust");
let rc: String = rust.split_whitespace().collect();
for f in ["publicfunputSlice(", "publicfunputRefVec("] {
let body = kc
.split(f)
.nth(1)
.map(|s| s.split("publicfun").next().unwrap_or(s).to_string())
.unwrap_or_default();
assert!(
body.contains("fooVecNew"),
"`{f}` must take the Vec-build path — both spellings are one type to \
the model:\n{kotlin}"
);
}
assert_eq!(
rc.matches("letv=unsafe{&*(v_handleas*constVec<myflat::Foo>)};")
.count(),
2,
"both borrowed runs must get the same unascribed `&Vec<Foo>` local:\n{rust}"
);
assert!(
!rc.contains("letv:&[myflat::Foo]="),
"ascribing `&[Foo]` coerces at the `let`, so a `&Vec<Foo>` parameter \
gets a `&[Foo]` and the generated crate does not build (E0308):\n{rust}"
);
}
#[test]
fn a_wrapped_vec_element_keeps_the_push_path_and_shares_one_trio() {
let loc = myflat_loc();
let items: Vec<(syn::Item, SourceLocation)> = vec![
(
syn::Item::Struct(syn::parse_quote!(
pub struct Foo {
pub id: i64,
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn put_bare(v: Vec<Foo>) {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn put_boxed_elem(v: Vec<Box<Foo>>) {
unimplemented!()
}
)),
loc.clone(),
),
(
syn::Item::Fn(syn::parse_quote!(
pub fn put_boxed_elem_slice(v: &[Box<Foo>]) {
unimplemented!()
}
)),
loc.clone(),
),
];
let registry =
crate::test_util::reg_from_items(declare_referenced(items)).expect("index items");
let jni = JniGenBuilder::new()
.set_package_prefix("io.test.jni")
.package(
crate::package!("foo")
.class(crate::data_class!(Foo))
.fun(prebindgen_registry::fun!(put_bare))
.fun(prebindgen_registry::fun!(put_boxed_elem))
.fun(prebindgen_registry::fun!(put_boxed_elem_slice)),
);
let dir = unique_test_dir("jnigen_wrapped_vec_elem");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let gen = jni.build_with(registry).expect("resolve");
let kdir = dir.join("kotlin");
let paths = gen.write_kotlin(&kdir).expect("write_kotlin");
let kotlin: String = paths
.iter()
.map(|p| std::fs::read_to_string(p).unwrap())
.collect::<Vec<_>>()
.join("\n");
let kc: String = kotlin.split_whitespace().collect();
let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust");
let rust = std::fs::read_to_string(&rust_path).expect("read rust");
let rc: String = rust.split_whitespace().collect();
assert!(
kc.contains("val__vec_v=JNINative.fooVecNew(v.size)"),
"the bare `Vec<Foo>` must take the Vec-build path — otherwise this test \
proves nothing about the wrapped one:\n{kotlin}"
);
let boxed_body = kc
.split("publicfunputBoxedElem(")
.nth(1)
.map(|s| s.split("publicfun").next().unwrap_or(s).to_string())
.unwrap_or_default();
assert!(
boxed_body.contains("fooVecNew"),
"`Vec<Box<Foo>>` fell back to the general `JObject` converter — a `Box` \
the model erases must not cost the push-helper path (#296):\n{kotlin}"
);
assert_eq!(
kc.matches("externalfunfooVecNew(cap:Int):Long").count(),
1,
"the two spellings must share ONE helper trio — a per-spelling trio \
would emit `fooVecNew` twice and collide:\n{kotlin}"
);
assert!(
rc.contains("Vec::<myflat::Foo>::with_capacity"),
"the trio must store the CANONICAL element:\n{rust}"
);
assert!(
rc.contains(".map(|__e|::std::boxed::Box::new(__e))"),
"the element wrapper must go back on where the Vec is consumed:\n{rust}"
);
assert_eq!(
rc.matches(".map(|__e|::std::boxed::Box::new(__e))").count(),
1,
"only the wrapped spelling maps its elements:\n{rust}"
);
let slice_body = kc
.split("publicfunputBoxedElemSlice(")
.nth(1)
.map(|s| s.split("publicfun").next().unwrap_or(s).to_string())
.unwrap_or_default();
assert!(
!slice_body.contains("fooVecNew"),
"`&[Box<Foo>]` must keep the general converter path — serving it would \
mean consuming the Vec the arm exists to borrow:\n{kotlin}"
);
}