blob: ce7114ab9d12c0ac02990ccb522456c78d1d1299 [file] [edit]
// Copyright 2026 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
#![no_std]
#[cfg(feature = "std")]
extern crate std;
use core::cell::UnsafeCell;
use core::ffi::{c_char, CStr};
use core::fmt::{self, Arguments, Debug, Write};
use core::{cmp, mem, ptr, slice};
// Bazel includes the module as part of the source.
#[cfg(not(rust_bindgen_include))]
#[allow(
dead_code,
non_camel_case_types,
non_snake_case,
non_upper_case_globals,
clippy::decimal_literal_representation
)]
mod pw_unit_test_bindgen;
#[cfg(not(rust_bindgen_include))]
#[allow(
dead_code,
non_camel_case_types,
non_snake_case,
non_upper_case_globals,
clippy::decimal_literal_representation
)]
mod pw_unit_test_config_bindgen;
// GN requires that the bindgen source be included explicitly.
#[cfg(rust_bindgen_include)]
#[allow(
dead_code,
non_camel_case_types,
non_snake_case,
non_upper_case_globals,
clippy::decimal_literal_representation
)]
mod pw_unit_test_bindgen {
core::include!(env!("PW_UNIT_TEST_BINDGEN_RS_FILE"));
}
#[cfg(rust_bindgen_include)]
#[allow(
dead_code,
non_camel_case_types,
non_snake_case,
non_upper_case_globals,
clippy::decimal_literal_representation
)]
mod pw_unit_test_config_bindgen {
core::include!(env!("PW_UNIT_TEST_CONFIG_BINDGEN_RS_FILE"));
}
use pw_unit_test_bindgen::{pw_unit_test_FfiTestContext, pw_unit_test_TestDescriptor};
use pw_unit_test_config_bindgen::root::pw::unit_test::config::kExpectationBufferSizeBytes;
#[doc(hidden)]
pub mod __private {
pub use crate::pw_unit_test_bindgen::*;
pub use crate::pw_unit_test_config_bindgen::root::pw::unit_test::config::*;
pub use crate::{
assert_eq_failed, assert_failed, assert_ne_failed, TestDescriptor, _PW_UNIT_TEST_SENTINEL,
};
}
// SAFETY: The pointers point to static string literals or test functions.
unsafe impl Sync for pw_unit_test_TestDescriptor {}
unsafe impl Send for pw_unit_test_TestDescriptor {}
/// Re-exports the custom `#[test]` procedural macro.
pub use pw_unit_test_macro::test;
const DEFAULT_BUFFER_LEN: usize = kExpectationBufferSizeBytes;
/// Buffer for formatting runtime test strings without dynamic heap allocation.
pub struct StringBuffer<const N: usize = DEFAULT_BUFFER_LEN> {
buf: [u8; N],
len: usize,
}
impl<const N: usize> Default for StringBuffer<N> {
fn default() -> Self {
Self::new()
}
}
impl<const N: usize> StringBuffer<N> {
/// Creates a new empty `StringBuffer`.
#[must_use]
pub const fn new() -> Self {
Self {
buf: [0; N],
len: 0,
}
}
/// Resets the buffer to empty.
pub fn clear(&mut self) {
self.len = 0;
self.buf[0] = 0;
}
/// Returns a null-terminated C string pointer.
pub fn as_c_str(&mut self) -> *const c_char {
self.buf[self.len] = 0;
self.buf.as_ptr().cast::<c_char>()
}
}
impl<const N: usize> Write for StringBuffer<N> {
fn write_str(&mut self, s: &str) -> fmt::Result {
let bytes = s.as_bytes();
// Always reserve 1 byte for null terminator so appending '\0' never overwrites a UTF-8 byte.
let max_len = self.buf.len().saturating_sub(1);
if self.len >= max_len {
return Ok(());
}
let rem = max_len - self.len;
let mut to_copy = cmp::min(rem, bytes.len());
if to_copy < bytes.len() {
// Ensure we don't truncate in the middle of a multi-byte UTF-8 code point.
while !s.is_char_boundary(to_copy) {
to_copy -= 1;
}
}
self.buf[self.len..self.len + to_copy].copy_from_slice(&bytes[..to_copy]);
self.len += to_copy;
self.buf[self.len] = 0;
Ok(())
}
}
/// Type alias for the registered test descriptor generated by bindgen.
pub type TestDescriptor = pw_unit_test_TestDescriptor;
struct TestContextCell(UnsafeCell<*mut pw_unit_test_FfiTestContext>);
// SAFETY: Tests are executed strictly sequentially in a single-threaded test harness.
unsafe impl Sync for TestContextCell {}
static ACTIVE_TEST_CONTEXT: TestContextCell = TestContextCell(UnsafeCell::new(ptr::null_mut()));
/// Returns the currently executing test context.
#[inline]
#[must_use]
pub fn current_test_context() -> *mut pw_unit_test_FfiTestContext {
// SAFETY: Tests are only ever invoked sequentially on a single thread.
unsafe { *ACTIVE_TEST_CONTEXT.0.get() }
}
/// Sets the active test context pointer.
///
/// # Safety
///
/// * The caller must ensure that test execution is strictly single-threaded and sequential.
/// * `ctx` must either be null or point to a valid `pw_unit_test_FfiTestContext` that outlives
/// the duration of the test being executed.
#[inline]
pub(crate) unsafe fn set_active_test_context(ctx: *mut pw_unit_test_FfiTestContext) {
// SAFETY: Single-threaded sequential test execution is guaranteed by the test harness.
unsafe {
*ACTIVE_TEST_CONTEXT.0.get() = ctx;
}
}
/// Dispatches a test expectation failure to the C++ unit test framework using the active test context.
fn current_test_expect(
file: &'static str,
line: u32,
expression: &mut StringBuffer,
evaluated_expression: &mut StringBuffer,
) {
let handle = current_test_context();
if handle.is_null() {
return;
}
let mut file_buf: StringBuffer = StringBuffer::new();
let _ = file_buf.write_str(file);
unsafe {
pw_unit_test_bindgen::pw_unit_test_Expect(
handle,
expression.as_c_str(),
evaluated_expression.as_c_str(),
file_buf.as_c_str(),
line,
);
}
}
/// Formats and reports an `assert!` failure to the C++ test framework.
#[cold]
#[inline(never)]
#[doc(hidden)]
pub fn assert_failed(file: &'static str, line: u32, expr: &str, args: Option<Arguments>) {
let mut expr_buf: StringBuffer = StringBuffer::new();
let _ = core::write!(&mut expr_buf, "{expr} is true");
let mut evaluated_buf: StringBuffer = StringBuffer::new();
if let Some(args) = args {
let _ = core::write!(&mut evaluated_buf, "false\n{args}");
} else {
let _ = core::write!(&mut evaluated_buf, "false");
}
current_test_expect(file, line, &mut expr_buf, &mut evaluated_buf);
}
#[allow(clippy::too_many_arguments)]
fn format_binary_assert<T: Debug + ?Sized, U: Debug + ?Sized>(
file: &'static str,
line: u32,
op: &str,
lhs_expr: &str,
rhs_expr: &str,
lhs: &T,
rhs: &U,
args: Option<Arguments>,
) {
let mut expr_buf: StringBuffer = StringBuffer::new();
let _ = core::write!(&mut expr_buf, "{lhs_expr} {op} {rhs_expr}");
let mut evaluated_buf: StringBuffer = StringBuffer::new();
if let Some(args) = args {
let _ = core::write!(&mut evaluated_buf, "{lhs:?} {op} {rhs:?}\n{args}");
} else {
let _ = core::write!(&mut evaluated_buf, "{lhs:?} {op} {rhs:?}");
}
current_test_expect(file, line, &mut expr_buf, &mut evaluated_buf);
}
/// Formats and reports an `assert_eq!` failure to the C++ test framework.
#[cold]
#[inline(never)]
#[doc(hidden)]
pub fn assert_eq_failed<T: Debug + ?Sized, U: Debug + ?Sized>(
file: &'static str,
line: u32,
lhs_expr: &str,
rhs_expr: &str,
lhs: &T,
rhs: &U,
args: Option<Arguments>,
) {
format_binary_assert(file, line, "==", lhs_expr, rhs_expr, lhs, rhs, args);
}
/// Formats and reports an `assert_ne!` failure to the C++ test framework.
#[cold]
#[inline(never)]
#[doc(hidden)]
pub fn assert_ne_failed<T: Debug + ?Sized, U: Debug + ?Sized>(
file: &'static str,
line: u32,
lhs_expr: &str,
rhs_expr: &str,
lhs: &T,
rhs: &U,
args: Option<Arguments>,
) {
format_binary_assert(file, line, "!=", lhs_expr, rhs_expr, lhs, rhs, args);
}
/// Asserts that a condition is true.
#[macro_export]
macro_rules! assert {
($cond:expr $(,)?) => {
if !($cond) {
$crate::__private::assert_failed(file!(), line!(), stringify!($cond), None);
return;
}
};
($cond:expr, $($arg:tt)+) => {
if !($cond) {
$crate::__private::assert_failed(
file!(),
line!(),
stringify!($cond),
Some(::core::format_args!($($arg)+)),
);
return;
}
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __binary_assert {
($lhs:expr, ==, $rhs:expr, $msg:expr) => {
match (&$lhs, &$rhs) {
(lhs_val, rhs_val) => {
if !(*lhs_val == *rhs_val) {
$crate::__private::assert_eq_failed(
file!(),
line!(),
stringify!($lhs),
stringify!($rhs),
lhs_val,
rhs_val,
$msg,
);
return;
}
}
}
};
($lhs:expr, !=, $rhs:expr, $msg:expr) => {
match (&$lhs, &$rhs) {
(lhs_val, rhs_val) => {
if *lhs_val == *rhs_val {
$crate::__private::assert_ne_failed(
file!(),
line!(),
stringify!($lhs),
stringify!($rhs),
lhs_val,
rhs_val,
$msg,
);
return;
}
}
}
};
}
/// Asserts that two expressions are equal.
#[macro_export]
macro_rules! assert_eq {
($lhs:expr, $rhs:expr $(,)?) => {
$crate::__binary_assert!($lhs, ==, $rhs, None);
};
($lhs:expr, $rhs:expr, $($arg:tt)+) => {
$crate::__binary_assert!(
$lhs,
==,
$rhs,
Some(::core::format_args!($($arg)+))
);
};
}
/// Asserts that two expressions are not equal.
#[macro_export]
macro_rules! assert_ne {
($lhs:expr, $rhs:expr $(,)?) => {
$crate::__binary_assert!($lhs, !=, $rhs, None);
};
($lhs:expr, $rhs:expr, $($arg:tt)+) => {
$crate::__binary_assert!(
$lhs,
!=,
$rhs,
Some(::core::format_args!($($arg)+))
);
};
}
extern "Rust" {
static __start_pw_unit_test_desc: TestDescriptor;
static __stop_pw_unit_test_desc: TestDescriptor;
}
/// An empty sentinel descriptor to ensure the `pw_unit_test_desc`
/// section always exists in the binary and `__start`/`__stop` symbols are defined by the linker.
#[doc(hidden)]
#[cfg_attr(target_vendor = "apple", link_section = "__DATA,__pw_unit_test")]
#[cfg_attr(not(target_vendor = "apple"), link_section = "pw_unit_test_desc")]
#[used]
pub static _PW_UNIT_TEST_SENTINEL: TestDescriptor = TestDescriptor {
name: c"".as_ptr(),
suite: c"".as_ptr(),
test_fn: Some(sentinel_test_fn),
};
unsafe extern "C" fn sentinel_test_fn() {}
/// Returns an iterator over all linker-registered `TestDescriptor` entries,
/// automatically filtering out the empty sentinel entry.
fn get_registered_tests() -> impl Iterator<Item = &'static TestDescriptor> {
let slice = unsafe {
let start = ptr::addr_of!(__start_pw_unit_test_desc);
let stop = ptr::addr_of!(__stop_pw_unit_test_desc);
let count =
(stop as usize).saturating_sub(start as usize) / mem::size_of::<TestDescriptor>();
if count == 0 {
&[]
} else {
slice::from_raw_parts(start, count)
}
};
slice.iter().filter(|test| {
if test.name.is_null() || test.suite.is_null() {
return false;
}
let test_name = unsafe { CStr::from_ptr(test.name) }.to_bytes();
let test_suite = unsafe { CStr::from_ptr(test.suite) }.to_bytes();
!test_name.is_empty() && !test_suite.is_empty()
})
}
/// The main entry point for C++ to invoke Rust unit tests.
///
/// # Safety
///
/// Calling this function is safe provided that the following preconditions are met:
/// - Must be called from the C++ test runner thread within `Framework::RunAllTests()`,
/// after `RunAllTestsStart()` and before `RunAllTestsEnd()`.
/// - No test case is currently active or in-flight in the C++ test framework on any thread.
/// - Must not be called concurrently with other test executions or framework mutations.
/// - The linker distributed slice bounded by `__start_pw_unit_test_desc` and
/// `__stop_pw_unit_test_desc` must contain valid, initialized `TestDescriptor` entries
/// with non-null, valid C-string pointers for `suite` and `name`, and valid `test_fn` pointers.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pw_unit_test_RunRustTests() {
let mut ctx = pw_unit_test_FfiTestContext {
event_handler: ptr::null_mut(),
suite: ptr::null(),
name: ptr::null(),
file: ptr::null(),
};
for test in get_registered_tests() {
// SAFETY: `test.suite` is a valid null-terminated C string pointer to a
// static string literal created by the `#[pw_unit_test::test]` macro.
let should_run = unsafe { pw_unit_test_bindgen::pw_unit_test_ShouldRunSuite(test.suite) };
if !should_run {
continue;
}
// SAFETY: `ctx`, `test.suite`, and `test.name` are valid pointers.
unsafe {
pw_unit_test_bindgen::pw_unit_test_StartTest(
&mut ctx,
test.suite,
test.name,
ptr::null(),
);
}
set_active_test_context(&mut ctx);
if let Some(test_fn) = test.test_fn {
test_fn();
}
set_active_test_context(ptr::null_mut());
// SAFETY: `ctx` is a valid pointer to the active test context.
unsafe {
pw_unit_test_bindgen::pw_unit_test_EndTest(&mut ctx);
}
}
}