Wave 64-65 cleanup: Proto regeneration and build system updates from Tonic 0.12→0.14 upgrade Files updated: - Cargo.lock: Dependency resolution for Tonic 0.14.2 - All build.rs: Updated for tonic-prost-build - Proto files: Regenerated with tonic-prost 0.14 - Examples/tests: Updated for new gRPC API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
447 lines
13 KiB
Rust
447 lines
13 KiB
Rust
#![allow(unused_crate_dependencies)]
|
|
//! Comprehensive tests for SIMD fallback paths and lock-free data structures
|
|
//!
|
|
//! This test suite ensures proper fallback behavior when CPU features are unavailable
|
|
//! and tests edge cases in concurrent data structures.
|
|
|
|
// use trading_engine::types::simd_optimizations::{CacheAlignedPriceArray, SIMDFinancialOps};
|
|
// use common::{Price, Quantity}; // Unused in current tests
|
|
use std::sync::Arc;
|
|
use std::thread;
|
|
use trading_engine::lockfree::mpsc_queue::{AtomicCounter, MPSCQueue};
|
|
|
|
// ============================================================================
|
|
// SIMD Fallback Path Tests
|
|
// ============================================================================
|
|
// Note: SIMD tests are disabled as simd_optimizations module is not implemented yet
|
|
|
|
/*
|
|
#[test]
|
|
fn test_simd_portfolio_value_empty_arrays() {
|
|
let positions: Vec<Quantity> = vec![];
|
|
let prices: Vec<Price> = vec![];
|
|
|
|
let result = SIMDFinancialOps::portfolio_value_simd(&positions, &prices);
|
|
assert_eq!(result, Price::ZERO);
|
|
}
|
|
|
|
#[test]
|
|
fn test_simd_portfolio_value_single_element() {
|
|
let positions = vec![Quantity(100)];
|
|
let prices = vec![Price::from_f64(50000.0).unwrap()];
|
|
|
|
let result = SIMDFinancialOps::portfolio_value_simd(&positions, &prices);
|
|
let expected = Price::from_f64(5_000_000.0).unwrap();
|
|
|
|
// Use approximate comparison for floating point
|
|
assert!((result.to_f64() - expected.to_f64()).abs() < 0.01);
|
|
}
|
|
|
|
#[test]
|
|
fn test_simd_portfolio_value_misaligned_length() {
|
|
// Test with length that's not a multiple of 8
|
|
let positions = vec![
|
|
Quantity(100),
|
|
Quantity(200),
|
|
Quantity(150),
|
|
Quantity(75),
|
|
Quantity(300),
|
|
];
|
|
|
|
let prices = vec![
|
|
Price::from_f64(50000.0).unwrap(),
|
|
Price::from_f64(3000.0).unwrap(),
|
|
Price::from_f64(100.0).unwrap(),
|
|
Price::from_f64(1.0).unwrap(),
|
|
Price::from_f64(200.0).unwrap(),
|
|
];
|
|
|
|
let result = SIMDFinancialOps::portfolio_value_simd(&positions, &prices);
|
|
|
|
// Manual calculation: 100*50000 + 200*3000 + 150*100 + 75*1 + 300*200
|
|
// = 5,000,000 + 600,000 + 15,000 + 75 + 60,000 = 5,675,075
|
|
let expected = Price::from_f64(5_675_075.0).unwrap();
|
|
|
|
assert!((result.to_f64() - expected.to_f64()).abs() < 0.01);
|
|
}
|
|
|
|
#[test]
|
|
fn test_simd_portfolio_value_exactly_8_elements() {
|
|
// Test with exactly 8 elements (one SIMD register worth)
|
|
let positions = vec![
|
|
Quantity(100), Quantity(200), Quantity(150), Quantity(75),
|
|
Quantity(300), Quantity(50), Quantity(125), Quantity(175),
|
|
];
|
|
|
|
let prices: Vec<Price> = vec![
|
|
Price::from_f64(1000.0).unwrap(),
|
|
Price::from_f64(2000.0).unwrap(),
|
|
Price::from_f64(3000.0).unwrap(),
|
|
Price::from_f64(4000.0).unwrap(),
|
|
Price::from_f64(5000.0).unwrap(),
|
|
Price::from_f64(6000.0).unwrap(),
|
|
Price::from_f64(7000.0).unwrap(),
|
|
Price::from_f64(8000.0).unwrap(),
|
|
];
|
|
|
|
let result = SIMDFinancialOps::portfolio_value_simd(&positions, &prices);
|
|
|
|
// Manual: 100*1000 + 200*2000 + 150*3000 + 75*4000 + 300*5000 + 50*6000 + 125*7000 + 175*8000
|
|
// = 100000 + 400000 + 450000 + 300000 + 1500000 + 300000 + 875000 + 1400000 = 5,325,000
|
|
let expected = Price::from_f64(5_325_000.0).unwrap();
|
|
|
|
assert!((result.to_f64() - expected.to_f64()).abs() < 0.01);
|
|
}
|
|
|
|
#[test]
|
|
fn test_simd_portfolio_value_large_array() {
|
|
// Test with a large array (multiple SIMD registers)
|
|
let mut positions = Vec::new();
|
|
let mut prices = Vec::new();
|
|
|
|
for i in 0..1000 {
|
|
positions.push(Quantity(i + 1));
|
|
prices.push(Price::from_f64((i + 1) as f64 * 100.0).unwrap());
|
|
}
|
|
|
|
let result = SIMDFinancialOps::portfolio_value_simd(&positions, &prices);
|
|
|
|
// Verify result is reasonable (should be sum of i * i * 100 for i=1 to 1000)
|
|
// Sum formula: sum(i^2 * 100) = 100 * (n * (n+1) * (2n+1) / 6)
|
|
// For n=1000: 100 * (1000 * 1001 * 2001 / 6) = 333,833,500
|
|
let expected = Price::from_f64(333_833_500.0).unwrap();
|
|
|
|
assert!((result.to_f64() - expected.to_f64()).abs() < 100.0); // Allow some FP error
|
|
}
|
|
|
|
#[test]
|
|
fn test_simd_portfolio_value_zero_prices() {
|
|
let positions = vec![Quantity(100), Quantity(200), Quantity(150)];
|
|
let prices = vec![
|
|
Price::from_f64(0.0).unwrap(),
|
|
Price::from_f64(0.0).unwrap(),
|
|
Price::from_f64(0.0).unwrap(),
|
|
];
|
|
|
|
let result = SIMDFinancialOps::portfolio_value_simd(&positions, &prices);
|
|
assert_eq!(result, Price::ZERO);
|
|
}
|
|
|
|
#[test]
|
|
fn test_simd_portfolio_value_negative_positions() {
|
|
// Short positions (negative quantities)
|
|
let positions = vec![Quantity(-100), Quantity(-200)];
|
|
let prices = vec![
|
|
Price::from_f64(50000.0).unwrap(),
|
|
Price::from_f64(3000.0).unwrap(),
|
|
];
|
|
|
|
let result = SIMDFinancialOps::portfolio_value_simd(&positions, &prices);
|
|
|
|
// Result should be negative: -100*50000 + -200*3000 = -5,600,000
|
|
let expected = Price::from_f64(-5_600_000.0).unwrap();
|
|
|
|
assert!((result.to_f64() - expected.to_f64()).abs() < 0.01);
|
|
}
|
|
|
|
#[test]
|
|
fn test_cache_aligned_price_array() {
|
|
let prices = [
|
|
Price::from_f64(1.0).unwrap(),
|
|
Price::from_f64(2.0).unwrap(),
|
|
Price::from_f64(3.0).unwrap(),
|
|
Price::from_f64(4.0).unwrap(),
|
|
Price::from_f64(5.0).unwrap(),
|
|
Price::from_f64(6.0).unwrap(),
|
|
Price::from_f64(7.0).unwrap(),
|
|
Price::from_f64(8.0).unwrap(),
|
|
];
|
|
|
|
let aligned = CacheAlignedPriceArray::new(prices);
|
|
|
|
let slice = aligned.as_slice();
|
|
assert_eq!(slice.len(), 8);
|
|
assert_eq!(slice[0], prices[0]);
|
|
assert_eq!(slice[7], prices[7]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_cache_aligned_price_array_mutation() {
|
|
let prices = [Price::ZERO; 8];
|
|
let mut aligned = CacheAlignedPriceArray::new(prices);
|
|
|
|
let mut_slice = aligned.as_mut_slice();
|
|
mut_slice[0] = Price::from_f64(100.0).unwrap();
|
|
mut_slice[7] = Price::from_f64(200.0).unwrap();
|
|
|
|
let slice = aligned.as_slice();
|
|
assert_eq!(slice[0], Price::from_f64(100.0).unwrap());
|
|
assert_eq!(slice[7], Price::from_f64(200.0).unwrap());
|
|
}
|
|
*/
|
|
|
|
// ============================================================================
|
|
// Lock-free MPSC Queue Edge Cases
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_mpsc_queue_concurrent_push_pop() {
|
|
let queue = Arc::new(MPSCQueue::<u64>::new());
|
|
let num_producers = 8;
|
|
let items_per_producer = 10000;
|
|
|
|
// Spawn multiple producers
|
|
let mut producer_handles = Vec::new();
|
|
for producer_id in 0..num_producers {
|
|
let queue_clone = Arc::clone(&queue);
|
|
let handle = thread::spawn(move || {
|
|
for i in 0..items_per_producer {
|
|
let value = (producer_id as u64) * items_per_producer + i;
|
|
queue_clone.push(value);
|
|
}
|
|
});
|
|
producer_handles.push(handle);
|
|
}
|
|
|
|
// Consumer thread
|
|
let queue_consumer = Arc::clone(&queue);
|
|
let consumer_handle = thread::spawn(move || {
|
|
let mut received = Vec::new();
|
|
let expected_total = (num_producers * items_per_producer) as usize;
|
|
|
|
while received.len() < expected_total {
|
|
if let Some(item) = queue_consumer.try_pop() {
|
|
received.push(item);
|
|
} else {
|
|
thread::yield_now();
|
|
}
|
|
}
|
|
received
|
|
});
|
|
|
|
// Wait for producers
|
|
for handle in producer_handles {
|
|
handle.join().expect("Producer thread failed");
|
|
}
|
|
|
|
// Wait for consumer
|
|
let received = consumer_handle.join().expect("Consumer thread failed");
|
|
|
|
// Verify all items received
|
|
assert_eq!(
|
|
received.len(),
|
|
(num_producers * items_per_producer) as usize
|
|
);
|
|
|
|
// Verify queue is empty
|
|
assert!(queue.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_mpsc_queue_pop_from_empty() {
|
|
let queue = MPSCQueue::<u64>::new();
|
|
|
|
// Multiple pops from empty queue should return None
|
|
assert_eq!(queue.try_pop(), None);
|
|
assert_eq!(queue.try_pop(), None);
|
|
assert_eq!(queue.try_pop(), None);
|
|
|
|
assert!(queue.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_mpsc_queue_push_pop_interleaved() {
|
|
let queue = MPSCQueue::<u64>::new();
|
|
|
|
// Interleave push and pop operations
|
|
queue.push(1);
|
|
assert_eq!(queue.try_pop(), Some(1));
|
|
|
|
queue.push(2);
|
|
queue.push(3);
|
|
assert_eq!(queue.try_pop(), Some(2));
|
|
|
|
queue.push(4);
|
|
assert_eq!(queue.try_pop(), Some(3));
|
|
assert_eq!(queue.try_pop(), Some(4));
|
|
|
|
assert!(queue.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_mpsc_queue_rapid_push() {
|
|
let queue = Arc::new(MPSCQueue::<u64>::new());
|
|
let queue_clone = Arc::clone(&queue);
|
|
|
|
// Rapidly push 100,000 items
|
|
let handle = thread::spawn(move || {
|
|
for i in 0..100_000 {
|
|
queue_clone.push(i);
|
|
}
|
|
});
|
|
|
|
handle.join().expect("Producer thread failed");
|
|
|
|
// Verify count
|
|
assert_eq!(queue.len(), 100_000);
|
|
|
|
// Pop all items and verify order
|
|
let mut prev = None;
|
|
let mut count = 0;
|
|
|
|
while let Some(item) = queue.try_pop() {
|
|
if let Some(p) = prev {
|
|
assert_eq!(item, p + 1, "Items should be in order");
|
|
}
|
|
prev = Some(item);
|
|
count += 1;
|
|
}
|
|
|
|
assert_eq!(count, 100_000);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mpsc_queue_size_tracking() {
|
|
let queue = MPSCQueue::<u64>::new();
|
|
|
|
assert_eq!(queue.len(), 0);
|
|
assert!(queue.is_empty());
|
|
|
|
queue.push(1);
|
|
assert_eq!(queue.len(), 1);
|
|
assert!(!queue.is_empty());
|
|
|
|
queue.push(2);
|
|
queue.push(3);
|
|
assert_eq!(queue.len(), 3);
|
|
|
|
queue.try_pop();
|
|
assert_eq!(queue.len(), 2);
|
|
|
|
queue.try_pop();
|
|
queue.try_pop();
|
|
assert_eq!(queue.len(), 0);
|
|
assert!(queue.is_empty());
|
|
}
|
|
|
|
// ============================================================================
|
|
// Atomic Counter Edge Cases
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_atomic_counter_custom_increment() {
|
|
let counter = AtomicCounter::new_with(100, 5);
|
|
|
|
assert_eq!(counter.get(), 100);
|
|
assert_eq!(counter.next(), 100);
|
|
assert_eq!(counter.next(), 105);
|
|
assert_eq!(counter.next(), 110);
|
|
assert_eq!(counter.get(), 115);
|
|
}
|
|
|
|
#[test]
|
|
fn test_atomic_counter_reset() {
|
|
let counter = AtomicCounter::new();
|
|
|
|
counter.next();
|
|
counter.next();
|
|
counter.next();
|
|
assert_eq!(counter.get(), 3);
|
|
|
|
counter.reset(0);
|
|
assert_eq!(counter.get(), 0);
|
|
assert_eq!(counter.next(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_atomic_counter_add() {
|
|
let counter = AtomicCounter::new();
|
|
|
|
assert_eq!(counter.add(10), 0); // Returns old value
|
|
assert_eq!(counter.get(), 10);
|
|
|
|
assert_eq!(counter.add(5), 10); // Returns old value
|
|
assert_eq!(counter.get(), 15);
|
|
}
|
|
|
|
#[test]
|
|
fn test_atomic_counter_concurrent_increments() {
|
|
let counter = Arc::new(AtomicCounter::new());
|
|
let num_threads = 16;
|
|
let increments_per_thread = 10000;
|
|
|
|
let mut handles = Vec::new();
|
|
|
|
for _ in 0..num_threads {
|
|
let counter_clone: Arc<AtomicCounter> = Arc::clone(&counter);
|
|
let handle = thread::spawn(move || {
|
|
for _ in 0..increments_per_thread {
|
|
counter_clone.next();
|
|
}
|
|
});
|
|
handles.push(handle);
|
|
}
|
|
|
|
for handle in handles {
|
|
handle.join().expect("Thread failed");
|
|
}
|
|
|
|
// Final value should be num_threads * increments_per_thread
|
|
assert_eq!(counter.get(), (num_threads * increments_per_thread) as u64);
|
|
}
|
|
|
|
#[test]
|
|
fn test_atomic_counter_wraparound() {
|
|
let counter = AtomicCounter::new_with(u64::MAX - 5, 1);
|
|
|
|
assert_eq!(counter.next(), u64::MAX - 5);
|
|
assert_eq!(counter.next(), u64::MAX - 4);
|
|
assert_eq!(counter.next(), u64::MAX - 3);
|
|
assert_eq!(counter.next(), u64::MAX - 2);
|
|
assert_eq!(counter.next(), u64::MAX - 1);
|
|
assert_eq!(counter.next(), u64::MAX);
|
|
// Next increment will wrap around to 0
|
|
assert_eq!(counter.next(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mpsc_queue_debug_output() {
|
|
let queue = MPSCQueue::<u64>::new();
|
|
queue.push(1);
|
|
queue.push(2);
|
|
|
|
let debug_str = format!("{:?}", queue);
|
|
assert!(debug_str.contains("MPSCQueue"));
|
|
assert!(debug_str.contains("size"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_atomic_counter_debug_output() {
|
|
let counter = AtomicCounter::new_with(42, 5);
|
|
|
|
let debug_str = format!("{:?}", counter);
|
|
assert!(debug_str.contains("AtomicCounter"));
|
|
assert!(debug_str.contains("42"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_mpsc_queue_with_large_items() {
|
|
let queue = MPSCQueue::<Vec<u64>>::new();
|
|
|
|
// Push large vectors
|
|
for i in 0..100 {
|
|
let vec = vec![i; 1000]; // 1000 elements each
|
|
queue.push(vec);
|
|
}
|
|
|
|
// Pop and verify
|
|
let mut count = 0;
|
|
while let Some(vec) = queue.try_pop() {
|
|
assert_eq!(vec.len(), 1000);
|
|
assert_eq!(vec[0], count);
|
|
count += 1;
|
|
}
|
|
|
|
assert_eq!(count, 100);
|
|
}
|