Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
ctest: Replace check_same_hex with check_same_bytes
This allows us to print the entire slice on failure rather than only a
single mismatched byte. Also use this as an opportunity to quote types
in failure messages.

A possible future improvement here would be to check if the type is an
integer in our template and call `check_same` instead if so, which would
let us print the decimal value as well.
  • Loading branch information
tgross35 committed Oct 28, 2025
commit 8a224ab4dffdf3bfddf97087445a2b28238d462d
40 changes: 20 additions & 20 deletions ctest-test/tests/all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,26 +41,26 @@ fn t2() {
let (output, status) = output(&mut cmd("t2"));
assert!(!status.success(), "output: {output}");
let errors = [
"bad T2Foo signed",
"bad T2TypedefFoo signed",
"bad T2TypedefInt signed",
"bad T2Bar size",
"bad T2Bar align",
"bad T2Bar signed",
"bad T2Baz size",
"bad field offset a of T2Baz",
"bad field type a of T2Baz",
"bad field offset b of T2Baz",
"bad field type b of T2Baz",
"bad T2a function pointer",
"bad T2C value at byte 0",
"bad const T2S string",
"bad T2Union size",
"bad field type b of T2Union",
"bad field offset b of T2Union",
"bad enum_wrong_signedness signed",
"bad enum_repr_too_small size",
"bad enum_repr_too_small align",
"bad `T2Foo` signed",
"bad `T2TypedefFoo` signed",
"bad `T2TypedefInt` signed",
"bad `T2Bar` size",
"bad `T2Bar` align",
"bad `T2Bar` signed",
"bad `T2Baz` size",
"bad field offset `a` of `T2Baz`",
"bad field pointer access `a` of `T2Baz`",
"bad field offset `b` of `T2Baz`",
"bad field pointer access `b` of `T2Baz`",
"bad `T2a` function pointer",
"bad `T2C` value at byte 0",
"bad const `T2S` string",
"bad `T2Union` size",
"bad field offset `b` of `T2Union`",
"bad field pointer access `b` of `T2Union`",
"bad `enum_wrong_signedness` signed",
"bad `enum_repr_too_small` size",
"bad `enum_repr_too_small` align",
];
let mut errors = errors.iter().cloned().collect::<HashSet<_>>();

Expand Down
70 changes: 44 additions & 26 deletions ctest/templates/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ mod generated_tests {
#![deny(improper_ctypes_definitions)]
#[allow(unused_imports)]
use std::ffi::{CStr, c_int, c_char, c_uint};
use std::fmt::{Debug, LowerHex};
use std::fmt::{Debug, Write};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
#[allow(unused_imports)]
use std::{mem, ptr, slice};
Expand All @@ -35,17 +35,37 @@ mod generated_tests {
}
}

/// Check that the value returned from the Rust and C side in a certain test is equivalent.
///
/// Internally it will remember which checks failed and how many tests have been run. This
/// method is the same as `check_same` but prints errors in bytes in hex.
fn check_same_hex<T: PartialEq + LowerHex + Debug>(rust: T, c: T, attr: &str) {
if rust != c {
eprintln!("bad {attr}: rust: {rust:?} ({rust:#x}) != c {c:?} ({c:#x})");
FAILED.store(true, Ordering::Relaxed);
} else {
fn check_same_bytes(rust: &[u8], c: &[u8], attr: &str) {
if rust == c {
NTESTS.fetch_add(1, Ordering::Relaxed);
return;
}

FAILED.store(true, Ordering::Relaxed);
// Buffer to a string so we don't write individual bytes to stdio
let mut s = String::new();
if rust.len() == c.len() {
for (i, (&rb, &cb)) in rust.iter().zip(c.iter()).enumerate() {
if rb != cb {
writeln!(
s, "bad {attr} at byte {i}: rust: {rb:?} ({rb:#x}) != c {cb:?} ({cb:#x})"
).unwrap();
break;
}
}
} else {
writeln!(s, "bad {attr}: rust len {} != c len {}", rust.len(), c.len()).unwrap();
}

write!(s, " rust bytes:").unwrap();
for b in rust {
write!(s, " {b:02x}").unwrap();
}
write!(s, "\n c bytes: ").unwrap();
for b in c {
write!(s, " {b:02x}").unwrap();
}
eprintln!("{s}");
}

{%- for const_cstr in ctx.const_cstr_tests +%}
Expand All @@ -60,7 +80,7 @@ mod generated_tests {
// SAFETY: we assume that `c_char` pointer consts are for C strings.
let r_val = unsafe {
let r_ptr: *const c_char = {{ const_cstr.rust_val }};
assert!(!r_ptr.is_null(), "const {{ const_cstr.rust_val }} is null");
assert!(!r_ptr.is_null(), "const `{{ const_cstr.rust_val }}` is null");
CStr::from_ptr(r_ptr)
};

Expand All @@ -70,7 +90,7 @@ mod generated_tests {
CStr::from_ptr(c_ptr)
};

check_same(r_val, c_val, "const {{ const_cstr.rust_val }} string");
check_same(r_val, c_val, "const `{{ const_cstr.rust_val }}` string");
}
{%- endfor +%}

Expand All @@ -97,9 +117,7 @@ mod generated_tests {
slice::from_raw_parts(c_ptr.cast::<u8>(), size_of::<T>())
};

for (i, (&b1, &b2)) in r_bytes.iter().zip(c_bytes.iter()).enumerate() {
check_same_hex(b1, b2, &format!("{{ constant.rust_val }} value at byte {}", i));
}
check_same_bytes(r_bytes, c_bytes, "`{{ constant.rust_val }}` value");
}
{%- endfor +%}

Expand All @@ -118,8 +136,8 @@ mod generated_tests {
let rust_align = align_of::<{{ item.rust_ty }}>() as u64;
let c_align = unsafe { ctest_align_of__{{ item.id }}() };

check_same(rust_size, c_size, "{{ item.id }} size");
check_same(rust_align, c_align, "{{ item.id }} align");
check_same(rust_size, c_size, "`{{ item.id }}` size");
check_same(rust_align, c_align, "`{{ item.id }}` align");
}
{%- endfor +%}

Expand All @@ -138,7 +156,7 @@ mod generated_tests {
let all_zeros = 0 as {{ alias.id }};
let c_is_signed = unsafe { ctest_signededness_of__{{ alias.id }}() };

check_same((all_ones < all_zeros) as u32, c_is_signed, "{{ alias.id }} signed");
check_same((all_ones < all_zeros) as u32, c_is_signed, "`{{ alias.id }}` signed");
}
{%- endfor +%}

Expand All @@ -163,11 +181,11 @@ mod generated_tests {
// SAFETY: FFI call with no preconditions
let ctest_field_offset = unsafe { ctest_offset_of__{{ item.id }}__{{ item.field.ident() }}() };
check_same(offset_of!({{ item.id }}, {{ item.field.ident() }}) as u64, ctest_field_offset,
"field offset {{ item.field.ident() }} of {{ item.id }}");
"field offset `{{ item.field.ident() }}` of `{{ item.id }}`");
// SAFETY: FFI call with no preconditions
let ctest_field_size = unsafe { ctest_size_of__{{ item.id }}__{{ item.field.ident() }}() };
check_same(size_of_val(&val) as u64, ctest_field_size,
"field size {{ item.field.ident() }} of {{ item.id }}");
"field size `{{ item.field.ident() }}` of `{{ item.id }}`");
}
{%- endfor +%}

