Skip to content

Commit 5ced981

Browse files
committed
ctest: add tests for size and alignment of structs, unions, and aliases, and signededness of aliases.
1 parent 891cb8c commit 5ced981

15 files changed

+511
-27
lines changed

ctest-next/src/ast/type_alias.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use crate::BoxStr;
55
pub struct Type {
66
pub(crate) public: bool,
77
pub(crate) ident: BoxStr,
8-
#[expect(unused)]
98
pub(crate) ty: syn::Type,
109
}
1110

ctest-next/src/ffi_items.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ impl FfiItems {
3737
}
3838

3939
/// Return a list of all type aliases found.
40-
#[cfg_attr(not(test), expect(unused))]
4140
pub(crate) fn aliases(&self) -> &Vec<Type> {
4241
&self.aliases
4342
}

ctest-next/src/generator.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ pub struct TestGenerator {
4444
array_arg: Option<ArrayArg>,
4545
skip_private: bool,
4646
skip_roundtrip: Option<SkipTest>,
47+
pub(crate) skip_signededness: Option<SkipTest>,
4748
}
4849

4950
#[derive(Debug, Error)]
@@ -832,6 +833,28 @@ impl TestGenerator {
832833
self
833834
}
834835

836+
/// Configures whether a type's signededness is tested or not.
837+
///
838+
/// The closure is given the name of a Rust type, and returns whether the
839+
/// type should be tested as having the right sign (positive or negative).
840+
///
841+
/// By default all signededness checks are performed.
842+
///
843+
/// # Examples
844+
///
845+
/// ```no_run
846+
/// use ctest_next::TestGenerator;
847+
///
848+
/// let mut cfg = TestGenerator::new();
849+
/// cfg.skip_signededness(|s| {
850+
/// s.starts_with("foo_")
851+
/// });
852+
/// ```
853+
pub fn skip_signededness(&mut self, f: impl Fn(&str) -> bool + 'static) -> &mut Self {
854+
self.skip_signededness = Some(Box::new(f));
855+
self
856+
}
857+
835858
/// Generate the Rust and C testing files.
836859
///
837860
/// Returns the path to the generated file.

ctest-next/src/template.rs

