feat: improve object pool
- improve object pool - add docstring for test coverage
This commit is contained in:
+42
-7
@@ -10,6 +10,7 @@ fn cptr_of(bytes: &[u8]) -> cstr_ffi::CStrPtr {
|
||||
bytes.as_ptr() as cstr_ffi::CStrPtr
|
||||
}
|
||||
|
||||
/// Coverage: single NUL-terminated string push -> parse round trip.
|
||||
#[test]
|
||||
fn push_single_string_round_trip() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
@@ -18,12 +19,16 @@ fn push_single_string_round_trip() {
|
||||
assert_eq!(cstr_ffi::parse_ffi_string(ptr).unwrap(), "hello");
|
||||
}
|
||||
|
||||
/// Coverage: pointers returned by earlier `push_ffi_string` calls stay valid after the internal
|
||||
/// storage reallocates from many later pushes (move-stability of the cached `CString` buffers).
|
||||
#[test]
|
||||
fn pushed_strings_remain_valid_after_more_pushes() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
let p1 = cstr_ffi::push_ffi_string("first").unwrap();
|
||||
// Force the internal Vec to reallocate by pushing many entries.
|
||||
let later: Vec<_> = (0..64).map(|_| cstr_ffi::push_ffi_string("filler").unwrap()).collect();
|
||||
let later: Vec<_> = (0..64)
|
||||
.map(|_| cstr_ffi::push_ffi_string("filler").unwrap())
|
||||
.collect();
|
||||
let p2 = cstr_ffi::push_ffi_string("second").unwrap();
|
||||
let p3 = cstr_ffi::push_ffi_string("third").unwrap();
|
||||
assert_eq!(cstr_ffi::parse_ffi_string(p1).unwrap(), "first");
|
||||
@@ -32,17 +37,20 @@ fn pushed_strings_remain_valid_after_more_pushes() {
|
||||
assert_eq!(cstr_ffi::parse_ffi_string(later[40]).unwrap(), "filler");
|
||||
}
|
||||
|
||||
/// Coverage: `push_ffi_string` rejects a string containing an interior NUL byte.
|
||||
#[test]
|
||||
fn push_ffi_string_rejects_interior_nul() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
assert!(cstr_ffi::push_ffi_string("a\x00b").is_err());
|
||||
}
|
||||
|
||||
/// Coverage: `parse_ffi_string` rejects a null pointer.
|
||||
#[test]
|
||||
fn parse_ffi_string_null_ptr_errors() {
|
||||
assert!(cstr_ffi::parse_ffi_string(std::ptr::null()).is_err());
|
||||
}
|
||||
|
||||
/// Coverage: `parse_ffi_string` rejects a NUL-terminated buffer whose content is not valid UTF-8.
|
||||
#[test]
|
||||
fn parse_ffi_string_invalid_utf8_errors() {
|
||||
let mut buf: Vec<u8> = vec![b'h', b'i', 0xFF];
|
||||
@@ -51,6 +59,7 @@ fn parse_ffi_string_invalid_utf8_errors() {
|
||||
assert!(cstr_ffi::parse_ffi_string(ptr).is_err());
|
||||
}
|
||||
|
||||
/// Coverage: nullptr-terminated string vector push -> parse round trip.
|
||||
#[test]
|
||||
fn push_string_vec_nullptr_terminated_round_trip() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
@@ -59,6 +68,8 @@ fn push_string_vec_nullptr_terminated_round_trip() {
|
||||
assert_eq!(collected, vec!["a", "bb", "ccc"]);
|
||||
}
|
||||
|
||||
/// Coverage: an empty input to `push_ffi_string_vec` yields a valid empty list (only the trailing
|
||||
/// null), which parses back to an empty vector.
|
||||
#[test]
|
||||
fn push_string_vec_empty_is_valid_empty_list() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
@@ -67,17 +78,20 @@ fn push_string_vec_empty_is_valid_empty_list() {
|
||||
assert!(collected.is_empty());
|
||||
}
|
||||
|
||||
/// Coverage: `push_ffi_string_vec` rejects an entry containing an interior NUL byte.
|
||||
#[test]
|
||||
fn push_string_vec_rejects_interior_nul() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
assert!(cstr_ffi::push_ffi_string_vec(&["ok", "ba\x00d"]).is_err());
|
||||
}
|
||||
|
||||
/// Coverage: `parse_ffi_string_vec` (nullptr-terminated) rejects a null pointer.
|
||||
#[test]
|
||||
fn parse_ffi_string_vec_null_ptr_errors() {
|
||||
assert!(cstr_ffi::parse_ffi_string_vec(std::ptr::null()).is_err());
|
||||
}
|
||||
|
||||
/// Coverage: count-terminated string vector push -> parse round trip, with the returned count.
|
||||
#[test]
|
||||
fn push_string_vec_with_len_round_trip() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
@@ -87,6 +101,8 @@ fn push_string_vec_with_len_round_trip() {
|
||||
assert_eq!(collected, vec!["x", "yy", "zzz"]);
|
||||
}
|
||||
|
||||
/// Coverage: empty input to `push_ffi_string_vec_with_len` yields count 0 and parses to an empty
|
||||
/// vector.
|
||||
#[test]
|
||||
fn push_string_vec_with_len_empty() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
@@ -96,20 +112,23 @@ fn push_string_vec_with_len_empty() {
|
||||
assert!(collected.is_empty());
|
||||
}
|
||||
|
||||
/// Coverage: `parse_ffi_string_vec_with_len` with count 0 accepts a null pointer and returns an
|
||||
/// empty vector.
|
||||
#[test]
|
||||
fn parse_ffi_string_vec_with_len_null_zero_len_is_empty() {
|
||||
assert!(
|
||||
cstr_ffi::parse_ffi_string_vec_with_len(std::ptr::null(), 0)
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
assert!(cstr_ffi::parse_ffi_string_vec_with_len(std::ptr::null(), 0)
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
/// Coverage: `parse_ffi_string_vec_with_len` with count > 0 rejects a null pointer.
|
||||
#[test]
|
||||
fn parse_ffi_string_vec_with_len_null_nonzero_errors() {
|
||||
assert!(cstr_ffi::parse_ffi_string_vec_with_len(std::ptr::null(), 1).is_err());
|
||||
}
|
||||
|
||||
/// Coverage: independent single-string, count-terminated, and nullptr-terminated slots produced in
|
||||
/// one call cycle all coexist and read back correctly.
|
||||
#[test]
|
||||
fn multiple_string_vec_slots_coexist() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
@@ -127,6 +146,8 @@ fn multiple_string_vec_slots_coexist() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Coverage: `push_ffi_string_view` re-exposes the source string in place (no copy) -- the pointer
|
||||
/// aliases the source buffer and the length matches.
|
||||
#[test]
|
||||
fn push_ffi_string_view_points_into_source() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
@@ -138,6 +159,7 @@ fn push_ffi_string_view_points_into_source() {
|
||||
assert_eq!(parsed, owned);
|
||||
}
|
||||
|
||||
/// Coverage: a string view may contain interior NUL bytes (length-delimited, not NUL-terminated).
|
||||
#[test]
|
||||
fn string_view_allows_interior_nul() {
|
||||
let src: &str = "ab\x00cd";
|
||||
@@ -147,16 +169,20 @@ fn string_view_allows_interior_nul() {
|
||||
assert_eq!(parsed.as_bytes(), src.as_bytes());
|
||||
}
|
||||
|
||||
/// Coverage: `parse_ffi_string_view` with a null pointer and zero length is interpreted as an
|
||||
/// empty string (C++ default `std::string_view`).
|
||||
#[test]
|
||||
fn parse_ffi_string_view_null_zero_is_empty() {
|
||||
assert_eq!(cstr_ffi::parse_ffi_string_view(std::ptr::null(), 0).unwrap(), "");
|
||||
}
|
||||
|
||||
/// Coverage: `parse_ffi_string_view` rejects a null pointer when the length is non-zero.
|
||||
#[test]
|
||||
fn parse_ffi_string_view_null_nonzero_errors() {
|
||||
assert!(cstr_ffi::parse_ffi_string_view(std::ptr::null(), 3).is_err());
|
||||
}
|
||||
|
||||
/// Coverage: `parse_ffi_string_view` rejects a buffer whose content is not valid UTF-8.
|
||||
#[test]
|
||||
fn parse_ffi_string_view_invalid_utf8_errors() {
|
||||
let bad: [u8; 2] = [b'a', 0xFF];
|
||||
@@ -164,6 +190,8 @@ fn parse_ffi_string_view_invalid_utf8_errors() {
|
||||
assert!(cstr_ffi::parse_ffi_string_view(ptr, 2).is_err());
|
||||
}
|
||||
|
||||
/// Coverage: string-view vector push -> parse round trip; the container is cached while the string
|
||||
/// data stays caller-owned.
|
||||
#[test]
|
||||
fn push_string_view_vec_with_len_round_trip() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
@@ -176,16 +204,18 @@ fn push_string_view_vec_with_len_round_trip() {
|
||||
assert_eq!(parsed, vec!["alpha", "beta", "gamma"]);
|
||||
}
|
||||
|
||||
/// Coverage: empty input to `push_ffi_string_view_vec_with_len` yields count 0; parsing (which
|
||||
/// ignores the possibly-dangling head) returns an empty vector.
|
||||
#[test]
|
||||
fn push_string_view_vec_with_len_empty() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
let (head, count) = cstr_ffi::push_ffi_string_view_vec_with_len(&[]);
|
||||
assert_eq!(count, 0);
|
||||
// count is 0, so head is ignored (may be dangling); parsing must yield an empty vector.
|
||||
let collected = cstr_ffi::parse_ffi_string_view_vec_with_len(head, count).unwrap();
|
||||
assert!(collected.is_empty());
|
||||
}
|
||||
|
||||
/// Coverage: string-view vector entries may individually contain interior NUL bytes.
|
||||
#[test]
|
||||
fn string_view_vec_allows_interior_nul_entries() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
@@ -198,6 +228,8 @@ fn string_view_vec_allows_interior_nul_entries() {
|
||||
assert_eq!(parsed[1].as_bytes(), s2.as_bytes());
|
||||
}
|
||||
|
||||
/// Coverage: `parse_ffi_string_view_vec_with_len` with count 0 accepts a null pointer and returns
|
||||
/// an empty vector.
|
||||
#[test]
|
||||
fn parse_ffi_string_view_vec_null_zero_len_is_empty() {
|
||||
assert!(cstr_ffi::parse_ffi_string_view_vec_with_len(std::ptr::null(), 0)
|
||||
@@ -205,11 +237,14 @@ fn parse_ffi_string_view_vec_null_zero_len_is_empty() {
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
/// Coverage: `parse_ffi_string_view_vec_with_len` with count > 0 rejects a null pointer.
|
||||
#[test]
|
||||
fn parse_ffi_string_view_vec_null_nonzero_errors() {
|
||||
assert!(cstr_ffi::parse_ffi_string_view_vec_with_len(std::ptr::null(), 1).is_err());
|
||||
}
|
||||
|
||||
/// Coverage: after `clear_ffi_strings`, the cache is reusable -- a fresh push/parse works
|
||||
/// independently of any state pushed before the clear.
|
||||
#[test]
|
||||
fn clear_makes_cache_reusable() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
|
||||
@@ -27,6 +27,7 @@ fn parse_cstr(ptr: CStrPtr) -> String {
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Coverage: the cleared/default state -- no outcome recorded, code is `CERROR_OK`, message empty.
|
||||
#[test]
|
||||
fn fresh_state_is_absolute_success() {
|
||||
last_error::clear_last_error();
|
||||
@@ -35,6 +36,8 @@ fn fresh_state_is_absolute_success() {
|
||||
assert_eq!(parse_cstr(last_error::get_error_message()), "");
|
||||
}
|
||||
|
||||
/// Coverage: `set_last_error` records both the `Into<CError>`-mapped code and the `Display`-derived
|
||||
/// message, and `has_last_message` becomes true.
|
||||
#[test]
|
||||
fn set_records_code_and_message() {
|
||||
last_error::clear_last_error();
|
||||
@@ -44,6 +47,8 @@ fn set_records_code_and_message() {
|
||||
assert_eq!(parse_cstr(last_error::get_error_message()), "boom error");
|
||||
}
|
||||
|
||||
/// Coverage: `clear_last_error` resets a previously-recorded outcome back to the absolute-success
|
||||
/// state (no code, empty message).
|
||||
#[test]
|
||||
fn clear_resets_to_absolute_success() {
|
||||
last_error::set_last_error(TestErr::Boom);
|
||||
@@ -54,6 +59,8 @@ fn clear_resets_to_absolute_success() {
|
||||
assert_eq!(parse_cstr(last_error::get_error_message()), "");
|
||||
}
|
||||
|
||||
/// Coverage: interior NUL bytes in an error's `Display` text are replaced so the message C string
|
||||
/// builds without panicking, and the code is still recorded.
|
||||
#[test]
|
||||
fn interior_nul_in_message_is_sanitized() {
|
||||
last_error::clear_last_error();
|
||||
@@ -65,6 +72,7 @@ fn interior_nul_in_message_is_sanitized() {
|
||||
assert!(msg.contains("value"));
|
||||
}
|
||||
|
||||
/// Coverage: a second `set_last_error` fully overwrites the previously recorded code and message.
|
||||
#[test]
|
||||
fn overwriting_replaces_previous_state() {
|
||||
last_error::clear_last_error();
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
//! Integration tests for the `object_pool` module.
|
||||
|
||||
use sarasacw_omrf::object_pool::{ObjectPool, INVALID_TOKEN};
|
||||
|
||||
/// Coverage: the `INVALID_TOKEN` constant is `0`, the value the counter never produces.
|
||||
#[test]
|
||||
fn invalid_token_is_zero() {
|
||||
assert_eq!(INVALID_TOKEN, 0);
|
||||
}
|
||||
|
||||
/// Coverage: `allocate` returns distinct, non-zero tokens across successive calls.
|
||||
#[test]
|
||||
fn allocate_yields_distinct_nonzero_tokens() {
|
||||
let pool: ObjectPool<i32> = ObjectPool::new();
|
||||
let t1 = pool.allocate(10);
|
||||
let t2 = pool.allocate(20);
|
||||
let t3 = pool.allocate(30);
|
||||
assert_ne!(t1, 0);
|
||||
assert_ne!(t2, 0);
|
||||
assert_ne!(t3, 0);
|
||||
assert_ne!(t1, t2);
|
||||
assert_ne!(t2, t3);
|
||||
assert_ne!(t1, t3);
|
||||
}
|
||||
|
||||
/// Coverage: the guard-style `get` reads and `get_mut` writes an entry, with changes observable
|
||||
/// by a subsequent `get`.
|
||||
#[test]
|
||||
fn get_and_get_mut_round_trip() {
|
||||
let pool: ObjectPool<i32> = ObjectPool::new();
|
||||
let t = pool.allocate(7);
|
||||
assert_eq!(*pool.get(t).unwrap(), 7);
|
||||
*pool.get_mut(t).unwrap() = 99;
|
||||
assert_eq!(*pool.get(t).unwrap(), 99);
|
||||
}
|
||||
|
||||
/// Coverage: the deadlock-safe closure-based `get_with`/`get_mut_with` read and write an entry.
|
||||
#[test]
|
||||
fn get_with_and_get_mut_with_round_trip() {
|
||||
let pool: ObjectPool<i32> = ObjectPool::new();
|
||||
let t = pool.allocate(1);
|
||||
assert_eq!(pool.get_with(t, |v| *v).unwrap(), 1);
|
||||
pool.get_mut_with(t, |v| *v = 42).unwrap();
|
||||
assert_eq!(pool.get_with(t, |v| *v).unwrap(), 42);
|
||||
}
|
||||
|
||||
/// Coverage: `pop` returns the owned value and invalidates the token for any further access.
|
||||
#[test]
|
||||
fn pop_returns_owned_value_and_invalidates_token() {
|
||||
let pool: ObjectPool<String> = ObjectPool::new();
|
||||
let t = pool.allocate("hello".to_string());
|
||||
let owned = pool.pop(t).unwrap();
|
||||
assert_eq!(owned, "hello");
|
||||
assert!(pool.get(t).is_err());
|
||||
}
|
||||
|
||||
/// Coverage: `free` removes the entry; subsequent `get`/`get_mut` on the same token fail.
|
||||
#[test]
|
||||
fn free_invalidates_token() {
|
||||
let pool: ObjectPool<i32> = ObjectPool::new();
|
||||
let t = pool.allocate(5);
|
||||
pool.free(t).unwrap();
|
||||
assert!(pool.get(t).is_err());
|
||||
assert!(pool.get_mut(t).is_err());
|
||||
}
|
||||
|
||||
/// Coverage: every access/mutation/removal method rejects `INVALID_TOKEN` (and any absent token).
|
||||
#[test]
|
||||
fn invalid_token_errors_on_all_methods() {
|
||||
let pool: ObjectPool<i32> = ObjectPool::new();
|
||||
let bad = INVALID_TOKEN;
|
||||
assert!(pool.get(bad).is_err());
|
||||
assert!(pool.get_mut(bad).is_err());
|
||||
assert!(pool.get_with(bad, |_| ()).is_err());
|
||||
assert!(pool.get_mut_with(bad, |_| ()).is_err());
|
||||
assert!(pool.free(bad).is_err());
|
||||
assert!(pool.pop(bad).is_err());
|
||||
}
|
||||
|
||||
/// Coverage: `clear` empties the pool; all previously-issued tokens become invalid.
|
||||
#[test]
|
||||
fn clear_removes_all_objects() {
|
||||
let pool: ObjectPool<i32> = ObjectPool::new();
|
||||
let t1 = pool.allocate(1);
|
||||
let t2 = pool.allocate(2);
|
||||
pool.clear();
|
||||
assert!(pool.get(t1).is_err());
|
||||
assert!(pool.get(t2).is_err());
|
||||
}
|
||||
|
||||
/// Coverage: two distinct tokens can be mutated independently via sequential closure access, which
|
||||
/// never holds two guards at once (the deadlock-safe pattern).
|
||||
#[test]
|
||||
fn two_tokens_modified_independently_via_closure() {
|
||||
let pool: ObjectPool<i32> = ObjectPool::new();
|
||||
let t1 = pool.allocate(0);
|
||||
let t2 = pool.allocate(0);
|
||||
pool.get_mut_with(t1, |v| *v = 11).unwrap();
|
||||
pool.get_mut_with(t2, |v| *v = 22).unwrap();
|
||||
assert_eq!(pool.get_with(t1, |v| *v).unwrap(), 11);
|
||||
assert_eq!(pool.get_with(t2, |v| *v).unwrap(), 22);
|
||||
}
|
||||
Reference in New Issue
Block a user