use std::{ ffi::OsString, fs, panic, path::{Path, PathBuf}, }; use flapigen::{rustfmt_cnt, CppConfig, Generator, JavaConfig, LanguageConfig, RustEdition}; use log::warn; use syn::Token; use tempfile::tempdir; include!(concat!(env!("OUT_DIR"), "/test_expectations.rs")); #[test] fn test_expectations_class_with_methods_without_constructor() { let _ = env_logger::try_init(); let langs = [ForeignLang::Java, ForeignLang::Cpp]; for lang in &langs { let name = format!("class_with_methods_without_constructor {:?}", lang); parse_code( &name, Source::Str( r#" foreign_class!(class Foo { }); "#, ), *lang, ) .expect(&name); } for lang in &langs { let name = format!("class_with_methods_without_constructor {:?}", lang); let ret = panic::catch_unwind(|| { parse_code( &name, Source::Str( r#" foreign_class!(class Foo { self_type SomeType; }); "#, ), *lang, ) .expect(&name) }); assert!(ret.is_err()); } for lang in &langs { let result = panic::catch_unwind(|| { let name = format!("class_with_methods_without_constructor {:?}", lang); parse_code( &name, Source::Str( r#" foreign_class!(class Foo { self_type SomeType; method SomeType::f(&self) -> i32; }); "#, ), *lang, ) .expect(&name); }); assert!(result.is_err()); } } #[test] fn test_foreign_typemap_not_direct_intermediate() { let name = "test_foreign_typemap_not_direct_intermediate"; let _ = env_logger::try_init(); for lang in &[ForeignLang::Java, ForeignLang::Cpp] { let ret = panic::catch_unwind(|| { parse_code( name, Source::Str( r###" foreign_typemap!( ($p:r_type) Type2 => Type3 { $out = $p }; ($p:f_type) => "Type3"; ); foreign_typemap!( ($p:r_type) TypeX => Type2 { $out = typex_to_type2($p) }; ($p:f_type) => "FType4" r#" $out = f_type2_to_type4($p); "#; ); foreign_class!(class Foo { fn f1() -> TypeX; }); "###, ), *lang, ) .unwrap_or_else(|err| panic!("Test {} failed for lang {:?}: {}", name, lang, err)); }); assert!(ret.is_err()); } } #[test] fn test_callback_without_self_err() { let _ = env_logger::try_init(); for lang in &[ForeignLang::Java, ForeignLang::Cpp] { let result = panic::catch_unwind(|| { let name = format!("test_callback_without_self_err {:?}", lang); parse_code( &name, Source::Str( r#" foreign_interface! (interface FooBar { self_type Foo; bar = Foo::bar(); }); "#, ), *lang, ) .expect(&name); }); println!("result: {:?}", result); assert!(result.is_err()); } } #[test] fn test_expectations_parse_without_self_type_err() { let _ = env_logger::try_init(); for lang in &[ForeignLang::Java, ForeignLang::Cpp] { println!("test_parse_without_self_type_err: lang {:?}", lang); let result = panic::catch_unwind(|| { let name = format!("test_parse_without_self_type_err {:?}", lang); parse_code( &name, Source::Str( r#" foreign_class!(class DownloadItem { self_type DownloadItem; private constructor = empty; method DownloadItem::total_size(&self) -> u64; }); foreign_class!(class Document { constructor Document::new(remote: DownloadItem) -> Document; method Document::remote(&self) -> bool; }); "#, ), *lang, ) .expect(&name); }); assert!(result.is_err()); } } #[test] fn test_expectations_foreign_vec_as_arg() { let _ = env_logger::try_init(); let name = "foreign_vec_as_arg"; let src = r#" foreign_class!( #[derive(Clone)] class Boo { self_type Boo; constructor Boo::default() -> Boo; fn Boo::clone(&self) -> Boo; }); foreign_class!(class FooImpl { self_type Foo<'a>; constructor Foo::create() -> Foo<'a>; fn Foo::set_alternate_boarding(&mut self, p: Vec); alias setAlternateBoarding; }); "#; for _ in 0..100 { let cpp_code = parse_code(name, Source::Str(src), ForeignLang::Cpp).expect("parse failed"); println!("c/c++: {}", cpp_code.foreign_code); assert!(cpp_code .foreign_code .contains("void setAlternateBoarding(RustForeignVecBoo p)")); let java_code = parse_code(name, Source::Str(src), ForeignLang::Java).expect("parse failed"); println!("Java: {}", java_code.foreign_code); assert!(java_code .foreign_code .contains("void setAlternateBoarding(@NonNull Boo [] p)")); } } #[test] fn test_foreign_enum_vs_int() { let _ = env_logger::try_init(); let name = "foreign_enum_vs_int"; let src = r#" foreign_enum!(enum MyEnum { ITEM1 = MyEnum::Item1, ITEM2 = MyEnum::Item2, ITEM3 = MyEnum::Item3, }); foreign_class!(class TestEnumClass { self_type Moo; constructor Moo::default() -> Moo; fn Moo::f1(&mut self, v: MyEnum) -> i32; fn Moo::next_enum(v: MyEnum) -> MyEnum; }); "#; for _ in 0..10 { let java_code = parse_code(name, Source::Str(src), ForeignLang::Java).unwrap(); println!("{}", java_code.foreign_code); assert!(java_code.foreign_code.contains("int f1(@NonNull MyEnum")); let _cpp_code = parse_code(name, Source::Str(src), ForeignLang::Cpp).unwrap(); } } #[test] fn test_return_result_type_with_object() { let _ = env_logger::try_init(); let name = "return_result_type_with_object"; let src = r#" foreign_class!(class Position { self_type GnssInfo; private constructor create_position() -> GnssInfo; method Position::getLatitude(&self) -> f64; }); foreign_class!(class LocationService { static_method LocationService::position() -> Result; static_method LocationService::do_something() -> Result<(), String>; }); "#; for i in 0..10 { println!("iter {}", i); let java_code = parse_code(name, Source::Str(src), ForeignLang::Java).unwrap(); println!("{}", java_code.foreign_code); assert!(java_code.foreign_code.contains( r#"public static @NonNull Position position() throws Exception { long ret = do_position(); Position convRet = new Position(InternalPointerMarker.RAW_PTR, ret); return convRet; } private static native long do_position() throws Exception;"# )); let cpp_code = parse_code(name, Source::Str(src), ForeignLang::Cpp).unwrap(); println!("c/c++: {}", cpp_code.foreign_code); assert!(cpp_code .foreign_code .contains("static std::variant position()")); } } #[test] fn test_return_foreign_class_ref() { let _ = env_logger::try_init(); for _ in 0..10 { let cpp_code = parse_code( "return_foreign_class_ref", Source::Str( r#" foreign_class!(class Boo { self_type Boo; constructor create_boo() -> Boo; method Boo::test(&self, _: bool) -> f32; method Boo::set_a(&mut self, _: i32); }); foreign_class!(class Moo { self_type Moo; constructor TestPathAndResult::default() -> Moo; method TestPathAndResult::get_boo(&self) -> &Boo; method TestReferences::update_boo(&mut self, boo: &Boo); }); "#, ), ForeignLang::Cpp, ) .unwrap(); println!("c/c++: {}", cpp_code.foreign_code); assert!(cpp_code.foreign_code.contains("BooRef get_boo() const")); assert!(cpp_code .foreign_code .contains("void Moo_update_boo(MooOpaque * const self, const BooOpaque * boo);")); assert!(cpp_code .foreign_code .contains("void update_boo(const Boo & boo)")); } } #[test] fn test_foreign_interface_cpp() { let _ = env_logger::try_init(); let name = "foreign_interface_cpp"; let src = r#" foreign_class!(class Uuid { self_type Uuid; private constructor uuid_private_constructor() -> Uuid; static_method Uuid::new_v4() -> Uuid; }); foreign_interface!(interface RepoChangedCallback { self_type RepoChangedCallback; on_save = RepoChangedCallback::on_save(&self, uuid: &Uuid); on_remove = RepoChangedCallback::on_remove(&self, uuid: &Uuid); }); "#; for _ in 0..100 { let cpp_code = parse_code(name, Source::Str(src), ForeignLang::Cpp).unwrap(); println!("c/c++: {}", cpp_code.foreign_code); assert!(cpp_code .foreign_code .contains("virtual void on_save(UuidRef uuid) const noexcept = 0;")); assert!(cpp_code .foreign_code .contains("virtual void on_remove(UuidRef uuid) const noexcept = 0;")); assert!(cpp_code.foreign_code.contains( r#" static void c_on_save(const UuidOpaque * uuid, void *opaque) { assert(opaque != nullptr); auto pi = static_cast(opaque); pi->on_save(UuidRef{ static_cast(uuid) }); } static void c_on_remove(const UuidOpaque * uuid, void *opaque) { assert(opaque != nullptr); auto pi = static_cast(opaque); pi->on_remove(UuidRef{ static_cast(uuid) }); } "# )); } } #[test] fn test_derive_extension_usage() { let _ = env_logger::try_init(); let rust_src = r#" foreign_class!( #[derive(QObject)] class MyObj { self_type MyObj; private constructor MyObj::new() -> MyObj; #[Q_INVOKABLE] fn MyObj::f(); }); foreign_enum!( #[derive(EnumClass)] /// enum comment enum MyEnum { A = MyEnum::A, B = MyEnum::B, } ); "#; let tmp_dir = tempdir().expect("Can not create tmp directory"); let swig_gen = Generator::new(LanguageConfig::CppConfig(CppConfig::new( tmp_dir.path().into(), "org_examples".into(), ))) .with_pointer_target_width(64) .register_class_attribute_callback("QObject", |code, class_name| { println!("class attribute callback class_name {}", class_name); let include = b"#include"; let mut last_pos = 0; let mut search_shift = 0; while let Some(pos) = find_subsequence(&code[(last_pos + search_shift)..], include) { last_pos += pos + search_shift; search_shift = 1; } let last_include_pos = last_pos; let new_line = &code[last_include_pos..].iter().position(|x| *x == b'\n'); let new_line_pos = last_include_pos + new_line.unwrap(); let addon = br##" #include "##; code.splice(new_line_pos..new_line_pos, addon.iter().copied()); let needle = format!("class {}Wrapper {{", class_name); let class_pos = find_subsequence(&code, needle.as_bytes()).unwrap(); let end_pos = class_pos + needle.as_bytes().len(); let new_code = format!( r#"class {}Wrapper : public QObject {{ Q_OBJECT"#, class_name ); code.splice(class_pos..end_pos, new_code.as_bytes().iter().copied()); }) .register_method_attribute_callback("Q_INVOKABLE", |code, ctx| { println!( "method attribute callback class {}, method {}", ctx.class_name, ctx.method_name ); let needle = match ctx.variant { flapigen::MethodVariant::Constructor => { panic!("unsupported"); } flapigen::MethodVariant::Method(_) => format!("void {}(", ctx.method_name), flapigen::MethodVariant::StaticMethod => format!("static void {}(", ctx.method_name), }; let pos = find_subsequence(&code, needle.as_bytes()).unwrap(); code.splice(pos..pos, b"Q_INVOKABLE ".iter().copied()); }) .register_enum_attribute_callback("EnumClass", |code, ctx| { println!("EnumClass callback, ctx {}", ctx); let needle = format!("enum {}", ctx); let pos = find_subsequence(&code, needle.as_bytes()).unwrap(); code.splice((pos + 5)..(pos + 5), b"class ".iter().copied()); }); let rust_code_path = tmp_dir.path().join("test.rs"); let rust_src_path = tmp_dir.path().join("src.rs"); fs::write(&rust_src_path, rust_src).unwrap(); swig_gen.expand("derive_extension_usage", rust_src_path, &rust_code_path); let _rust_code = fs::read_to_string(rust_code_path).unwrap(); let foreign_code = collect_code_in_dir(tmp_dir.path(), &[".h", ".hpp"]).unwrap(); println!("foreign_code: {}", foreign_code); assert!(foreign_code.contains( r##"#include "## )); assert!(foreign_code.contains( r#" class MyObjWrapper : public QObject { Q_OBJECT "# )); assert!(foreign_code.contains("Q_INVOKABLE static void f()")); assert!(foreign_code.contains( r#"//enum comment enum class MyEnum { A = 0, B = 1"# )); tmp_dir.close().unwrap(); } fn find_subsequence(haystack: &[T], needle: &[T]) -> Option where for<'a> &'a [T]: PartialEq, { haystack .windows(needle.len()) .position(|window| window == needle) } #[derive(PartialEq, Debug, Clone, Copy)] enum ForeignLang { Java, Cpp, } #[derive(Clone)] struct CodePair { rust_code: String, foreign_code: String, } struct PrintTestInfo { code_pair: CodePair, test_name: String, lang: ForeignLang, print_on_drop: bool, foreign_code_search_pattern: String, rust_pat: String, } impl PrintTestInfo { fn new(code_pair: CodePair, test_name: String, lang: ForeignLang) -> Self { PrintTestInfo { code_pair, test_name, lang, print_on_drop: true, foreign_code_search_pattern: String::new(), rust_pat: String::new(), } } fn success(&mut self) { self.print_on_drop = false; } } impl Drop for PrintTestInfo { fn drop(&mut self) { if self.print_on_drop { if !self.foreign_code_search_pattern.is_empty() { println!( "{} / {:?}: search foreign pat '{}'", self.test_name, self.lang, self.foreign_code_search_pattern ); } if !self.rust_pat.is_empty() { println!( "{} / {:?}: search rust pat '{}'", self.test_name, self.lang, self.rust_pat, ); } println!( "{} / {:?}: flapigen generated such foreign_code: {}", self.test_name, self.lang, self.code_pair.foreign_code ); println!( "{} / {:?}: flapigen generated such rust_code: {}", self.test_name, self.lang, rustfmt_without_errors(self.code_pair.rust_code.clone()), ); } } } #[derive(Debug)] struct Error { msg: String, } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.msg) } } impl From for Error { fn from(x: std::io::Error) -> Self { Error { msg: format!("io: {}", x), } } } impl From for Error { fn from(x: syn::Error) -> Self { Error { msg: format!("syn: {}", x), } } } impl std::error::Error for Error {} fn collect_code_in_dir(dir_with_code: &Path, exts: &[&str]) -> Result { let mut code = String::new(); for path in fs::read_dir(dir_with_code)? { let path = path?; if path.file_type()?.is_file() && exts .iter() .any(|ext| path.path().to_str().map_or(false, |x| x.ends_with(ext))) { code.push_str(format!("<<< generated file: {:?} >>>\n", path.file_name()).as_str()); code.push_str(&fs::read_to_string(path.path())?); code.push('\n'); code.push_str(format!(">>> end of file: {:?} <<<\n", path.file_name()).as_str()); } } Ok(code) } enum Source<'a> { Str(&'a str), Path(&'a Path), } fn parse_code(test_name: &str, rust_src: Source, lang: ForeignLang) -> Result { let tmp_dir = tempdir().expect("Can not create tmp directory"); let (swig_gen, ext_list): (Generator, &[&'static str]) = match lang { ForeignLang::Java => { let swig_gen = Generator::new(LanguageConfig::JavaConfig( JavaConfig::new(tmp_dir.path().into(), "org.example".into()) .use_null_annotation_from_package("android.support.annotation".into()), )) .with_pointer_target_width(64); (swig_gen, &[".java"]) } ForeignLang::Cpp => { let swig_gen = Generator::new(LanguageConfig::CppConfig(CppConfig::new( tmp_dir.path().into(), "org_examples".into(), ))) .with_pointer_target_width(64); (swig_gen, &[".h", ".hpp"]) } }; let rust_code_path = tmp_dir.path().join("test.rs"); match rust_src { Source::Path(rust_src_path) => swig_gen.expand(test_name, rust_src_path, &rust_code_path), Source::Str(rust_src) => { let rust_src_path = tmp_dir.path().join("src.rs"); fs::write(&rust_src_path, rust_src)?; swig_gen.expand(test_name, rust_src_path, &rust_code_path); } } let rust_code = fs::read_to_string(rust_code_path)?; let foreign_code = collect_code_in_dir(tmp_dir.path(), ext_list)?; tmp_dir.close()?; Ok(CodePair { rust_code, foreign_code, }) } struct ExpectationPatterns(Vec); impl syn::parse::Parse for ExpectationPatterns { fn parse(input: syn::parse::ParseStream) -> syn::Result { let lit_vec: syn::punctuated::Punctuated = syn::punctuated::Punctuated::parse_terminated(input)?; Ok(ExpectationPatterns( lit_vec.into_iter().map(|v| v.value()).collect(), )) } } fn parse_code_expectation(exp_path: &Path) -> Result, Error> { let patterns_str = fs::read_to_string(exp_path)?; let pats: ExpectationPatterns = syn::parse_str(&patterns_str)?; Ok(pats.0) } fn new_path(main_path: &Path, ext: &str) -> PathBuf { let base_name = main_path.file_stem().expect("name without extenstion"); let mut new_name: OsString = base_name.into(); new_name.push(ext); main_path.with_file_name(new_name) } fn check_expectation(test_name: &str, test_case: &Path, lang: ForeignLang) -> bool { let (main_ext, rust_ext) = match lang { ForeignLang::Cpp => (".cpp", ".cpp_rs"), ForeignLang::Java => (".java", ".java_rs"), }; let main_expectation = new_path(test_case, main_ext); if main_expectation.exists() { let code_pair = parse_code(&test_name, Source::Path(&test_case), lang).expect("parse_code failed"); let pats = parse_code_expectation(&main_expectation).expect("parsing of patterns failed"); let mut print_test_info = PrintTestInfo::new(code_pair.clone(), test_name.into(), lang); for pat in pats { print_test_info.foreign_code_search_pattern = pat.clone(); assert!(code_pair.foreign_code.contains(&pat)); } print_test_info.foreign_code_search_pattern.clear(); let rust_cpp_expectation = new_path(&test_case, rust_ext); if rust_cpp_expectation.exists() { let pats = parse_code_expectation(&rust_cpp_expectation).expect("parsing of patterns failed"); let pats: Vec = pats .into_iter() .map(|v| rustfmt_without_errors(v)) .collect(); let rust_code = rustfmt_without_errors(code_pair.rust_code); for pat in pats { print_test_info.rust_pat = pat.clone(); assert!(rust_code.contains(&pat)); } print_test_info.rust_pat.clear(); } print_test_info.success(); true } else { false } } fn rustfmt_without_errors(rust_code: String) -> String { let rust_code2 = rust_code.clone(); match rustfmt_cnt(rust_code.into_bytes(), RustEdition::Edition2018) { Ok(code) => String::from_utf8(code).expect("not valid utf-8"), Err(err) => { warn!("rustfmt failed: {}", err); rust_code2 } } }