Lines changed: 116 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,15 @@ impl CTestTemplate {
4646
/// Stores all information necessary for generation of tests for all items.
4747
#[derive(Clone, Debug, Default)]
4848
pub(crate) struct TestTemplate {
49+
pub signededness_tests: Vec<TestSignededness>,
50+
pub size_align_tests: Vec<TestSizeAlign>,
4951
pub const_cstr_tests: Vec<TestCStr>,
5052
pub const_tests: Vec<TestConst>,
5153
pub test_idents: Vec<BoxStr>,
5254
}
5355

5456
impl TestTemplate {
57+
/// Populate all tests for all items depending on the configuration provided.
5558
pub(crate) fn new(
5659
ffi_items: &FfiItems,
5760
generator: &TestGenerator,
@@ -62,15 +65,20 @@ impl TestTemplate {
6265
translator: Translator::new(),
6366
};
6467

65-
/* Figure out which tests are to be generated. */
66-
// FIXME(ctest): Populate more test information, maybe extract into separate methods.
67-
// The workflow would be to create a struct that stores information for the new test,
68-
// and populating that struct here, so that the also things that have to be added to
69-
// the test templates are the new tests parameterized by that struct.
68+
let mut template = Self::default();
69+
template.populate_const_and_cstr_tests(&helper)?;
70+
template.populate_size_align_tests(&helper)?;
71+
template.populate_signededness_tests(&helper)?;
7072

71-
let mut const_tests = vec![];
72-
let mut const_cstr_tests = vec![];
73-
for constant in ffi_items.constants() {
73+
Ok(template)
74+
}
75+
76+
/// Populates tests for constants and C-str constants, keeping track of the names of each test.
77+
fn populate_const_and_cstr_tests(
78+
&mut self,
79+
helper: &TranslateHelper,
80+
) -> Result<(), TranslationError> {
81+
for constant in helper.ffi_items.constants() {
7482
if let syn::Type::Ptr(ptr) = &constant.ty
7583
&& let syn::Type::Path(path) = &*ptr.elem
7684
&& path.path.segments.last().unwrap().ident == "c_char"
@@ -82,29 +90,95 @@ impl TestTemplate {
8290
rust_val: constant.ident().into(),
8391
c_val: helper.c_ident(constant).into(),
8492
};
85-
const_cstr_tests.push(item)
93+
self.const_cstr_tests.push(item.clone());
94+
self.test_idents.push(item.test_name);
8695
} else {
8796
let item = TestConst {
8897
id: constant.ident().into(),
8998
test_name: const_test_ident(constant.ident()),
90-
rust_val: constant.ident.clone(),
99+
rust_val: constant.ident().into(),
91100
rust_ty: constant.ty.to_token_stream().to_string().into_boxed_str(),
92101
c_val: helper.c_ident(constant).into(),
93102
c_ty: helper.c_type(constant)?.into(),
94103
};
95-
const_tests.push(item)
104+
self.const_tests.push(item.clone());
105+
self.test_idents.push(item.test_name);
96106
}
97107
}
98108

99-
let mut test_idents = vec![];
100-
test_idents.extend(const_cstr_tests.iter().map(|test| test.test_name.clone()));
101-
test_idents.extend(const_tests.iter().map(|test| test.test_name.clone()));
109+
Ok(())
110+
}
102111

103-
Ok(Self {
104-
const_cstr_tests,
105-
const_tests,
106-
test_idents,
107-
})
112+
/// Populates size and alignment tests for aliases, structs, and unions.
113+
///
114+
/// It also keeps track of the names of each test.
115+
fn populate_size_align_tests(
116+
&mut self,
117+
helper: &TranslateHelper,
118+
) -> Result<(), TranslationError> {
119+
for alias in helper.ffi_items.aliases() {
120+
let item = TestSizeAlign {
121+
test_name: size_align_test_ident(alias.ident()),
122+
id: alias.ident().into(),
123+
rust_ty: alias.ident().into(),
124+
c_ty: helper.c_type(alias)?.into(),
125+
};
126+
self.size_align_tests.push(item.clone());
127+
self.test_idents.push(item.test_name);
128+
}
129+
for struct_ in helper.ffi_items.structs() {
130+
let item = TestSizeAlign {
131+
test_name: size_align_test_ident(struct_.ident()),
132+
id: struct_.ident().into(),
133+
rust_ty: struct_.ident().into(),
134+
c_ty: helper.c_type(struct_)?.into(),
135+
};
136+
self.size_align_tests.push(item.clone());
137+
self.test_idents.push(item.test_name);
138+
}
139+
for union_ in helper.ffi_items.unions() {
140+
let item = TestSizeAlign {
141+
test_name: size_align_test_ident(union_.ident()),
142+
id: union_.ident().into(),
143+
rust_ty: union_.ident().into(),
144+
c_ty: helper.c_type(union_)?.into(),
145+
};
146+
self.size_align_tests.push(item.clone());
147+
self.test_idents.push(item.test_name);
148+
}
149+
150+
Ok(())
151+
}
152+
153+
/// Populates signededness tests for aliases.
154+
///
155+
/// It also keeps track of the names of each test.
156+
fn populate_signededness_tests(
157+
&mut self,
158+
helper: &TranslateHelper,
159+
) -> Result<(), TranslationError> {
160+
for alias in helper.ffi_items.aliases() {
161+
let should_skip_signededness_test = helper
162+
.generator
163+
.skip_signededness
164+
.as_ref()
165+
.is_some_and(|skip| skip(alias.ident()));
166+
167+
if !helper.translator.is_signed(helper.ffi_items, &alias.ty)
168+
|| should_skip_signededness_test
169+
{
170+
continue;
171+
}
172+
let item = TestSignededness {
173+
test_name: signededness_test_ident(alias.ident()),
174+
id: alias.ident().into(),
175+
c_ty: helper.c_type(alias)?.into(),
176+
};
177+
self.signededness_tests.push(item.clone());
178+
self.test_idents.push(item.test_name);
179+
}
180+
181+
Ok(())
108182
}
109183
}
110184

@@ -119,6 +193,21 @@ impl TestTemplate {
119193
* - `c_ty`: The C type of the constant, qualified with `struct` or `union` if needed.
120194
*/
121195

196+
#[derive(Clone, Debug)]
197+
pub(crate) struct TestSignededness {
198+
pub test_name: BoxStr,
199+
pub id: BoxStr,
200+
pub c_ty: BoxStr,
201+
}
202+
203+
#[derive(Clone, Debug)]
204+
pub(crate) struct TestSizeAlign {
205+
pub test_name: BoxStr,
206+
pub id: BoxStr,
207+
pub rust_ty: BoxStr,
208+
pub c_ty: BoxStr,
209+
}
210+
122211
/// Information required to test a constant CStr.
123212
#[derive(Clone, Debug)]
124213
pub(crate) struct TestCStr {
@@ -139,16 +228,18 @@ pub(crate) struct TestConst {
139228
pub c_ty: BoxStr,
140229
}
141230

142-
/// The Rust name of the cstr test.
143-
///
144-
/// The C name of this same test is the same with `__` prepended.
231+
fn signededness_test_ident(ident: &str) -> BoxStr {
232+
format!("ctest_signededness_{ident}").into()
233+
}
234+
235+
fn size_align_test_ident(ident: &str) -> BoxStr {
236+
format!("ctest_size_align_{ident}").into()
237+
}
238+
145239
fn cstr_test_ident(ident: &str) -> BoxStr {
146240
format!("ctest_const_cstr_{ident}").into()
147241
}
148242

149-
/// The Rust name of the const test.
150-
///
151-
/// The C name of this test is the same with `__` prepended.
152243
fn const_test_ident(ident: &str) -> BoxStr {
153244
format!("ctest_const_{ident}").into()
154245
}

ctest-next/src/translator.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use syn::spanned::Spanned;
1111
use thiserror::Error;
1212

1313
use crate::BoxStr;
14+
use crate::ffi_items::FfiItems;
1415

1516
/// An error that occurs during translation, detailing cause and location.
1617
#[derive(Debug, Error)]
@@ -314,6 +315,31 @@ impl Translator {
314315
}
315316
}
316317
}
318+
319+
/// Determine whether a C type is a signed type.
320+
///
321+
/// For primitive types it checks against a known list of signed types, but for aliases
322+
/// which are the only thing other than primitives that can be signed, it recursively checks
323+
/// the underlying type of the alias.
324+
pub(crate) fn is_signed(&self, ffi_items: &FfiItems, ty: &syn::Type) -> bool {
325+
match ty {
326+
syn::Type::Path(path) => {
327+
let ident = path.path.segments.last().unwrap().ident.clone();
328+
if let Some(aliased) = ffi_items.aliases().iter().find(|a| ident == a.ident()) {
329+
return self.is_signed(ffi_items, &aliased.ty);
330+
}
331+
match self.translate_primitive_type(&ident).as_str() {
332+
"char" | "short" | "long" | "long long" | "size_t" | "ssize_t" => true,
333+
s => {
334+
s.starts_with("int")
335+
|| s.starts_with("uint") | s.starts_with("signed ")
336+
|| s.starts_with("unsigned ")
337+
}
338+
}
339+
}
340+
_ => false,
341+
}
342+
}
317343
}
318344

