-
Notifications
You must be signed in to change notification settings - Fork 1.1k
ctest: extract bindings from template into source #4551
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -2,78 +2,185 @@ 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<Self, TranslationError> { | ||||||||||||||||||
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<String>, | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
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<Self, TranslationError> { | ||||||||||||||||||
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<TestCstr>, | ||||||||||||||||||
pub(crate) const_tests: Vec<TestConst>, | ||||||||||||||||||
pub(crate) test_idents: Vec<BoxStr>, | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
impl TestTemplate { | ||||||||||||||||||
pub(crate) fn new( | ||||||||||||||||||
ffi_items: &FfiItems, | ||||||||||||||||||
generator: &TestGenerator, | ||||||||||||||||||
) -> Result<Self, TranslationError> { | ||||||||||||||||||
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, | ||||||||||||||||||
Comment on lines
+117
to
+120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Optional nit: if a struct isn't public, |
||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
/// 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<MapInput<'a>>) -> Result<String, GenerationError> { | ||||||||||||||||||
pub(crate) fn c_ident(&self, item: impl Into<MapInput<'a>>) -> String { | ||||||||||||||||||
self.generator.map(item) | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
/// Returns the equivalent C/Cpp type of the Rust item. | ||||||||||||||||||
pub(crate) fn c_type(&self, item: impl Into<MapInput<'a>>) -> Result<String, GenerationError> { | ||||||||||||||||||
let item: MapInput<'a> = item.into(); | ||||||||||||||||||
pub(crate) fn c_type(&self, item: impl Into<MapInput<'a>>) -> Result<String, TranslationError> { | ||||||||||||||||||
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!"), | ||||||||||||||||||
MapInput::UnionType(_) => panic!("MapInput::UnionType is not allowed!"), | ||||||||||||||||||
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) { | ||||||||||||||||||
MapInput::UnionType(&ty) | ||||||||||||||||||
} else { | ||||||||||||||||||
MapInput::Type(&ty) | ||||||||||||||||||
}; | ||||||||||||||||||
self.generator.map(item) | ||||||||||||||||||
|
||||||||||||||||||
Ok(self.generator.map(item)) | ||||||||||||||||||
} | ||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <stdbool.h> | ||
#include <stddef.h> | ||
#include <stdint.h> | ||
#include <stdio.h> | ||
|
||
{%- 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 +%} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leave this one as |
||
unsafe { | ||
let ptr = *__test_const_{{ ident }}(); | ||
let ptr = *__{{ const_cstr.test_ident }}(); | ||
let val = CStr::from_ptr(ptr.cast::<c_char>()); | ||
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 }}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same, |
||
} | ||
let val = {{ ident }}; | ||
let val = {{ constant.rust_ident }}; | ||
unsafe { | ||
let ptr1 = ptr::from_ref(&val).cast::<u8>(); | ||
let ptr2 = __test_const_{{ ident }}().cast::<u8>(); | ||
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::<u8>(); | ||
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 +%} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't add any test in the case where it is a pointer but not a
CStr
. Either make the top-levelif
also check for the pointer type so the nestedif
isn't needed:Or bump the MSRV by 1 again and use let chains to do the same thing but slightly cleaner.