Expand All @@ -188,7 +206,7 @@ mod generated_tests {
// SAFETY: FFI call with no preconditions
let ctest_field_ptr = unsafe { ctest_field_ptr__{{ item.id }}__{{ item.field.ident() }}(ty_ptr) };
check_same(field_ptr.cast(), ctest_field_ptr,
"field type {{ item.field.ident() }} of {{ item.id }}");
"field pointer access `{{ item.field.ident() }}` of `{{ item.id }}`");
}

{%- endfor +%}
Expand Down Expand Up @@ -279,7 +297,7 @@ mod generated_tests {
if SIZE != c_size {
FAILED.store(true, Ordering::Relaxed);
eprintln!(
"size of {{ item.c_ty }} is {c_size} in C and {SIZE} in Rust\n",
"size of `{{ item.c_ty }}` is {c_size} in C and {SIZE} in Rust\n",
);
return;
}
Expand All @@ -295,7 +313,7 @@ mod generated_tests {
let rust = unsafe { *input_ptr.add(i) };
let c = c_value_bytes[i];
if rust != c {
eprintln!("rust[{}] = {} != {} (C): Rust \"{{ item.id }}\" -> C", i, rust, c);
eprintln!("rust[{}] = {} != {} (C): Rust `{{ item.id }}` -> C", i, rust, c);
FAILED.store(true, Ordering::Relaxed);
}
}
Expand All @@ -307,7 +325,7 @@ mod generated_tests {
let c = unsafe { (&raw const r).cast::<u8>().add(i).read_volatile() as usize };
if rust != c {
eprintln!(
"rust [{i}] = {rust} != {c} (C): C \"{{ item.id }}\" -> Rust",
"rust [{i}] = {rust} != {c} (C): C `{{ item.id }}` -> Rust",
);
FAILED.store(true, Ordering::Relaxed);
}
Expand All @@ -324,7 +342,7 @@ mod generated_tests {
}
let actual = unsafe { ctest_foreign_fn__{{ item.id }}() } as u64;
let expected = {{ item.id }} as u64;
check_same(actual, expected, "{{ item.id }} function pointer");
check_same(actual, expected, "`{{ item.id }}` function pointer");
}
{%- endfor +%}

Expand All @@ -339,7 +357,7 @@ mod generated_tests {
let expected = unsafe {
ctest_static__{{ static_.id }}().addr()
};
check_same(actual, expected, "{{ static_.id }} static");
check_same(actual, expected, "`{{ static_.id }}` static");
}
{%- endfor +%}
}
Expand Down
60 changes: 39 additions & 21 deletions ctest/tests/input/hierarchy.out.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ mod generated_tests {
#![deny(improper_ctypes_definitions)]
#[allow(unused_imports)]
use std::ffi::{CStr, c_int, c_char, c_uint};
use std::fmt::{Debug, LowerHex};
use std::fmt::{Debug, Write};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
#[allow(unused_imports)]
use std::{mem, ptr, slice};
Expand All @@ -32,17 +32,37 @@ mod generated_tests {
}
}

