#![allow( clippy::too_many_lines, clippy::cast_precision_loss, clippy::cast_sign_loss, clippy::missing_const_for_fn )] use std::{sync::Arc, time::Duration}; use tidaldb::{ replication::ShardId, wal::{SignalEvent, WalConfig, WalHandle, segment}, }; fn test_config(dir: &std::path::Path) -> WalConfig { WalConfig { dir: dir.to_path_buf(), segment_size: 16 * 1024 * 1024, batch_size: 100, batch_timeout: Duration::from_millis(1), dedup_window: Duration::from_secs(30), } } fn make_event(id: u64) -> SignalEvent { SignalEvent { entity_id: id, signal_type: 1, weight: 1.0, timestamp_nanos: id * 1_000_000_000, } } // -- AC-1, AC-2: Wire format byte-level tests are in format.rs unit tests. // These integration tests validate the full pipeline. #[test] fn wal_basic_round_trip() { let dir = tempfile::tempdir().expect("tempdir creation should succeed"); let config = test_config(dir.path()); // Write events let (handle, replayed, _) = WalHandle::open(config).expect("open should succeed"); assert!(replayed.is_empty()); for i in 1..=10 { handle.append(make_event(i)).expect("append should succeed"); } handle.shutdown().expect("shutdown should succeed"); // Reopen and verify replay let config = test_config(dir.path()); let (handle, replayed, _) = WalHandle::open(config).expect("reopen should succeed"); assert_eq!(replayed.len(), 10); for (i, event) in replayed.iter().enumerate() { assert_eq!(event.entity_id, (i + 1) as u64); assert_eq!(event.signal_type, 1); assert_eq!(event.weight.to_bits(), 1.0_f32.to_bits()); } handle.shutdown().expect("shutdown should succeed"); } // -- AC-10, AC-11: Deduplication #[test] fn wal_dedup_silent() { let dir = tempfile::tempdir().expect("tempdir creation should succeed"); let config = test_config(dir.path()); let (handle, _, _) = WalHandle::open(config).expect("open should succeed"); let event = make_event(42); let seq1 = handle .append(event.clone()) .expect("first append should succeed"); let seq2 = handle .append(event.clone()) .expect("second append should succeed"); let seq3 = handle.append(event).expect("third append should succeed"); assert!(seq1 > 0, "first event should get real sequence number"); assert_eq!(seq2, 0, "duplicate should return seq=0"); assert_eq!(seq3, 0, "duplicate should return seq=0"); handle.shutdown().expect("shutdown should succeed"); // Verify only one event on disk let config = test_config(dir.path()); let (handle, replayed, _) = WalHandle::open(config).expect("reopen should succeed"); assert_eq!(replayed.len(), 1, "only one unique event should be on disk"); handle.shutdown().expect("shutdown should succeed"); } // -- AC-12: No false positives #[test] fn wal_dedup_no_false_positives() { let dir = tempfile::tempdir().expect("tempdir creation should succeed"); // Use a large batch size so batches fill quickly from concurrent writers. let config = WalConfig { dir: dir.path().to_path_buf(), segment_size: 16 * 1024 * 1024, batch_size: 256, batch_timeout: Duration::from_millis(1), dedup_window: Duration::from_secs(60), }; let (handle, _, _) = WalHandle::open(config).expect("open should succeed"); let handle = Arc::new(handle); let total_events: u64 = 1_000; let num_threads = 10u64; let per_thread = total_events / num_threads; let mut threads = Vec::new(); for t in 0..num_threads { let handle = Arc::clone(&handle); threads.push(std::thread::spawn(move || { let mut count = 0u64; for i in 0..per_thread { let entity_id = t * per_thread + i; let event = SignalEvent { entity_id, signal_type: (entity_id % 256) as u8, weight: entity_id as f32, timestamp_nanos: entity_id * 1_000_000, }; let seq = handle.append(event).expect("append should succeed"); if seq > 0 { count += 1; } } count })); } let mut real_seqs = 0u64; for thread in threads { real_seqs += thread.join().expect("thread should join"); } let handle = Arc::try_unwrap(handle).expect("should be sole owner of WalHandle Arc"); handle.shutdown().expect("shutdown should succeed"); assert_eq!( real_seqs, total_events, "all {total_events} unique events must be accepted (no false positives)" ); } // -- AC-5, AC-6: Segment rotation #[test] fn wal_segment_rotation() { let dir = tempfile::tempdir().expect("tempdir creation should succeed"); // Use very small segment size to force rotation let config = WalConfig { dir: dir.path().to_path_buf(), segment_size: 256, // tiny: one batch exceeds this batch_size: 10, batch_timeout: Duration::from_millis(10), dedup_window: Duration::from_secs(30), }; let (handle, _, _) = WalHandle::open(config).expect("open should succeed"); // Write enough events to trigger multiple rotations for i in 1..=100 { handle.append(make_event(i)).expect("append should succeed"); } handle.shutdown().expect("shutdown should succeed"); // Check segment files exist let wal_dir = dir.path().join("wal"); let segments = segment::list_segments(&wal_dir).expect("list should succeed"); assert!( segments.len() > 1, "expected multiple segments, got {}", segments.len() ); // Verify segment naming: all should match wal-{seq:020}.seg pattern for (seq, path) in &segments { let filename = path .file_name() .expect("should have filename") .to_str() .expect("should be valid UTF-8"); assert_eq!( filename, segment::segment_filename(ShardId::SINGLE, *seq), "segment filename mismatch" ); } // Verify replay gets all events let config = WalConfig { dir: dir.path().to_path_buf(), segment_size: 256, batch_size: 10, batch_timeout: Duration::from_millis(10), dedup_window: Duration::from_secs(30), }; let (handle, replayed, _) = WalHandle::open(config).expect("reopen should succeed"); assert_eq!(replayed.len(), 100, "all events should be replayed"); handle.shutdown().expect("shutdown should succeed"); }