aedb 0.2.9

Embedded Rust storage engine with transactional commits, WAL durability, and snapshot-consistent reads
Documentation
use super::{FrameError, FrameReader, FrameWriter, PAYLOAD_TYPE_SIZE_BYTES, U64_SIZE_BYTES};
use std::io::Cursor;

const FRAME_LENGTH_SIZE_BYTES: usize = 4;

#[test]
fn frame_happy_path_reads_what_was_written() {
    let mut writer = FrameWriter::new(Vec::<u8>::new());
    for i in 1..=1000 {
        writer
            .append(i, 1000 + i, 0x01, format!("payload-{i}").as_bytes())
            .expect("append");
    }
    let bytes = writer.into_inner();

    let mut reader = FrameReader::new(Cursor::new(bytes));
    for i in 1..=1000 {
        let frame = reader.next_frame().expect("next").expect("frame");
        assert_eq!(frame.commit_seq, i);
        assert_eq!(frame.timestamp_micros, 1000 + i);
        assert_eq!(frame.payload_type, 0x01);
        assert_eq!(frame.payload, format!("payload-{i}").as_bytes());
    }
    assert!(reader.next_frame().expect("final next").is_none());
}

#[test]
fn frame_corruption_detected() {
    let mut writer = FrameWriter::new(Vec::<u8>::new());
    for i in 1..=10 {
        writer
            .append(i, i, 0x01, format!("payload-{i}").as_bytes())
            .expect("append");
    }
    let mut bytes = writer.into_inner();
    let mut frame_offset_bytes = 0usize;
    let frame_count = 10usize;
    for frame_index in 1..=frame_count {
        assert!(frame_index <= frame_count);
        let frame_body_size_bytes = u32::from_be_bytes(
            bytes[frame_offset_bytes..frame_offset_bytes + FRAME_LENGTH_SIZE_BYTES]
                .try_into()
                .expect("frame size bytes"),
        ) as usize;
        let frame_size_bytes = FRAME_LENGTH_SIZE_BYTES + frame_body_size_bytes;
        assert!(frame_offset_bytes + frame_size_bytes <= bytes.len());
        if frame_index == 5 {
            let payload_offset_bytes = frame_offset_bytes
                + FRAME_LENGTH_SIZE_BYTES
                + U64_SIZE_BYTES
                + U64_SIZE_BYTES
                + PAYLOAD_TYPE_SIZE_BYTES;
            bytes[payload_offset_bytes] ^= 0xFF;
            break;
        }
        frame_offset_bytes += frame_size_bytes;
    }

    let mut reader = FrameReader::new(Cursor::new(bytes));
    for _ in 0..4 {
        let _ = reader
            .next_frame()
            .expect("valid frame")
            .expect("some frame");
    }
    assert_eq!(
        reader.next_frame().expect_err("must be corruption"),
        FrameError::Corruption
    );
}

#[test]
fn frame_truncation_detected() {
    let mut writer = FrameWriter::new(Vec::<u8>::new());
    for i in 1..=10 {
        writer.append(i, i, 0x01, &[1, 2, 3, 4, 5]).expect("append");
    }
    let bytes = writer.into_inner();

    for cut in 1..20 {
        let truncated = &bytes[..bytes.len() - cut];
        let mut reader = FrameReader::new(Cursor::new(truncated));
        loop {
            match reader.next_frame() {
                Ok(Some(_)) => {}
                Ok(None) => break,
                Err(FrameError::Truncation) => break,
                Err(e) => panic!("unexpected error: {e:?}"),
            }
        }
    }
}

#[test]
fn empty_file_returns_none() {
    let mut reader = FrameReader::new(Cursor::new(Vec::<u8>::new()));
    assert!(reader.next_frame().expect("next").is_none());
}

#[test]
fn partial_length_returns_truncation() {
    let mut reader = FrameReader::new(Cursor::new(vec![0x00, 0x00]));
    assert_eq!(
        reader.next_frame().expect_err("truncation"),
        FrameError::Truncation
    );
}

#[test]
fn oversized_frame_length_is_rejected_without_allocation() {
    let mut bytes = Vec::new();
    let oversized = (super::MAX_FRAME_BODY_BYTES as u32).saturating_add(1);
    bytes.extend_from_slice(&oversized.to_be_bytes());
    let mut reader = FrameReader::new(Cursor::new(bytes));
    assert_eq!(
        reader.next_frame().expect_err("oversized frame"),
        FrameError::Corruption
    );
}