From 8f9853b1f40b8c20338a29565d36f4b0b2f45ed2 Mon Sep 17 00:00:00 2001 From: mbyx Date: Wed, 16 Jul 2025 16:51:28 +0500 Subject: [PATCH] ctest: extract bindings from template into source --- ctest-next/src/generator.rs | 12 +- ctest-next/src/template.rs | 171 ++++++++++++++---- ctest-next/templates/test.c | 27 ++- ctest-next/templates/test.rs | 44 +++-- ctest-next/tests/input/hierarchy.out.c | 7 +- ctest-next/tests/input/hierarchy.out.rs | 8 +- ctest-next/tests/input/macro.out.c | 1 + .../tests/input/simple.out.with-renames.c | 13 +- .../tests/input/simple.out.with-renames.rs | 16 +- .../tests/input/simple.out.with-skips.c | 7 +- .../tests/input/simple.out.with-skips.rs | 8 +- 11 files changed, 219 insertions(+), 95 deletions(-) diff --git a/ctest-next/src/generator.rs b/ctest-next/src/generator.rs index d4d2b56b12ecc..e24ceed9811b6 100644 --- a/ctest-next/src/generator.rs +++ b/ctest-next/src/generator.rs @@ -605,6 +605,9 @@ impl TestGenerator { .map_err(GenerationError::OsError)? .write_all( RustTestTemplate::new(&ffi_items, self) + .map_err(|e| { + GenerationError::TemplateRender("Rust".to_string(), e.to_string()) + })? .render() .map_err(|e| { GenerationError::TemplateRender("Rust".to_string(), e.to_string()) @@ -619,6 +622,7 @@ impl TestGenerator { .map_err(GenerationError::OsError)? .write_all( CTestTemplate::new(&ffi_items, self) + .map_err(|e| GenerationError::TemplateRender("C".to_string(), e.to_string()))? .render() .map_err(|e| GenerationError::TemplateRender("C".to_string(), e.to_string()))? .as_bytes(), @@ -657,12 +661,12 @@ impl TestGenerator { } /// Maps Rust identifiers or types to C counterparts, or defaults to the original name. - pub(crate) fn map<'a>(&self, item: impl Into>) -> Result { + pub(crate) fn map<'a>(&self, item: impl Into>) -> String { let item = item.into(); if let Some(mapped) = self.mapped_names.iter().find_map(|f| f(&item)) { - return Ok(mapped); + return mapped; } - Ok(match item { + match item { MapInput::Const(c) => c.ident().to_string(), MapInput::Fn(f) => f.ident().to_string(), MapInput::Static(s) => s.ident().to_string(), @@ -672,6 +676,6 @@ impl TestGenerator { MapInput::StructType(ty) => format!("struct {ty}"), MapInput::UnionType(ty) => format!("union {ty}"), MapInput::Type(ty) => ty.to_string(), - }) + } } } diff --git a/ctest-next/src/template.rs b/ctest-next/src/template.rs index 49b0de0284d40..c489c0071c0c5 100644 --- a/ctest-next/src/template.rs +++ b/ctest-next/src/template.rs @@ -2,62 +2,170 @@ use askama::Template; use quote::ToTokens; use crate::ffi_items::FfiItems; -use crate::generator::GenerationError; use crate::translator::Translator; -use crate::{MapInput, Result, TestGenerator}; +use crate::{BoxStr, MapInput, Result, TestGenerator, TranslationError}; /// Represents the Rust side of the generated testing suite. #[derive(Template, Clone)] #[template(path = "test.rs")] -pub(crate) struct RustTestTemplate<'a> { - ffi_items: &'a FfiItems, - #[expect(unused)] - generator: &'a TestGenerator, +pub(crate) struct RustTestTemplate { + pub(crate) template: TestTemplate, +} + +impl RustTestTemplate { + pub(crate) fn new( + ffi_items: &FfiItems, + generator: &TestGenerator, + ) -> Result { + Ok(Self { + template: TestTemplate::new(ffi_items, generator)?, + }) + } } /// Represents the C side of the generated testing suite. #[derive(Template, Clone)] #[template(path = "test.c")] -pub(crate) struct CTestTemplate<'a> { - translator: Translator, - ffi_items: &'a FfiItems, - generator: &'a TestGenerator, +pub(crate) struct CTestTemplate { + pub(crate) template: TestTemplate, + pub(crate) headers: Vec, } -impl<'a> RustTestTemplate<'a> { - /// Create a new test template to test the given items. - pub(crate) fn new(ffi_items: &'a FfiItems, generator: &'a TestGenerator) -> Self { - Self { - ffi_items, - generator, - } +impl CTestTemplate { + pub(crate) fn new( + ffi_items: &FfiItems, + generator: &TestGenerator, + ) -> Result { + Ok(Self { + template: TestTemplate::new(ffi_items, generator)?, + headers: generator.headers.clone(), + }) } } -impl<'a> CTestTemplate<'a> { - /// Create a new test template to test the given items. - pub(crate) fn new(ffi_items: &'a FfiItems, generator: &'a TestGenerator) -> Self { - Self { +/// Stores all information necessary for generation of tests for all items. +#[derive(Clone, Debug, Default)] +pub(crate) struct TestTemplate { + pub(crate) const_cstr_tests: Vec, + pub(crate) const_tests: Vec, + pub(crate) test_idents: Vec, +} + +impl TestTemplate { + pub(crate) fn new( + ffi_items: &FfiItems, + generator: &TestGenerator, + ) -> Result { + let helper = TranslateHelper { ffi_items, - translator: Translator::new(), generator, + translator: Translator::new(), + }; + + /* Figure out which tests are to be generated. */ + // FIXME(ctest): Populate more test information, maybe extract into separate methods. + // The workflow would be to create a struct that stores information for the new test, + // and populating that struct here, so that the also things that have to be added to + // the test templates are the new tests parameterized by that struct. + + let mut const_tests = vec![]; + let mut const_cstr_tests = vec![]; + for constant in ffi_items.constants() { + if let syn::Type::Ptr(ptr) = &constant.ty { + let is_const_c_char_ptr = matches!( + &*ptr.elem, + syn::Type::Path(path) + if path.path.segments.last().unwrap().ident == "c_char" + && ptr.mutability.is_none() + ); + if is_const_c_char_ptr { + let item = TestCstr { + test_ident: cstr_test_ident(constant.ident()), + rust_ident: constant.ident().into(), + c_ident: helper.c_ident(constant).into(), + c_type: helper.c_type(constant)?.into(), + }; + const_cstr_tests.push(item) + } + } else { + let item = TestConst { + test_ident: const_test_ident(constant.ident()), + rust_ident: constant.ident.clone(), + rust_type: constant.ty.to_token_stream().to_string().into_boxed_str(), + c_ident: helper.c_ident(constant).into(), + c_type: helper.c_type(constant)?.into(), + }; + const_tests.push(item) + } } + + let mut test_idents = vec![]; + test_idents.extend(const_cstr_tests.iter().map(|test| test.test_ident.clone())); + test_idents.extend(const_tests.iter().map(|test| test.test_ident.clone())); + + Ok(Self { + const_cstr_tests, + const_tests, + test_idents, + }) } +} + +/// Information required to test a constant CStr. +#[derive(Clone, Debug)] +pub(crate) struct TestCstr { + pub(crate) test_ident: BoxStr, + pub(crate) rust_ident: BoxStr, + pub(crate) c_ident: BoxStr, + pub(crate) c_type: BoxStr, +} + +/// Information required to test a constant. +#[derive(Clone, Debug)] +pub(crate) struct TestConst { + pub(crate) test_ident: BoxStr, + pub(crate) rust_ident: BoxStr, + pub(crate) rust_type: BoxStr, + pub(crate) c_ident: BoxStr, + pub(crate) c_type: BoxStr, +} + +/// The Rust name of the cstr test. +/// +/// The C name of this same test is the same with `__` prepended. +fn cstr_test_ident(ident: &str) -> BoxStr { + format!("ctest_const_cstr_{ident}").into() +} + +/// The Rust name of the const test. +/// +/// The C name of this test is the same with `__` prepended. +fn const_test_ident(ident: &str) -> BoxStr { + format!("ctest_const_{ident}").into() +} +/// Wrap methods that depend on both ffi items and the generator. +struct TranslateHelper<'a> { + ffi_items: &'a FfiItems, + generator: &'a TestGenerator, + translator: Translator, +} + +impl<'a> TranslateHelper<'a> { /// Returns the equivalent C/Cpp identifier of the Rust item. - pub(crate) fn c_ident(&self, item: impl Into>) -> Result { + pub(crate) fn c_ident(&self, item: impl Into>) -> String { self.generator.map(item) } /// Returns the equivalent C/Cpp type of the Rust item. - pub(crate) fn c_type(&self, item: impl Into>) -> Result { - let item: MapInput<'a> = item.into(); + pub(crate) fn c_type(&self, item: impl Into>) -> Result { + let item: MapInput = item.into(); let (ident, ty) = match item { - MapInput::Const(c) => (c.ident(), self.translator.translate_type(&c.ty)), - MapInput::Alias(a) => (a.ident(), self.translator.translate_type(&a.ty)), - MapInput::Field(_, f) => (f.ident(), self.translator.translate_type(&f.ty)), - MapInput::Static(s) => (s.ident(), self.translator.translate_type(&s.ty)), + MapInput::Const(c) => (c.ident(), self.translator.translate_type(&c.ty)?), + MapInput::Alias(a) => (a.ident(), self.translator.translate_type(&a.ty)?), + MapInput::Field(_, f) => (f.ident(), self.translator.translate_type(&f.ty)?), + MapInput::Static(s) => (s.ident(), self.translator.translate_type(&s.ty)?), MapInput::Fn(_) => unimplemented!(), MapInput::Struct(_) => unimplemented!(), MapInput::StructType(_) => panic!("MapInput::StructType is not allowed!"), @@ -65,8 +173,6 @@ impl<'a> CTestTemplate<'a> { MapInput::Type(_) => panic!("MapInput::Type is not allowed!"), }; - let ty = ty.map_err(|e| GenerationError::TemplateRender("C".to_string(), e.to_string()))?; - let item = if self.ffi_items.contains_struct(ident) { MapInput::StructType(&ty) } else if self.ffi_items.contains_union(ident) { @@ -74,6 +180,7 @@ impl<'a> CTestTemplate<'a> { } else { MapInput::Type(&ty) }; - self.generator.map(item) + + Ok(self.generator.map(item)) } } diff --git a/ctest-next/templates/test.c b/ctest-next/templates/test.c index ef7f09a3e019b..b0a1aa9f282c9 100644 --- a/ctest-next/templates/test.c +++ b/ctest-next/templates/test.c @@ -1,26 +1,37 @@ /* This file was autogenerated by ctest; do not modify directly */ {#- ↑ Doesn't apply here, this is the template! +#} +{%- let ctx = self.template +%} + #include #include #include #include -{%- for header in generator.headers +%} +{%- for header in self.headers +%} + #include <{{ header }}> {%- endfor +%} -{%- for constant in ffi_items.constants() +%} -{%- let ident = constant.ident() +%} -{%- let c_type = self.c_type(*constant)? +%} -{%- let c_ident = self.c_ident(*constant)? +%} +{%- for const_cstr in ctx.const_cstr_tests +%} + +static {{ const_cstr.c_type }} ctest_const_{{ const_cstr.rust_ident }}_val_static = {{ const_cstr.c_ident }}; + +// Define a function that returns a pointer to the value of the constant to test. +// This will later be called on the Rust side via FFI. +{{ const_cstr.c_type }}* __{{ const_cstr.test_ident }}(void) { + return &ctest_const_{{ const_cstr.rust_ident }}_val_static; +} +{%- endfor +%} + +{%- for constant in ctx.const_tests +%} -static {{ c_type }} __test_const_{{ ident }}_val = {{ c_ident }}; +static {{ constant.c_type }} ctest_const_{{ constant.rust_ident }}_val_static = {{ constant.c_ident }}; // Define a function that returns a pointer to the value of the constant to test. // This will later be called on the Rust side via FFI. -{{ c_type }}* __test_const_{{ ident }}(void) { - return &__test_const_{{ ident }}_val; +{{ constant.c_type }}* __{{ constant.test_ident }}(void) { + return &ctest_const_{{ constant.rust_ident }}_val_static; } {%- endfor +%} diff --git a/ctest-next/templates/test.rs b/ctest-next/templates/test.rs index cd8cf4caf2de3..896da20e31d50 100644 --- a/ctest-next/templates/test.rs +++ b/ctest-next/templates/test.rs @@ -1,6 +1,8 @@ /* This file was autogenerated by ctest; do not modify directly */ {#- ↑ Doesn't apply here, this is the template! +#} +{%- let ctx = self.template +%} + /// As this file is sometimes built using rustc, crate level attributes /// are not allowed at the top-level, so we hack around this by keeping it /// inside of a module. @@ -42,51 +44,47 @@ mod generated_tests { } } - {%- for constant in ffi_items.constants() +%} - {%- let ty = constant.ty.to_token_stream().to_string() +%} - {%- let ident = constant.ident() +%} - - {%- if ty == "* const c_char" +%} + {%- for const_cstr in ctx.const_cstr_tests +%} // Test that the string constant is the same in both Rust and C. // While fat pointers can't be translated, we instead use * const c_char. - pub fn const_{{ ident }}() { + pub fn {{ const_cstr.test_ident }}() { extern "C" { - fn __test_const_{{ ident }}() -> *const *const u8; + fn __{{ const_cstr.test_ident }}() -> *const *const u8; } - let val = {{ ident }}; + let val = {{ const_cstr.rust_ident }}; unsafe { - let ptr = *__test_const_{{ ident }}(); + let ptr = *__{{ const_cstr.test_ident }}(); let val = CStr::from_ptr(ptr.cast::()); - let val = val.to_str().expect("const {{ ident }} not utf8"); + let val = val.to_str().expect("const {{ const_cstr.rust_ident }} not utf8"); let c = ::std::ffi::CStr::from_ptr(ptr as *const _); - let c = c.to_str().expect("const {{ ident }} not utf8"); - check_same(val, c, "{{ ident }} string"); + let c = c.to_str().expect("const {{ const_cstr.rust_ident }} not utf8"); + check_same(val, c, "{{ const_cstr.rust_ident }} string"); } } + {%- endfor +%} - {%- else +%} + {%- for constant in ctx.const_tests +%} // Test that the value of the constant is the same in both Rust and C. // This performs a byte by byte comparision of the constant value. - pub fn const_{{ ident }}() { + pub fn {{ constant.test_ident }}() { extern "C" { - fn __test_const_{{ ident }}() -> *const {{ ty }}; + fn __{{ constant.test_ident }}() -> *const {{ constant.rust_type }}; } - let val = {{ ident }}; + let val = {{ constant.rust_ident }}; unsafe { let ptr1 = ptr::from_ref(&val).cast::(); - let ptr2 = __test_const_{{ ident }}().cast::(); - let ptr1_bytes = slice::from_raw_parts(ptr1, mem::size_of::<{{ ty }}>()); - let ptr2_bytes = slice::from_raw_parts(ptr2, mem::size_of::<{{ ty }}>()); + let ptr2 = __{{ constant.test_ident }}().cast::(); + let ptr1_bytes = slice::from_raw_parts(ptr1, mem::size_of::<{{ constant.rust_type }}>()); + let ptr2_bytes = slice::from_raw_parts(ptr2, mem::size_of::<{{ constant.rust_type }}>()); for (i, (&b1, &b2)) in ptr1_bytes.iter().zip(ptr2_bytes.iter()).enumerate() { // HACK: This may read uninitialized data! We do this because // there isn't a good way to recursively iterate all fields. - check_same_hex(b1, b2, &format!("{{ ident }} value at byte {}", i)); + check_same_hex(b1, b2, &format!("{{ constant.rust_ident }} value at byte {}", i)); } } } - {%- endif +%} {%- endfor +%} } @@ -107,8 +105,8 @@ fn main() { // Run all tests by calling the functions that define them. fn run_all() { - {%- for constant in ffi_items.constants() +%} - const_{{ constant.ident() }}(); + {%- for test in ctx.test_idents +%} + {{ test }}(); {%- endfor +%} } diff --git a/ctest-next/tests/input/hierarchy.out.c b/ctest-next/tests/input/hierarchy.out.c index 0574cbc03c6f1..913f8b6dc718d 100644 --- a/ctest-next/tests/input/hierarchy.out.c +++ b/ctest-next/tests/input/hierarchy.out.c @@ -4,12 +4,13 @@ #include #include #include + #include -static bool __test_const_ON_val = ON; +static bool ctest_const_ON_val_static = ON; // Define a function that returns a pointer to the value of the constant to test. // This will later be called on the Rust side via FFI. -bool* __test_const_ON(void) { - return &__test_const_ON_val; +bool* __ctest_const_ON(void) { + return &ctest_const_ON_val_static; } diff --git a/ctest-next/tests/input/hierarchy.out.rs b/ctest-next/tests/input/hierarchy.out.rs index f301c77caf378..e002e66747bd0 100644 --- a/ctest-next/tests/input/hierarchy.out.rs +++ b/ctest-next/tests/input/hierarchy.out.rs @@ -43,14 +43,14 @@ mod generated_tests { // Test that the value of the constant is the same in both Rust and C. // This performs a byte by byte comparision of the constant value. - pub fn const_ON() { + pub fn ctest_const_ON() { extern "C" { - fn __test_const_ON() -> *const bool; + fn __ctest_const_ON() -> *const bool; } let val = ON; unsafe { let ptr1 = ptr::from_ref(&val).cast::(); - let ptr2 = __test_const_ON().cast::(); + let ptr2 = __ctest_const_ON().cast::(); let ptr1_bytes = slice::from_raw_parts(ptr1, mem::size_of::()); let ptr2_bytes = slice::from_raw_parts(ptr2, mem::size_of::()); for (i, (&b1, &b2)) in ptr1_bytes.iter().zip(ptr2_bytes.iter()).enumerate() { @@ -79,5 +79,5 @@ fn main() { // Run all tests by calling the functions that define them. fn run_all() { - const_ON(); + ctest_const_ON(); } diff --git a/ctest-next/tests/input/macro.out.c b/ctest-next/tests/input/macro.out.c index 736c06b8291bd..faaf70bea02ab 100644 --- a/ctest-next/tests/input/macro.out.c +++ b/ctest-next/tests/input/macro.out.c @@ -4,4 +4,5 @@ #include #include #include + #include diff --git a/ctest-next/tests/input/simple.out.with-renames.c b/ctest-next/tests/input/simple.out.with-renames.c index 8d6794dafe228..c5564e86ebf58 100644 --- a/ctest-next/tests/input/simple.out.with-renames.c +++ b/ctest-next/tests/input/simple.out.with-renames.c @@ -4,20 +4,21 @@ #include #include #include + #include -static char const* __test_const_A_val = A; +static char const* ctest_const_A_val_static = A; // Define a function that returns a pointer to the value of the constant to test. // This will later be called on the Rust side via FFI. -char const** __test_const_A(void) { - return &__test_const_A_val; +char const** __ctest_const_cstr_A(void) { + return &ctest_const_A_val_static; } -static char const* __test_const_B_val = C_B; +static char const* ctest_const_B_val_static = C_B; // Define a function that returns a pointer to the value of the constant to test. // This will later be called on the Rust side via FFI. -char const** __test_const_B(void) { - return &__test_const_B_val; +char const** __ctest_const_cstr_B(void) { + return &ctest_const_B_val_static; } diff --git a/ctest-next/tests/input/simple.out.with-renames.rs b/ctest-next/tests/input/simple.out.with-renames.rs index 06929a6077860..1c003fa5486c7 100644 --- a/ctest-next/tests/input/simple.out.with-renames.rs +++ b/ctest-next/tests/input/simple.out.with-renames.rs @@ -43,13 +43,13 @@ mod generated_tests { // Test that the string constant is the same in both Rust and C. // While fat pointers can't be translated, we instead use * const c_char. - pub fn const_A() { + pub fn ctest_const_cstr_A() { extern "C" { - fn __test_const_A() -> *const *const u8; + fn __ctest_const_cstr_A() -> *const *const u8; } let val = A; unsafe { - let ptr = *__test_const_A(); + let ptr = *__ctest_const_cstr_A(); let val = CStr::from_ptr(ptr.cast::()); let val = val.to_str().expect("const A not utf8"); let c = ::std::ffi::CStr::from_ptr(ptr as *const _); @@ -60,13 +60,13 @@ mod generated_tests { // Test that the string constant is the same in both Rust and C. // While fat pointers can't be translated, we instead use * const c_char. - pub fn const_B() { + pub fn ctest_const_cstr_B() { extern "C" { - fn __test_const_B() -> *const *const u8; + fn __ctest_const_cstr_B() -> *const *const u8; } let val = B; unsafe { - let ptr = *__test_const_B(); + let ptr = *__ctest_const_cstr_B(); let val = CStr::from_ptr(ptr.cast::()); let val = val.to_str().expect("const B not utf8"); let c = ::std::ffi::CStr::from_ptr(ptr as *const _); @@ -93,6 +93,6 @@ fn main() { // Run all tests by calling the functions that define them. fn run_all() { - const_A(); - const_B(); + ctest_const_cstr_A(); + ctest_const_cstr_B(); } diff --git a/ctest-next/tests/input/simple.out.with-skips.c b/ctest-next/tests/input/simple.out.with-skips.c index 94df4ec988166..fa5df3939336e 100644 --- a/ctest-next/tests/input/simple.out.with-skips.c +++ b/ctest-next/tests/input/simple.out.with-skips.c @@ -4,12 +4,13 @@ #include #include #include + #include -static char const* __test_const_A_val = A; +static char const* ctest_const_A_val_static = A; // Define a function that returns a pointer to the value of the constant to test. // This will later be called on the Rust side via FFI. -char const** __test_const_A(void) { - return &__test_const_A_val; +char const** __ctest_const_cstr_A(void) { + return &ctest_const_A_val_static; } diff --git a/ctest-next/tests/input/simple.out.with-skips.rs b/ctest-next/tests/input/simple.out.with-skips.rs index dac02a72aab6c..231f099d1e4d4 100644 --- a/ctest-next/tests/input/simple.out.with-skips.rs +++ b/ctest-next/tests/input/simple.out.with-skips.rs @@ -43,13 +43,13 @@ mod generated_tests { // Test that the string constant is the same in both Rust and C. // While fat pointers can't be translated, we instead use * const c_char. - pub fn const_A() { + pub fn ctest_const_cstr_A() { extern "C" { - fn __test_const_A() -> *const *const u8; + fn __ctest_const_cstr_A() -> *const *const u8; } let val = A; unsafe { - let ptr = *__test_const_A(); + let ptr = *__ctest_const_cstr_A(); let val = CStr::from_ptr(ptr.cast::()); let val = val.to_str().expect("const A not utf8"); let c = ::std::ffi::CStr::from_ptr(ptr as *const _); @@ -76,5 +76,5 @@ fn main() { // Run all tests by calling the functions that define them. fn run_all() { - const_A(); + ctest_const_cstr_A(); }