| // 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); |
| } |
| } |
| } |