//! 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 = (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 = (0..64).map(|i| i as f32 * 0.5).collect(); let data_b: Vec = (0..64).map(|i| -(i as f32) * 0.3).collect(); let data_c: Vec = (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"); }