Files
wifi-densepose/crates/ruvector-temporal-tensor/tests/wasm_ffi_test.rs
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

354 lines
12 KiB
Rust

//! FFI interface tests for the temporal tensor store.
//!
//! These tests exercise the `tts_*` extern "C" functions exposed by
//! `store_ffi.rs` through their public API. Because the FFI layer uses
//! a single global `STORE_STATE`, tests **must** run sequentially:
//!
//! ```bash
//! cargo test -p ruvector-temporal-tensor --test wasm_ffi_test --features ffi -- --test-threads=1
//! ```
#![cfg(feature = "ffi")]
use ruvector_temporal_tensor::store_ffi::{
tts_block_count, tts_evict, tts_get, tts_init, tts_put, tts_stats, tts_tier_count, tts_touch,
};
// ── Constants mirrored from store_ffi.rs ────────────────────────────────
const ERR_BLOCK_NOT_FOUND: i32 = -4;
const ERR_BUFFER_TOO_SMALL: i32 = -5;
/// Binary stats size: 5 * u32 + 2 * u64 = 36 bytes.
const STATS_SIZE: usize = 5 * 4 + 2 * 8;
// ── Helpers ─────────────────────────────────────────────────────────────
/// Re-initialize the global store with default config before each test.
/// This replaces whatever state was left by a previous test.
fn reset() {
let rc = tts_init(std::ptr::null(), 0);
assert_eq!(rc, 0, "tts_init with default config must succeed");
}
/// Read a little-endian u32 from `buf` at the given byte offset.
fn read_u32_le(buf: &[u8], off: usize) -> u32 {
u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
}
/// Read a little-endian u64 from `buf` at the given byte offset.
fn read_u64_le(buf: &[u8], off: usize) -> u64 {
let mut arr = [0u8; 8];
arr.copy_from_slice(&buf[off..off + 8]);
u64::from_le_bytes(arr)
}
// ── Tests ───────────────────────────────────────────────────────────────
#[test]
fn test_ffi_init_and_destroy() {
// Calling tts_init with a null pointer and zero length should use
// the default TierConfig and return success (0).
let rc = tts_init(std::ptr::null(), 0);
assert_eq!(rc, 0, "tts_init should return 0 on success");
// The freshly initialized store must contain zero blocks.
assert_eq!(tts_block_count(), 0, "new store should have 0 blocks");
// Re-initializing must also succeed (replaces old state).
let rc2 = tts_init(std::ptr::null(), 0);
assert_eq!(rc2, 0, "re-init should succeed");
assert_eq!(tts_block_count(), 0, "re-init should reset block count");
}
#[test]
fn test_ffi_put_get_roundtrip() {
reset();
// Create 64 f32 values with a clear pattern.
let data: Vec<f32> = (0..64).map(|i| (i as f32 - 32.0) * 0.1).collect();
let rc = tts_put(0, 1, 0, data.as_ptr(), data.len());
assert_eq!(rc, 0, "tts_put should return 0 on success");
let mut out = vec![0.0f32; 64];
let n = tts_get(0, 1, 0, out.as_mut_ptr(), out.len());
assert_eq!(n, 64, "tts_get should return 64 elements");
// Verify accuracy. New blocks default to Hot (8-bit quantization)
// so the error should be small.
let max_abs = data.iter().map(|v| v.abs()).fold(0.0f32, f32::max);
for (i, (&orig, &dec)) in data.iter().zip(out.iter()).enumerate() {
let err = (orig - dec).abs();
assert!(
err < max_abs * 0.05,
"element {i}: orig={orig}, decoded={dec}, err={err}, tolerance={}",
max_abs * 0.05,
);
}
}
#[test]
fn test_ffi_multi_tensor() {
reset();
let data_a: Vec<f32> = (0..64).map(|i| i as f32 * 0.5).collect();
let data_b: Vec<f32> = (0..64).map(|i| -(i as f32) * 0.3).collect();
let data_c: Vec<f32> = (0..64).map(|i| (i as f32).sin()).collect();
// Three different tensor IDs using hi/lo split for u128:
// tensor A: hi=0, lo=1 -> tensor_id = 1
// tensor B: hi=0, lo=2 -> tensor_id = 2
// tensor C: hi=1, lo=0 -> tensor_id = 1 << 64
assert_eq!(tts_put(0, 1, 0, data_a.as_ptr(), data_a.len()), 0);
assert_eq!(tts_put(0, 2, 0, data_b.as_ptr(), data_b.len()), 0);
assert_eq!(tts_put(1, 0, 0, data_c.as_ptr(), data_c.len()), 0);
assert_eq!(tts_block_count(), 3, "should have 3 blocks total");
// Read back each tensor independently.
let mut out = vec![0.0f32; 64];
let n_a = tts_get(0, 1, 0, out.as_mut_ptr(), out.len());
assert_eq!(n_a, 64);
// Spot-check first element of tensor A.
assert!(
(out[0] - data_a[0]).abs() < 0.5,
"tensor A readback mismatch"
);
let n_b = tts_get(0, 2, 0, out.as_mut_ptr(), out.len());
assert_eq!(n_b, 64);
assert!(
(out[0] - data_b[0]).abs() < 0.5,
"tensor B readback mismatch"
);
let n_c = tts_get(1, 0, 0, out.as_mut_ptr(), out.len());
assert_eq!(n_c, 64);
assert!(
(out[0] - data_c[0]).abs() < 0.5,
"tensor C readback mismatch"
);
}
#[test]
fn test_ffi_eviction() {
reset();
let data = vec![1.0f32; 64];
assert_eq!(tts_put(0, 42, 0, data.as_ptr(), data.len()), 0);
assert_eq!(tts_block_count(), 1);
// Evict the block.
let rc = tts_evict(0, 42, 0);
assert_eq!(rc, 0, "tts_evict should return 0 on success");
assert_eq!(tts_block_count(), 0, "evicted block should be gone");
// A subsequent get should return ERR_BLOCK_NOT_FOUND.
let mut out = vec![0.0f32; 64];
let rc_get = tts_get(0, 42, 0, out.as_mut_ptr(), out.len());
assert_eq!(
rc_get, ERR_BLOCK_NOT_FOUND,
"get after evict should return block-not-found"
);
// Evicting again should also return block-not-found.
let rc2 = tts_evict(0, 42, 0);
assert_eq!(rc2, ERR_BLOCK_NOT_FOUND);
}
#[test]
fn test_ffi_touch_updates_access() {
reset();
let data = vec![1.0f32; 64];
assert_eq!(tts_put(0, 7, 3, data.as_ptr(), data.len()), 0);
assert_eq!(tts_block_count(), 1);
// Touch the block multiple times.
for _ in 0..5 {
let rc = tts_touch(0, 7, 3);
assert_eq!(rc, 0, "tts_touch should return 0 on success");
}
// Block count should remain unchanged (touch does not add/remove blocks).
assert_eq!(tts_block_count(), 1, "touch should not change block count");
// The block should still be readable.
let mut out = vec![0.0f32; 64];
let n = tts_get(0, 7, 3, out.as_mut_ptr(), out.len());
assert_eq!(n, 64, "block should still be readable after touches");
// Touching a non-existent block should fail.
let rc_missing = tts_touch(0, 99, 0);
assert_eq!(rc_missing, ERR_BLOCK_NOT_FOUND);
}
#[test]
fn test_ffi_tier_counts() {
reset();
// All new blocks are placed in Hot (tier 0) by default.
let data = vec![1.0f32; 64];
assert_eq!(tts_put(0, 1, 0, data.as_ptr(), data.len()), 0);
assert_eq!(tts_put(0, 1, 1, data.as_ptr(), data.len()), 0);
assert_eq!(tts_put(0, 2, 0, data.as_ptr(), data.len()), 0);
assert_eq!(tts_block_count(), 3);
assert_eq!(tts_tier_count(0), 3, "all blocks should be Hot");
assert_eq!(tts_tier_count(1), 0, "no Warm blocks");
assert_eq!(tts_tier_count(2), 0, "no Cool blocks");
assert_eq!(tts_tier_count(3), 0, "no Cold blocks");
// Invalid tier should return an error.
assert!(tts_tier_count(99) < 0, "invalid tier should return error");
}
#[test]
fn test_ffi_stats_output() {
reset();
let data = vec![1.0f32; 64];
assert_eq!(tts_put(0, 1, 0, data.as_ptr(), data.len()), 0);
assert_eq!(tts_put(0, 1, 1, data.as_ptr(), data.len()), 0);
assert_eq!(tts_put(0, 2, 0, data.as_ptr(), data.len()), 0);
let mut buf = vec![0u8; STATS_SIZE];
let written = tts_stats(buf.as_mut_ptr(), buf.len());
assert_eq!(
written, STATS_SIZE as i32,
"tts_stats should write exactly {STATS_SIZE} bytes"
);
// Parse the binary stats layout:
// [block_count:u32][hot:u32][warm:u32][cool:u32][cold:u32]
// [total_bytes:u64][tick_count:u64]
let block_count = read_u32_le(&buf, 0);
let hot = read_u32_le(&buf, 4);
let warm = read_u32_le(&buf, 8);
let cool = read_u32_le(&buf, 12);
let cold = read_u32_le(&buf, 16);
let total_bytes = read_u64_le(&buf, 20);
let _tick_count = read_u64_le(&buf, 28);
assert_eq!(block_count, 3, "block_count mismatch");
assert_eq!(hot, 3, "hot count mismatch");
assert_eq!(warm, 0, "warm count mismatch");
assert_eq!(cool, 0, "cool count mismatch");
assert_eq!(cold, 0, "cold count mismatch");
assert!(total_bytes > 0, "total_bytes should be > 0 after puts");
// Verify stats rejects a too-small buffer.
let mut small_buf = vec![0u8; 4];
let rc = tts_stats(small_buf.as_mut_ptr(), small_buf.len());
assert_eq!(rc, ERR_BUFFER_TOO_SMALL);
}
#[test]
fn test_ffi_put_multiple_blocks_same_tensor() {
reset();
let data = vec![2.5f32; 64];
// Put 5 blocks for the same tensor (different block indices).
for idx in 0..5u32 {
let rc = tts_put(0, 10, idx, data.as_ptr(), data.len());
assert_eq!(rc, 0, "put block_index={idx} should succeed");
}
assert_eq!(tts_block_count(), 5);
// Each block should be independently readable.
let mut out = vec![0.0f32; 64];
for idx in 0..5u32 {
let n = tts_get(0, 10, idx, out.as_mut_ptr(), out.len());
assert_eq!(n, 64, "block_index={idx} should return 64 elements");
}
}
#[test]
fn test_ffi_overwrite_block() {
reset();
let data1 = vec![1.0f32; 64];
assert_eq!(tts_put(0, 5, 0, data1.as_ptr(), data1.len()), 0);
let data2 = vec![9.0f32; 64];
assert_eq!(tts_put(0, 5, 0, data2.as_ptr(), data2.len()), 0);
// Block count should still be 1 (overwrite, not insert).
assert_eq!(tts_block_count(), 1);
// Should read back the second write.
let mut out = vec![0.0f32; 64];
let n = tts_get(0, 5, 0, out.as_mut_ptr(), out.len());
assert_eq!(n, 64);
for &v in &out {
assert!(
(v - 9.0).abs() < 0.5,
"expected ~9.0 after overwrite, got {v}"
);
}
}
#[test]
fn test_ffi_get_buffer_too_small() {
reset();
let data = vec![1.0f32; 64];
assert_eq!(tts_put(0, 1, 0, data.as_ptr(), data.len()), 0);
let mut small_out = vec![0.0f32; 2];
let rc = tts_get(0, 1, 0, small_out.as_mut_ptr(), small_out.len());
assert_eq!(
rc, ERR_BUFFER_TOO_SMALL,
"get with undersized buffer should return buffer-too-small"
);
}
#[test]
fn test_ffi_evict_then_reinsert() {
reset();
let data = vec![3.0f32; 64];
assert_eq!(tts_put(0, 1, 0, data.as_ptr(), data.len()), 0);
assert_eq!(tts_block_count(), 1);
// Evict.
assert_eq!(tts_evict(0, 1, 0), 0);
assert_eq!(tts_block_count(), 0);
// Re-insert at the same key.
let data2 = vec![7.0f32; 64];
assert_eq!(tts_put(0, 1, 0, data2.as_ptr(), data2.len()), 0);
assert_eq!(tts_block_count(), 1);
// Should read back the new data.
let mut out = vec![0.0f32; 64];
let n = tts_get(0, 1, 0, out.as_mut_ptr(), out.len());
assert_eq!(n, 64);
for &v in &out {
assert!(
(v - 7.0).abs() < 0.5,
"expected ~7.0 after re-insert, got {v}"
);
}
}
#[test]
fn test_ffi_large_tensor_id() {
reset();
// Use the full u128 range: hi=u64::MAX, lo=u64::MAX -> tensor_id = u128::MAX.
let data = vec![0.5f32; 64];
assert_eq!(
tts_put(u64::MAX, u64::MAX, 0, data.as_ptr(), data.len()),
0,
"put with max tensor_id should succeed"
);
let mut out = vec![0.0f32; 64];
let n = tts_get(u64::MAX, u64::MAX, 0, out.as_mut_ptr(), out.len());
assert_eq!(n, 64, "get with max tensor_id should succeed");
}