319345
/// Translate a simple Rust expression to C.

ctest-next/templates/test.c

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,22 @@ static {{ constant.c_ty }} ctest_const_{{ constant.id }}_val_static = {{ constan
3434
return &ctest_const_{{ constant.id }}_val_static;
3535
}
3636
{%- endfor +%}
37+
38+
{%- for item in ctx.size_align_tests +%}
39+
40+
// Return the size of a type.
41+
uint64_t ctest_size_of__{{ item.id }}(void) { return sizeof({{ item.c_ty }}); }
42+
43+
// Return the alignment of a type.
44+
uint64_t ctest_align_of__{{ item.id }}(void) { return _Alignof({{ item.c_ty }}); }
45+
{%- endfor +%}
46+
47+
{%- for alias in ctx.signededness_tests +%}
48+
49+
// Return `1` if the type is signed, otherwise return `0`.
50+
// Casting -1 to the aliased type if signed evaluates to `-1 < 0`, if unsigned to `MAX_VALUE < 0`
51+
uint32_t ctest_signededness_of__{{ alias.id }}(void) {
52+
{{ alias.c_ty }} all_ones = -1;
53+
return all_ones < 0;
54+
}
55+
{%- endfor +%}

ctest-next/templates/test.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,45 @@ mod generated_tests {
9898
}
9999
}
100100
{%- endfor +%}
101+
102+
{%- for item in ctx.size_align_tests +%}
103+
104+
/// Compare the size and alignment of the type in Rust and C, making sure they are the same.
105+
pub fn {{ item.test_name }}() {
106+
extern "C" {
107+
fn ctest_size_of__{{ item.id }}() -> u64;
108+
fn ctest_align_of__{{ item.id }}() -> u64;
109+
}
110+
111+
let rust_size = size_of::<{{ item.rust_ty }}>() as u64;
112+
let c_size = unsafe { ctest_size_of__{{ item.id }}() };
113+
114+
let rust_align = align_of::<{{ item.rust_ty }}>() as u64;
115+
let c_align = unsafe { ctest_align_of__{{ item.id }}() };
116+
117+
check_same(rust_size, c_size, "{{ item.id }} size");
118+
check_same(rust_align, c_align, "{{ item.id }} align");
119+
}
120+
{%- endfor +%}
121+
122+
{%- for alias in ctx.signededness_tests +%}
123+
124+
/// Make sure that the signededness of a type alias in Rust and C is the same.
125+
///
126+
/// This is done by casting 0 to that type and flipping all of its bits. For unsigned types,
127+
/// this would result in a value larger than zero. For signed types, this results in a value
128+
/// smaller than 0.
129+
pub fn {{ alias.test_name }}() {
130+
extern "C" {
131+
fn ctest_signededness_of__{{ alias.id }}() -> u32;
132+
}
133+
let all_ones = !(0 as {{ alias.id }});
134+
let all_zeros = 0 as {{ alias.id }};
135+
let c_is_signed = unsafe { ctest_signededness_of__{{ alias.id }}() };
136+
137+
check_same((all_ones < all_zeros) as u32, c_is_signed, "{{ alias.id }} signed");
138+
}
139+
{%- endfor +%}
101140
}
102141

103142
use generated_tests::*;

ctest-next/tests/input/hierarchy.out.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,16 @@ static bool ctest_const_ON_val_static = ON;
1414
bool *ctest_const__ON(void) {
1515
return &ctest_const_ON_val_static;
1616
}
17+
18+
// Return the size of a type.
19+
uint64_t ctest_size_of__in6_addr(void) { return sizeof(in6_addr); }
20+
21+
// Return the alignment of a type.
22+
uint64_t ctest_align_of__in6_addr(void) { return _Alignof(in6_addr); }
23+
24+
// Return `1` if the type is signed, otherwise return `0`.
25+
// Casting -1 to the aliased type if signed evaluates to `-1 < 0`, if unsigned to `MAX_VALUE < 0`
26+
uint32_t ctest_signededness_of__in6_addr(void) {
27+
in6_addr all_ones = -1;
28+
return all_ones < 0;
29+
}

0 commit comments

Comments
 (0)