/// Check that the value returned from the Rust and C side in a certain test is equivalent.
///
/// Internally it will remember which checks failed and how many tests have been run. This
/// method is the same as `check_same` but prints errors in bytes in hex.
fn check_same_hex<T: PartialEq + LowerHex + Debug>(rust: T, c: T, attr: &str) {
if rust != c {
eprintln!("bad {attr}: rust: {rust:?} ({rust:#x}) != c {c:?} ({c:#x})");
FAILED.store(true, Ordering::Relaxed);
} else {
fn check_same_bytes(rust: &[u8], c: &[u8], attr: &str) {
if rust == c {
NTESTS.fetch_add(1, Ordering::Relaxed);
return;
}

FAILED.store(true, Ordering::Relaxed);
// Buffer to a string so we don't write individual bytes to stdio
let mut s = String::new();
if rust.len() == c.len() {
for (i, (&rb, &cb)) in rust.iter().zip(c.iter()).enumerate() {
if rb != cb {
writeln!(
s, "bad {attr} at byte {i}: rust: {rb:?} ({rb:#x}) != c {cb:?} ({cb:#x})"
).unwrap();
break;
}
}
} else {
writeln!(s, "bad {attr}: rust len {} != c len {}", rust.len(), c.len()).unwrap();
}

write!(s, " rust bytes:").unwrap();
for b in rust {
write!(s, " {b:02x}").unwrap();
}
write!(s, "\n c bytes: ").unwrap();
for b in c {
write!(s, " {b:02x}").unwrap();
}
eprintln!("{s}");
}

// Test that the value of the constant is the same in both Rust and C.
Expand All @@ -66,9 +86,7 @@ mod generated_tests {
slice::from_raw_parts(c_ptr.cast::<u8>(), size_of::<T>())
};

for (i, (&b1, &b2)) in r_bytes.iter().zip(c_bytes.iter()).enumerate() {
check_same_hex(b1, b2, &format!("ON value at byte {}", i));
}
check_same_bytes(r_bytes, c_bytes, "`ON` value");
}

/// Compare the size and alignment of the type in Rust and C, making sure they are the same.
Expand All @@ -84,8 +102,8 @@ mod generated_tests {
let rust_align = align_of::<in6_addr>() as u64;
let c_align = unsafe { ctest_align_of__in6_addr() };

check_same(rust_size, c_size, "in6_addr size");
check_same(rust_align, c_align, "in6_addr align");
check_same(rust_size, c_size, "`in6_addr` size");
check_same(rust_align, c_align, "`in6_addr` align");
}

/// Make sure that the signededness of a type alias in Rust and C is the same.
Expand All @@ -101,7 +119,7 @@ mod generated_tests {
let all_zeros = 0 as in6_addr;
let c_is_signed = unsafe { ctest_signededness_of__in6_addr() };

check_same((all_ones < all_zeros) as u32, c_is_signed, "in6_addr signed");
check_same((all_ones < all_zeros) as u32, c_is_signed, "`in6_addr` signed");
}

/// Generates a padding map for a specific type.
Expand Down Expand Up @@ -179,7 +197,7 @@ mod generated_tests {
if SIZE != c_size {
FAILED.store(true, Ordering::Relaxed);
eprintln!(
"size of in6_addr is {c_size} in C and {SIZE} in Rust\n",
"size of `in6_addr` is {c_size} in C and {SIZE} in Rust\n",
);
return;
}
Expand All @@ -195,7 +213,7 @@ mod generated_tests {
let rust = unsafe { *input_ptr.add(i) };
let c = c_value_bytes[i];
if rust != c {
eprintln!("rust[{}] = {} != {} (C): Rust \"in6_addr\" -> C", i, rust, c);
eprintln!("rust[{}] = {} != {} (C): Rust `in6_addr` -> C", i, rust, c);
FAILED.store(true, Ordering::Relaxed);
}
}
Expand All @@ -207,7 +225,7 @@ mod generated_tests {
let c = unsafe { (&raw const r).cast::<u8>().add(i).read_volatile() as usize };
if rust != c {
eprintln!(
"rust [{i}] = {rust} != {c} (C): C \"in6_addr\" -> Rust",
"rust [{i}] = {rust} != {c} (C): C `in6_addr` -> Rust",
);
FAILED.store(true, Ordering::Relaxed);
}
Expand All @@ -221,7 +239,7 @@ mod generated_tests {
}
let actual = unsafe { ctest_foreign_fn__malloc() } as u64;
let expected = malloc as u64;
check_same(actual, expected, "malloc function pointer");
check_same(actual, expected, "`malloc` function pointer");
}

// Tests if the pointer to the static variable matches in both Rust and C.
Expand All @@ -233,7 +251,7 @@ mod generated_tests {
let expected = unsafe {
ctest_static__in6addr_any().addr()
};
check_same(actual, expected, "in6addr_any static");
check_same(actual, expected, "`in6addr_any` static");
}
}

Expand Down
Loading
Loading