#![allow( clippy::too_many_lines, clippy::cast_precision_loss, clippy::cast_sign_loss, clippy::missing_const_for_fn )] use std::{fs, time::Duration}; use tidaldb::{ replication::ShardId, wal::{ SignalEvent, WalConfig, WalHandle, format::{self, EventRecord, HEADER_SIZE}, reader, 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-13, AC-14: Crash recovery with torn write #[test] fn wal_crash_recovery_torn_write() { let dir = tempfile::tempdir().expect("tempdir creation should succeed"); let wal_dir = dir.path().join("wal"); fs::create_dir_all(&wal_dir).expect("create dir should succeed"); // Write valid batches directly to simulate a crash mid-write let events1: Vec = (1..=5) .map(|i| EventRecord::signal(i, 1, 1.0, i * 1_000_000_000)) .collect(); let events2: Vec = (6..=10) .map(|i| EventRecord::signal(i, 1, 1.0, i * 1_000_000_000)) .collect(); let batch1 = format::encode_batch(&events1, 1, 1_000_000_000).expect("encode should succeed"); let batch2 = format::encode_batch(&events2, 6, 6_000_000_000).expect("encode should succeed"); // Write batch1 fully, then truncate batch2 at various offsets for truncate_at in [ 0, 10, 32, 63, HEADER_SIZE, HEADER_SIZE + 5, HEADER_SIZE + 20, ] { let seg_name = segment::segment_filename(ShardId::SINGLE, 1); let seg_path = wal_dir.join(&seg_name); let mut data = batch1.clone(); if truncate_at > 0 { data.extend_from_slice(&batch2[..truncate_at.min(batch2.len())]); } fs::write(&seg_path, &data).expect("write should succeed"); let recovery = reader::recover(&wal_dir).expect("recovery should succeed"); assert_eq!( recovery.events.len(), 5, "torn write at offset {truncate_at}: should recover 5 events" ); // Clean up for next iteration fs::remove_file(&seg_path).expect("cleanup should succeed"); } } // -- AC-15: No phantom records (clean shutdown variant) #[test] fn wal_clean_shutdown_no_data_loss() { let dir = tempfile::tempdir().expect("tempdir creation should succeed"); let config = test_config(dir.path()); // Write 5 events let (handle, _, _) = WalHandle::open(config).expect("open should succeed"); for i in 1..=5 { handle.append(make_event(i)).expect("append should succeed"); } handle.shutdown().expect("shutdown should succeed"); // Verify exactly 5 events on replay let config = test_config(dir.path()); let (handle, replayed, _) = WalHandle::open(config).expect("reopen should succeed"); assert_eq!( replayed.len(), 5, "should replay exactly 5 events, not more" ); // No phantom events (events from un-fsynced batches should not appear) for event in &replayed { assert!( event.entity_id >= 1 && event.entity_id <= 5, "unexpected entity_id {}", event.entity_id ); } handle.shutdown().expect("shutdown should succeed"); } // -- AC-16: Crash at any byte position never produces corrupt state #[test] fn wal_crash_at_any_byte_position() { let dir = tempfile::tempdir().expect("tempdir creation should succeed"); let wal_dir = dir.path().join("wal"); fs::create_dir_all(&wal_dir).expect("create dir should succeed"); let events: Vec = (1..=3) .map(|i| EventRecord::signal(i, 1, 1.0, i * 1_000_000_000)) .collect(); let batch = format::encode_batch(&events, 1, 1_000_000_000).expect("encode should succeed"); // Test truncation at every byte offset for truncate_at in 0..=batch.len() { let seg_name = segment::segment_filename(ShardId::SINGLE, 1); let seg_path = wal_dir.join(&seg_name); fs::write(&seg_path, &batch[..truncate_at]).expect("write should succeed"); let recovery = reader::recover(&wal_dir).expect("recovery should never fail"); if truncate_at == batch.len() { assert_eq!( recovery.events.len(), 3, "full batch should recover 3 events" ); } else { assert_eq!( recovery.events.len(), 0, "truncated at byte {truncate_at}: no events should be recovered" ); } // Clean up for next iteration fs::remove_file(&seg_path).expect("cleanup should succeed"); } }