package wal import ( "bytes" "encoding/binary" "hash/crc32" "testing" ) func TestBlockWriterSingleRecord(t *testing.T) { bw := NewBlockWriter() var buf bytes.Buffer payload := []byte("hello world") if err := bw.WriteRecord(RecFull, payload, &buf); err != nil { t.Fatalf("WriteRecord: %v", err) } // Record should still be buffered (block not full). if buf.Len() != 0 { t.Fatalf("expected no flush yet, got %d bytes", buf.Len()) } // Flush to get the data. if err := bw.Flush(&buf); err != nil { t.Fatalf("Flush: %v", err) } written := buf.Bytes() // Verify the record is at the start of a full block. if len(written) != WalBlockSize { t.Fatalf("expected full block %d bytes, got %d", WalBlockSize, len(written)) } // Decode and verify the physical record. rec, consumed, err := DecodePhysicalRecord(written) if err != nil { t.Fatalf("DecodePhysicalRecord: %v", err) } if rec.Type != RecFull { t.Errorf("type = %d, want RecFull(%d)", rec.Type, RecFull) } if string(rec.Payload) != "hello world" { t.Errorf("payload = %q, want %q", rec.Payload, "hello world") } // Remaining bytes after the record should be zero padding. recEnd := consumed for i := recEnd; i < WalBlockSize; i++ { if written[i] != 0 { t.Errorf("padding byte [%d] = %d, want 0", i, written[i]) } } } func TestBlockWriterPadding(t *testing.T) { bw := NewBlockWriter() var buf bytes.Buffer payloadLen := WalBlockSize - PhysicalRecordHeaderSize payload := make([]byte, payloadLen) for i := range payload { payload[i] = byte(i % 256) } if err := bw.WriteRecord(RecFull, payload, &buf); err != nil { t.Fatalf("WriteRecord: %v", err) } if buf.Len() != WalBlockSize { t.Fatalf("expected auto-flush of full block (%d bytes), got %d", WalBlockSize, buf.Len()) } if bw.BlockOffset() != 0 { t.Errorf("BlockOffset = %d, want 0 after full block write", bw.BlockOffset()) } buf.Reset() smallPayload := []byte("next") if err := bw.WriteRecord(RecFull, smallPayload, &buf); err != nil { t.Fatalf("WriteRecord after full block: %v", err) } if buf.Len() != 0 { t.Fatalf("expected no flush for partial block, got %d bytes", buf.Len()) } } func TestBlockWriterPaddingNeeded(t *testing.T) { tests := []struct { name string offset uint32 want int }{ {"beginning", 0, 0}, {"mid_block", 100, 0}, {"7_remaining", WalBlockSize - 7, 7}, {"6_remaining", WalBlockSize - 6, 6}, {"1_remaining", WalBlockSize - 1, 1}, {"full_block", WalBlockSize, 0}, // would be reset } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { bw := &BlockWriter{offset: tt.offset} got := bw.paddingNeeded() if got != tt.want { t.Errorf("paddingNeeded() = %d, want %d", got, tt.want) } }) } } func TestBlockWriterCrossBlock(t *testing.T) { bw := NewBlockWriter() var buf bytes.Buffer payloadLen := WalBlockSize - PhysicalRecordHeaderSize - 5 payload1 := make([]byte, payloadLen) for i := range payload1 { payload1[i] = byte('A' + i%26) } if err := bw.WriteRecord(RecFull, payload1, &buf); err != nil { t.Fatalf("WriteRecord payload1: %v", err) } if buf.Len() != 0 { t.Fatalf("expected no flush after first record, got %d bytes", buf.Len()) } payload2 := []byte("second") if err := bw.WriteRecord(RecFull, payload2, &buf); err != nil { t.Fatalf("WriteRecord payload2: %v", err) } if buf.Len() != WalBlockSize { t.Fatalf("expected %d bytes flushed, got %d", WalBlockSize, buf.Len()) } firstBlock := buf.Bytes()[:WalBlockSize] rec, _, err := DecodePhysicalRecord(firstBlock) if err != nil { t.Fatalf("DecodePhysicalRecord block 1: %v", err) } if rec.Type != RecFull { t.Errorf("type = %d, want RecFull", rec.Type) } if len(rec.Payload) != payloadLen { t.Errorf("payload len = %d, want %d", len(rec.Payload), payloadLen) } for i := WalBlockSize - 5; i < WalBlockSize; i++ { if firstBlock[i] != 0 { t.Errorf("padding byte [%d] = %d, want 0", i, firstBlock[i]) } } if err := bw.Flush(&buf); err != nil { t.Fatalf("Flush: %v", err) } secondBlock := buf.Bytes()[WalBlockSize:] if len(secondBlock) != WalBlockSize { t.Fatalf("second block: expected %d bytes, got %d", WalBlockSize, len(secondBlock)) } rec2, _, err := DecodePhysicalRecord(secondBlock) if err != nil { t.Fatalf("DecodePhysicalRecord block 2: %v", err) } if string(rec2.Payload) != "second" { t.Errorf("payload2 = %q, want %q", rec2.Payload, "second") } } func TestBlockWriterRecordTooLarge(t *testing.T) { bw := NewBlockWriter() var buf bytes.Buffer // Payload that exceeds block capacity. payload := make([]byte, WalBlockSize) err := bw.WriteRecord(RecFull, payload, &buf) if err == nil { t.Fatal("expected error for oversized record") } } func TestBlockWriterMultipleRecords(t *testing.T) { bw := NewBlockWriter() var buf bytes.Buffer // Write several small records. records := []struct { recType uint8 payload []byte }{ {RecFirst, []byte("part1")}, {RecMiddle, []byte("part2")}, {RecLast, []byte("part3")}, } for _, r := range records { if err := bw.WriteRecord(r.recType, r.payload, &buf); err != nil { t.Fatalf("WriteRecord(%d, %q): %v", r.recType, r.payload, err) } } if err := bw.Flush(&buf); err != nil { t.Fatalf("Flush: %v", err) } data := buf.Bytes() // Decode all three records from the block. offset := 0 for i, expected := range records { rec, consumed, err := DecodePhysicalRecord(data[offset:]) if err != nil { t.Fatalf("record %d: DecodePhysicalRecord at offset %d: %v", i, offset, err) } if rec.Type != expected.recType { t.Errorf("record %d: type = %d, want %d", i, rec.Type, expected.recType) } if string(rec.Payload) != string(expected.payload) { t.Errorf("record %d: payload = %q, want %q", i, rec.Payload, expected.payload) } offset += consumed } } func TestBlockWriterReset(t *testing.T) { bw := NewBlockWriter() var buf bytes.Buffer if err := bw.WriteRecord(RecFull, []byte("data"), &buf); err != nil { t.Fatalf("WriteRecord: %v", err) } if bw.BlockOffset() == 0 { t.Fatal("expected non-zero offset after write") } bw.Reset() if bw.BlockOffset() != 0 { t.Errorf("BlockOffset after Reset = %d, want 0", bw.BlockOffset()) } } func TestBlockWriterFlushEmptyBlock(t *testing.T) { bw := NewBlockWriter() var buf bytes.Buffer // Flushing an empty block should be a no-op. if err := bw.Flush(&buf); err != nil { t.Fatalf("Flush empty: %v", err) } if buf.Len() != 0 { t.Errorf("expected 0 bytes, got %d", buf.Len()) } } func TestBlockWriterOffsetTracking(t *testing.T) { bw := NewBlockWriter() var buf bytes.Buffer // Write a small record and verify offset. payload := []byte("track-me") if err := bw.WriteRecord(RecFull, payload, &buf); err != nil { t.Fatalf("WriteRecord: %v", err) } expectedOffset := uint32(PhysicalRecordHeaderSize + len(payload)) if bw.BlockOffset() != expectedOffset { t.Errorf("BlockOffset = %d, want %d", bw.BlockOffset(), expectedOffset) } // Flush should write exactly one full block. if err := bw.Flush(&buf); err != nil { t.Fatalf("Flush: %v", err) } if buf.Len() != WalBlockSize { t.Errorf("flushed %d bytes, want %d", buf.Len(), WalBlockSize) } } func TestBlockWriterAutoFlushFullBlock(t *testing.T) { bw := NewBlockWriter() var buf bytes.Buffer // Fill the block exactly. payloadLen := WalBlockSize - PhysicalRecordHeaderSize payload := make([]byte, payloadLen) for i := range payload { payload[i] = byte(i) } if err := bw.WriteRecord(RecFull, payload, &buf); err != nil { t.Fatalf("WriteRecord exact fill: %v", err) } // Block should have been auto-flushed. if buf.Len() != WalBlockSize { t.Errorf("expected auto-flush of %d bytes, got %d", WalBlockSize, buf.Len()) } if bw.BlockOffset() != 0 { t.Errorf("BlockOffset after auto-flush = %d, want 0", bw.BlockOffset()) } // Verify CRC is correct by decoding. data := buf.Bytes() rec, _, err := DecodePhysicalRecord(data) if err != nil { t.Fatalf("DecodePhysicalRecord: %v", err) } if len(rec.Payload) != payloadLen { t.Errorf("payload len = %d, want %d", len(rec.Payload), payloadLen) } } func TestBlockWriterPhysicalRecordCRC(t *testing.T) { bw := NewBlockWriter() var buf bytes.Buffer payload := []byte("crc-check") if err := bw.WriteRecord(RecFull, payload, &buf); err != nil { t.Fatalf("WriteRecord: %v", err) } if err := bw.Flush(&buf); err != nil { t.Fatalf("Flush: %v", err) } data := buf.Bytes() // Manually verify CRC: covers length + type + payload. storedCRC := binary.LittleEndian.Uint32(data[0:4]) length := binary.LittleEndian.Uint16(data[4:6]) recType := data[6] if recType != RecFull { t.Errorf("type = %d, want RecFull", recType) } if int(length) != len(payload) { t.Errorf("length = %d, want %d", length, len(payload)) } // Verify CRC over [length, type, payload]. crcData := data[4 : 7+length] computedCRC := crc32.ChecksumIEEE(crcData) if storedCRC != computedCRC { t.Errorf("CRC mismatch: stored %d, computed %d", storedCRC, computedCRC) } }