Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 56 additions & 8 deletions ctest/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use crate::ffi_items::FfiItems;
use crate::template::{CTestTemplate, RustTestTemplate};
use crate::translator::translate_primitive_type;
use crate::{
Const, Field, MapInput, Parameter, Result, Static, Struct, TranslationError, Type, Union,
VolatileItemKind, expand, get_build_target,
BoxStr, Const, Field, Language, MapInput, Parameter, Result, Static, Struct, TranslationError,
Type, Union, VolatileItemKind, expand, get_build_target,
};

/// A function that takes a mappable input and returns its mapping as `Some`, otherwise
Expand All @@ -35,14 +35,16 @@ type CEnum = Box<dyn Fn(&str) -> bool>;
#[derive(Default)]
#[expect(missing_debug_implementations)]
pub struct TestGenerator {
pub(crate) headers: Vec<String>,
pub(crate) headers: Vec<(BoxStr, Vec<BoxStr>)>,
pub(crate) target: Option<String>,
pub(crate) includes: Vec<PathBuf>,
out_dir: Option<PathBuf>,
pub(crate) flags: Vec<String>,
pub(crate) defines: Vec<(String, Option<String>)>,
pub(crate) global_defines: Vec<(String, Option<String>)>,
cfg: Vec<(String, Option<String>)>,
mapped_names: Vec<MappedName>,
/// The programming language to generate tests in.
pub(crate) language: Language,
pub(crate) skips: Vec<Skip>,
pub(crate) verbose_skip: bool,
pub(crate) volatile_items: Vec<VolatileItem>,
Expand Down Expand Up @@ -100,7 +102,53 @@ impl TestGenerator {
/// .header("bar.h");
/// ```
pub fn header(&mut self, header: &str) -> &mut Self {
self.headers.push(header.to_string());
self.headers.push((header.into(), vec![]));
self
}

/// Add a header to be included as part of the generated C file, as well as defines for it.
///
/// The generated C test will be compiled by a C compiler, and this can be
/// used to ensure that all the necessary header files are included to test
/// all FFI definitions. The defines are only set for the inclusion of that header file, and are
/// undefined immediately after.
///
/// # Examples
///
/// ```no_run
/// use ctest::TestGenerator;
///
/// let mut cfg = TestGenerator::new();
/// cfg.header_with_defines("foo.h", Vec::<String>::new())
/// .header_with_defines("bar.h", vec!["DEBUG", "DEPRECATED"]);
/// ```
pub fn header_with_defines(
&mut self,
header: &str,
defines: impl IntoIterator<Item = impl AsRef<str>>,
) -> &mut Self {
self.headers.push((
header.into(),
defines.into_iter().map(|d| d.as_ref().into()).collect(),
));
self
}

/// Sets the programming language, by default it is C.
///
/// This determines what compiler is chosen to compile the C/C++ tests, as well as adding
/// external linkage to the tests if set to C++, so that they can be used in Rust.
///
/// # Examples
///
/// ```no_run
/// use ctest::{TestGenerator, Language};
///
/// let mut cfg = TestGenerator::new();
/// cfg.language(Language::CXX);
/// ```
pub fn language(&mut self, language: Language) -> &mut Self {
self.language = language;
self
}

Expand Down Expand Up @@ -585,7 +633,7 @@ impl TestGenerator {

/// Set a `-D` flag for the C compiler being called.
///
/// This can be used to define various variables to configure how header
/// This can be used to define various global variables to configure how header
/// files are included or what APIs are exposed from header files.
///
/// # Examples
Expand All @@ -598,7 +646,7 @@ impl TestGenerator {
/// .define("_WIN32_WINNT", Some("0x8000"));
/// ```
pub fn define(&mut self, k: &str, v: Option<&str>) -> &mut Self {
self.defines
self.global_defines
.push((k.to_string(), v.map(std::string::ToString::to_string)));
self
}
Expand Down Expand Up @@ -999,7 +1047,7 @@ impl TestGenerator {
ensure_trailing_newline(&mut c_file);

// Generate the C/Cxx side of the tests.
let c_output_path = output_file_path.with_extension("c");
let c_output_path = output_file_path.with_extension(self.language.extension());
File::create(&c_output_path)
.map_err(GenerationError::OsError)?
.write_all(c_file.as_bytes())
Expand Down
21 changes: 21 additions & 0 deletions ctest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,27 @@ pub(crate) enum MapInput<'a> {
UnionFieldType(&'a Union, &'a Field),
}

/// The language used to generate the tests.
#[derive(Debug, Default, Clone)]
#[non_exhaustive]
pub enum Language {
/// The C Programming Language.
#[default]
C,
/// The C++ Programming Language.
CXX,
}

impl Language {
/// Return the file extension of the programming language.
pub(crate) fn extension(&self) -> &str {
match self {
Self::C => "c",
Self::CXX => "cpp",
}
}
}

/// Search for the target to build for, specified manually or through an environment variable.
///
/// This function will check the following places for the target name:
Expand Down
8 changes: 5 additions & 3 deletions ctest/src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};
use std::process::Command;

use crate::generator::GenerationError;
use crate::{EDITION, Result, TestGenerator, get_build_target};
use crate::{EDITION, Language, Result, TestGenerator, get_build_target};

/// Generate all tests for the given crate and output the Rust side to a file.
#[doc(hidden)]
Expand All @@ -24,7 +24,7 @@ pub fn generate_test(
.map_err(|_| GenerationError::EnvVarNotFound("HOST, HOST_PLATFORM".to_string()))?;

let mut cfg = cc::Build::new();
cfg.file(output_file_path.with_extension("c"));
cfg.file(output_file_path.with_extension(generator.language.extension()));
cfg.host(&host);

if target.contains("msvc") {
Expand Down Expand Up @@ -64,10 +64,12 @@ pub fn generate_test(
cfg.flag(flag);
}

for (k, v) in &generator.defines {
for (k, v) in &generator.global_defines {
cfg.define(k, v.as_ref().map(|s| &s[..]));
}

cfg.cpp(matches!(generator.language, Language::CXX));

let stem: &str = output_file_path.file_stem().unwrap().to_str().unwrap();
cfg.target(&target)
.out_dir(output_file_path.parent().unwrap())
Expand Down
2 changes: 1 addition & 1 deletion ctest/src/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ impl RustTestTemplate {
#[template(path = "test.c")]
pub(crate) struct CTestTemplate {
pub template: TestTemplate,
pub headers: Vec<String>,
pub headers: Vec<(BoxStr, Vec<BoxStr>)>,
}

impl CTestTemplate {
Expand Down
39 changes: 27 additions & 12 deletions ctest/templates/test.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,25 @@
#include <stdint.h>
#include <stdio.h>

{%- for header in self.headers +%}
{%- for (header, defines) in self.headers +%}
{%- for define in defines +%}

#define {{ define }}
{%- endfor +%}
#include <{{ header }}>
{%- for define in defines +%}
#undef {{ define }}
{%- endfor +%}
{%- endfor +%}

#if defined(__cplusplus)
#define CTEST_ALIGNOF(T) alignof(T)
#define CTEST_EXTERN extern "C"
#else
#define CTEST_ALIGNOF(T) _Alignof(T)
#define CTEST_EXTERN
#endif

typedef void (*ctest_void_func)(void);

{%- for const_cstr in ctx.const_cstr_tests +%}
Expand All @@ -21,7 +35,7 @@ static char *ctest_const_{{ const_cstr.id }}_val_static = {{ const_cstr.c_val }}

// 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 *ctest_const_cstr__{{ const_cstr.id }}(void) {
CTEST_EXTERN char *ctest_const_cstr__{{ const_cstr.id }}(void) {
return ctest_const_{{ const_cstr.id }}_val_static;
}
{%- endfor +%}
Expand All @@ -32,25 +46,25 @@ static {{ constant.c_ty }} ctest_const_{{ constant.id }}_val_static = {{ constan

// 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.
{{ constant.c_ty }} *ctest_const__{{ constant.id }}(void) {
CTEST_EXTERN {{ constant.c_ty }} *ctest_const__{{ constant.id }}(void) {
return &ctest_const_{{ constant.id }}_val_static;
}
{%- endfor +%}

{%- for item in ctx.size_align_tests +%}

// Return the size of a type.
uint64_t ctest_size_of__{{ item.id }}(void) { return sizeof({{ item.c_ty }}); }
CTEST_EXTERN uint64_t ctest_size_of__{{ item.id }}(void) { return sizeof({{ item.c_ty }}); }

// Return the alignment of a type.
uint64_t ctest_align_of__{{ item.id }}(void) { return _Alignof({{ item.c_ty }}); }
CTEST_EXTERN uint64_t ctest_align_of__{{ item.id }}(void) { return CTEST_ALIGNOF({{ item.c_ty }}); }
{%- endfor +%}

{%- for alias in ctx.signededness_tests +%}

// Return `1` if the type is signed, otherwise return `0`.
// Casting -1 to the aliased type if signed evaluates to `-1 < 0`, if unsigned to `MAX_VALUE < 0`
uint32_t ctest_signededness_of__{{ alias.id }}(void) {
CTEST_EXTERN uint32_t ctest_signededness_of__{{ alias.id }}(void) {
{{ alias.c_ty }} all_ones = ({{ alias.c_ty }}) -1;
return all_ones < 0;
}
Expand All @@ -59,12 +73,12 @@ uint32_t ctest_signededness_of__{{ alias.id }}(void) {
{%- for item in ctx.field_size_offset_tests +%}

// Return the offset of a struct/union field.
uint64_t ctest_offset_of__{{ item.id }}__{{ item.field.ident() }}(void) {
CTEST_EXTERN uint64_t ctest_offset_of__{{ item.id }}__{{ item.field.ident() }}(void) {
return offsetof({{ item.c_ty }}, {{ item.c_field }});
}

// Return the size of a struct/union field.
uint64_t ctest_size_of__{{ item.id }}__{{ item.field.ident() }}(void) {
CTEST_EXTERN uint64_t ctest_size_of__{{ item.id }}__{{ item.field.ident() }}(void) {
return sizeof((({{ item.c_ty }}){}).{{ item.c_field }});
}
{%- endfor +%}
Expand All @@ -75,7 +89,7 @@ uint64_t ctest_size_of__{{ item.id }}__{{ item.field.ident() }}(void) {
// This field can have a normal data type, or it could be a function pointer or an array, which
// have different syntax. A typedef is used for convenience, but the syntax must be precomputed.
typedef {{ item.volatile_keyword }}{{ item.field_return_type }};
ctest_field_ty__{{ item.id }}__{{ item.field.ident() }}
CTEST_EXTERN ctest_field_ty__{{ item.id }}__{{ item.field.ident() }}
ctest_field_ptr__{{ item.id }}__{{ item.field.ident() }}({{ item.c_ty }} *b) {
return &b->{{ item.c_field }};
}
Expand All @@ -92,7 +106,7 @@ ctest_field_ptr__{{ item.id }}__{{ item.field.ident() }}({{ item.c_ty }} *b) {
// Tests whether the struct/union/alias `x` when passed by value to C and back to Rust
// remains unchanged.
// It checks if the size is the same as well as if the padding bytes are all in the correct place.
{{ item.c_ty }} ctest_roundtrip__{{ item.id }}(
CTEST_EXTERN {{ item.c_ty }} ctest_roundtrip__{{ item.id }}(
{{ item.c_ty }} value,
const uint8_t is_padding_byte[sizeof({{ item.c_ty }})],
uint8_t value_bytes[sizeof({{ item.c_ty }})]
Expand Down Expand Up @@ -130,7 +144,8 @@ ctest_field_ptr__{{ item.id }}__{{ item.field.ident() }}({{ item.c_ty }} *b) {

{%- for item in ctx.foreign_fn_tests +%}

ctest_void_func ctest_foreign_fn__{{ item.id }}(void) {
// Return a function pointer.
CTEST_EXTERN ctest_void_func ctest_foreign_fn__{{ item.id }}(void) {
return (ctest_void_func){{ item.c_val }};
}
{%- endfor +%}
Expand All @@ -142,7 +157,7 @@ ctest_void_func ctest_foreign_fn__{{ item.id }}(void) {
{%- for static_ in ctx.foreign_static_tests +%}

// Return a pointer to the static variable content.
void *ctest_static__{{ static_.id }}(void) {
CTEST_EXTERN void *ctest_static__{{ static_.id }}(void) {
// FIXME(ctest): Not correct due to casting the function to a data pointer.
return (void *)&{{ static_.c_val }};
}
Expand Down
23 changes: 14 additions & 9 deletions ctest/tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ use pretty_assertions::assert_eq;
///
/// The files will be generated in a unique temporary directory that gets
/// deleted when it goes out of scope.
fn default_generator(opt_level: u8, header: &str) -> Result<(TestGenerator, tempfile::TempDir)> {
fn default_generator(
opt_level: u8,
header: Option<&str>,
) -> Result<(TestGenerator, tempfile::TempDir)> {
// FIXME(mbyx): Remove this in favor of not-unsafe alternatives.
unsafe { env::set_var("OPT_LEVEL", opt_level.to_string()) };
let temp_dir = tempfile::tempdir()?;
let mut generator = TestGenerator::new();
generator
.out_dir(&temp_dir)
.include("tests/input")
.header(header);
generator.out_dir(&temp_dir).include("tests/input");
if let Some(header) = header {
generator.header(header);
}

Ok((generator, temp_dir))
}
Expand Down Expand Up @@ -84,7 +87,7 @@ fn test_entrypoint_hierarchy() {
let crate_path = include_path.join("hierarchy/lib.rs");
let library_path = "hierarchy.out.a";

let (mut gen_, out_dir) = default_generator(1, "hierarchy.h").unwrap();
let (mut gen_, out_dir) = default_generator(1, Some("hierarchy.h")).unwrap();
check_entrypoint(&mut gen_, out_dir, crate_path, library_path, include_path);
}

Expand All @@ -95,7 +98,7 @@ fn test_skip_simple() {
let crate_path = include_path.join("simple.rs");
let library_path = "simple.out.with-skips.a";

let (mut gen_, out_dir) = default_generator(1, "simple.h").unwrap();
let (mut gen_, out_dir) = default_generator(1, Some("simple.h")).unwrap();
gen_.skip_const(|c| c.ident() == "B" || c.ident() == "A")
.skip_c_enum(|e| e == "Color")
.skip_alias(|a| a.ident() == "Byte")
Expand All @@ -114,7 +117,7 @@ fn test_map_simple() {
let crate_path = include_path.join("simple.rs");
let library_path = "simple.out.with-renames.a";

let (mut gen_, out_dir) = default_generator(1, "simple.h").unwrap();
let (mut gen_, out_dir) = default_generator(1, Some("simple.h")).unwrap();
gen_.rename_constant(|c| (c.ident() == "B").then(|| "C_B".to_string()))
.alias_is_c_enum(|e| e == "Color")
.skip_signededness(|ty| ty == "Color");
Expand All @@ -129,7 +132,9 @@ fn test_entrypoint_macro() {
let crate_path = include_path.join("macro.rs");
let library_path = "macro.out.a";

let (mut gen_, out_dir) = default_generator(1, "macro.h").unwrap();
let (mut gen_, out_dir) = default_generator(1, None).unwrap();
gen_.header_with_defines("macro.h", vec!["SUPPRESS_ERROR"]);

check_entrypoint(&mut gen_, out_dir, crate_path, library_path, include_path);
}

Expand Down
4 changes: 4 additions & 0 deletions ctest/tests/input/hierarchy.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#include <stdbool.h>
#include <stddef.h>

#ifdef SUPPRESS_ERROR
#error Expected SUPPRESS_ERROR to not be defined (testing per-file defines)
#endif

typedef unsigned int in6_addr;

#define ON true
Expand Down
Loading
Loading