feat: improve last_error and cffi_wrapper
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
//! Integration tests for the `cffi_wrapper!` macro and the parameter-direction macros.
|
||||
//!
|
||||
//! The wrapper functions below mirror the real FFI surface -- parameters typed via `in_param_ty!`
|
||||
//! / `out_param_ty!` and a `CError` return -- but omit the `extern "C"` / `no_mangle` attributes.
|
||||
//! Each is exercised by a `#[test]` caller, in the same shape as a real `WFProgramCreate`-style
|
||||
//! FFI function.
|
||||
//!
|
||||
//! Note: on the error path the value of any output parameter is undefined behavior; the error-case
|
||||
//! tests provide a valid pointer but intentionally never read it back.
|
||||
|
||||
use sarasacw_omrf::last_error::{self, CError, CERROR_OK, CStrPtr};
|
||||
use sarasacw_omrf::{cffi_wrapper, in_param_ty, out_param_ty};
|
||||
use thiserror::Error;
|
||||
|
||||
// ---- project-side conventions required by `cffi_wrapper!` ----
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum TestErr {
|
||||
#[error("not found")]
|
||||
NotFound,
|
||||
#[error("bad input")]
|
||||
Bad,
|
||||
#[error("parse: {0}")]
|
||||
Parse(#[from] core::num::ParseIntError),
|
||||
}
|
||||
|
||||
impl From<TestErr> for CError {
|
||||
fn from(e: TestErr) -> Self {
|
||||
match e {
|
||||
TestErr::NotFound => 2,
|
||||
TestErr::Bad => 3,
|
||||
TestErr::Parse(_) => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Result<T> = core::result::Result<T, TestErr>;
|
||||
|
||||
fn parse_cstr(ptr: CStrPtr) -> String {
|
||||
assert!(!ptr.is_null());
|
||||
unsafe { std::ffi::CStr::from_ptr(ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
// ============ FFI-style wrapper functions ============
|
||||
|
||||
/// Coverage: no inputs, no outputs -- success path. Returns absolute success and leaves no last
|
||||
/// error.
|
||||
fn ping() -> CError {
|
||||
cffi_wrapper!(|| { Ok(()) })
|
||||
}
|
||||
|
||||
/// Coverage: no inputs, no outputs -- error path. Returns the mapped code and records the message.
|
||||
fn ping_fail() -> CError {
|
||||
cffi_wrapper!(|| { Err(TestErr::NotFound) })
|
||||
}
|
||||
|
||||
/// Coverage: one input, no outputs. Succeeds for non-negative input, fails (`Bad`) otherwise.
|
||||
fn validate(x: in_param_ty!(i32)) -> CError {
|
||||
cffi_wrapper!(|x: i32| {
|
||||
if x < 0 {
|
||||
Err(TestErr::Bad)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Coverage: no inputs, one output -- success path. Writes the result through the output pointer.
|
||||
fn make_answer(out: out_param_ty!(i32)) -> CError {
|
||||
cffi_wrapper!(|| -> (out: i32) { Ok(42) })
|
||||
}
|
||||
|
||||
/// Coverage: no inputs, one output -- error path. The output value is UB after the call.
|
||||
fn make_fail(out: out_param_ty!(i32)) -> CError {
|
||||
cffi_wrapper!(|| -> (out: i32) { Err(TestErr::NotFound) })
|
||||
}
|
||||
|
||||
/// Coverage: one input, one output.
|
||||
fn scale(x: in_param_ty!(i32), out: out_param_ty!(i32)) -> CError {
|
||||
cffi_wrapper!(|x: i32| -> (out: i32) { Ok(x * 2) })
|
||||
}
|
||||
|
||||
/// Coverage: two inputs, two outputs (structured destructuring assignment through the pointers).
|
||||
fn add_mul(
|
||||
a: in_param_ty!(i32),
|
||||
b: in_param_ty!(i32),
|
||||
sum: out_param_ty!(i32),
|
||||
prod: out_param_ty!(i32),
|
||||
) -> CError {
|
||||
cffi_wrapper!(|a: i32, b: i32| -> (sum: i32, prod: i32) { Ok((a + b, a * b)) })
|
||||
}
|
||||
|
||||
/// Coverage: no inputs, two outputs (the no-input branch of the multi-output shape).
|
||||
fn make_pair(o1: out_param_ty!(i32), o2: out_param_ty!(i32)) -> CError {
|
||||
cffi_wrapper!(|| -> (o1: i32, o2: i32) { Ok((10, 20)) })
|
||||
}
|
||||
|
||||
/// Coverage: the `?` operator propagating a sub-error (`ParseIntError`) into `TestErr` via the
|
||||
/// `#[from]` conversion, exactly like the `WFProgramCreate` example chains `?`.
|
||||
fn parse_num(s: in_param_ty!(&str), out: out_param_ty!(i32)) -> CError {
|
||||
cffi_wrapper!(|s: &str| -> (out: i32) {
|
||||
let n: i32 = s.parse()?;
|
||||
Ok(n)
|
||||
})
|
||||
}
|
||||
|
||||
// ============ test callers ============
|
||||
|
||||
#[test]
|
||||
fn ping_returns_success() {
|
||||
last_error::clear_last_error();
|
||||
assert_eq!(ping(), CERROR_OK);
|
||||
assert!(!last_error::has_last_message());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ping_fail_records_error() {
|
||||
last_error::clear_last_error();
|
||||
assert_eq!(ping_fail(), 2);
|
||||
assert_eq!(parse_cstr(last_error::get_error_message()), "not found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_accepts_non_negative() {
|
||||
last_error::clear_last_error();
|
||||
assert_eq!(validate(5), CERROR_OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_negative() {
|
||||
last_error::clear_last_error();
|
||||
assert_eq!(validate(-1), 3);
|
||||
assert_eq!(parse_cstr(last_error::get_error_message()), "bad input");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_answer_writes_output() {
|
||||
last_error::clear_last_error();
|
||||
let mut out: i32 = 0;
|
||||
assert_eq!(make_answer(&mut out), CERROR_OK);
|
||||
assert_eq!(out, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_fail_records_error() {
|
||||
last_error::clear_last_error();
|
||||
let mut out: i32 = 0;
|
||||
// A valid pointer is required, but its value is UB after an error and is intentionally unread.
|
||||
assert_eq!(make_fail(&mut out), 2);
|
||||
assert_eq!(parse_cstr(last_error::get_error_message()), "not found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_writes_doubled() {
|
||||
last_error::clear_last_error();
|
||||
let mut out: i32 = 0;
|
||||
assert_eq!(scale(21, &mut out), CERROR_OK);
|
||||
assert_eq!(out, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_mul_writes_both_outputs() {
|
||||
last_error::clear_last_error();
|
||||
let mut sum: i32 = 0;
|
||||
let mut prod: i32 = 0;
|
||||
assert_eq!(add_mul(3, 4, &mut sum, &mut prod), CERROR_OK);
|
||||
assert_eq!(sum, 7);
|
||||
assert_eq!(prod, 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_pair_writes_both_outputs() {
|
||||
last_error::clear_last_error();
|
||||
let mut a: i32 = 0;
|
||||
let mut b: i32 = 0;
|
||||
assert_eq!(make_pair(&mut a, &mut b), CERROR_OK);
|
||||
assert_eq!(a, 10);
|
||||
assert_eq!(b, 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_num_success() {
|
||||
last_error::clear_last_error();
|
||||
let mut out: i32 = 0;
|
||||
assert_eq!(parse_num("123", &mut out), CERROR_OK);
|
||||
assert_eq!(out, 123);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_num_fail_propagates_suberror() {
|
||||
last_error::clear_last_error();
|
||||
let mut out: i32 = 0;
|
||||
// Output value is UB after an error and is intentionally unread.
|
||||
assert_eq!(parse_num("not_a_num", &mut out), 4);
|
||||
assert!(parse_cstr(last_error::get_error_message()).contains("parse"));
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//! Integration tests for the `last_error` module.
|
||||
|
||||
use sarasacw_omrf::last_error::{self, CError, CERROR_OK, CStrPtr};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum TestErr {
|
||||
#[error("boom error")]
|
||||
Boom,
|
||||
#[error("bad\x00value")]
|
||||
WithNul,
|
||||
}
|
||||
|
||||
impl From<TestErr> for CError {
|
||||
fn from(e: TestErr) -> Self {
|
||||
match e {
|
||||
TestErr::Boom => 7,
|
||||
TestErr::WithNul => 9,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_cstr(ptr: CStrPtr) -> String {
|
||||
assert!(!ptr.is_null());
|
||||
unsafe { std::ffi::CStr::from_ptr(ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fresh_state_is_absolute_success() {
|
||||
last_error::clear_last_error();
|
||||
assert!(!last_error::has_last_message());
|
||||
assert_eq!(last_error::get_error_code(), CERROR_OK);
|
||||
assert_eq!(parse_cstr(last_error::get_error_message()), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_records_code_and_message() {
|
||||
last_error::clear_last_error();
|
||||
last_error::set_last_error(TestErr::Boom);
|
||||
assert!(last_error::has_last_message());
|
||||
assert_eq!(last_error::get_error_code(), 7);
|
||||
assert_eq!(parse_cstr(last_error::get_error_message()), "boom error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_resets_to_absolute_success() {
|
||||
last_error::set_last_error(TestErr::Boom);
|
||||
assert!(last_error::has_last_message());
|
||||
last_error::clear_last_error();
|
||||
assert!(!last_error::has_last_message());
|
||||
assert_eq!(last_error::get_error_code(), CERROR_OK);
|
||||
assert_eq!(parse_cstr(last_error::get_error_message()), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interior_nul_in_message_is_sanitized() {
|
||||
last_error::clear_last_error();
|
||||
last_error::set_last_error(TestErr::WithNul);
|
||||
assert_eq!(last_error::get_error_code(), 9);
|
||||
let msg = parse_cstr(last_error::get_error_message());
|
||||
assert!(!msg.contains('\0'));
|
||||
assert!(msg.contains("bad"));
|
||||
assert!(msg.contains("value"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overwriting_replaces_previous_state() {
|
||||
last_error::clear_last_error();
|
||||
last_error::set_last_error(TestErr::Boom);
|
||||
last_error::set_last_error(TestErr::WithNul);
|
||||
assert_eq!(last_error::get_error_code(), 9);
|
||||
let msg = parse_cstr(last_error::get_error_message());
|
||||
assert!(!msg.contains("boom"));
|
||||
}
|
||||
Reference in New Issue
Block a user