package wal import ( "bytes" "encoding/binary" "hash/crc32" "strings" "testing" ) func TestRecordRoundtrip(t *testing.T) { payload := []byte("hello world") encoded := EncodePhysicalRecord(RecFull, payload) rec, consumed, err := DecodePhysicalRecord(encoded) if err != nil { t.Fatalf("DecodePhysicalRecord failed: %v", err) } if rec.Type != RecFull { t.Errorf("expected type RecFull(%d), got %d", RecFull, rec.Type) } if string(rec.Payload) != "hello world" { t.Errorf("expected payload 'hello world', got %q", string(rec.Payload)) } if consumed != 7+len(payload) { t.Errorf("expected consumed %d, got %d", 7+len(payload), consumed) } } func TestSplitSmallPayload(t *testing.T) { payload := make([]byte, 100) for i := range payload { payload[i] = byte(i) } records := SplitIntoRecords(payload) if len(records) != 1 { t.Fatalf("expected 1 record, got %d", len(records)) } rec, _, err := DecodePhysicalRecord(records[0]) if err != nil { t.Fatalf("DecodePhysicalRecord failed: %v", err) } if rec.Type != RecFull { t.Errorf("expected RecFull, got %d", rec.Type) } if !bytes.Equal(rec.Payload, payload) { t.Error("payload mismatch") } } func TestSplitIntoRecords(t *testing.T) { // 40 KB payload → needs to split across blocks. payload := make([]byte, 40*1024) for i := range payload { payload[i] = byte(i % 256) } records := SplitIntoRecords(payload) if len(records) < 2 { t.Fatalf("expected at least 2 records, got %d", len(records)) } // Verify fragment sequence. types := make([]uint8, len(records)) var concatenated []byte for i, enc := range records { rec, _, err := DecodePhysicalRecord(enc) if err != nil { t.Fatalf("DecodePhysicalRecord record %d failed: %v", i, err) } types[i] = rec.Type concatenated = append(concatenated, rec.Payload...) } // First record must be RecFirst. if types[0] != RecFirst { t.Errorf("first record type: expected RecFirst(%d), got %d", RecFirst, types[0]) } // Last record must be RecLast. if types[len(types)-1] != RecLast { t.Errorf("last record type: expected RecLast(%d), got %d", RecLast, types[len(types)-1]) } // Middle records must be RecMiddle. for i := 1; i < len(types)-1; i++ { if types[i] != RecMiddle { t.Errorf("record %d type: expected RecMiddle(%d), got %d", i, RecMiddle, types[i]) } } // Concatenated payloads must equal original. if !bytes.Equal(concatenated, payload) { t.Error("concatenated payloads do not match original") } } func TestBlockPadding(t *testing.T) { // blockOffset = WalBlockSize - 5 → remaining = 5, which is <= 7 → padding needed = 5. blockOffset := uint32(WalBlockSize - 5) padding := PaddingNeeded(blockOffset) if padding != 5 { t.Errorf("PaddingNeeded(%d): expected 5, got %d", blockOffset, padding) } // Cannot fit a record. if CanFitRecord(blockOffset, 1) { t.Error("CanFitRecord should return false when remaining <= 7") } // blockOffset = WalBlockSize - 8 → remaining = 8, which is > 7 → no padding needed. blockOffset2 := uint32(WalBlockSize - 8) padding2 := PaddingNeeded(blockOffset2) if padding2 != 0 { t.Errorf("PaddingNeeded(%d): expected 0, got %d", blockOffset2, padding2) } // Can fit a 1-byte payload: remaining=8, header=7, payload=1 → 8 >= 8. if !CanFitRecord(blockOffset2, 1) { t.Error("CanFitRecord should return true when remaining=8 and payloadLen=1") } // Cannot fit a 2-byte payload: remaining=8, header=7, payload=2 → 8 < 9. if CanFitRecord(blockOffset2, 2) { t.Error("CanFitRecord should return false when remaining=8 and payloadLen=2") } } func TestCRCMismatch(t *testing.T) { encoded := EncodePhysicalRecord(RecFull, []byte("test")) // Corrupt a payload byte. encoded[8] ^= 0xFF _, _, err := DecodePhysicalRecord(encoded) if err == nil { t.Error("expected CRC mismatch error") } } func TestDataTooShort(t *testing.T) { _, _, err := DecodePhysicalRecord([]byte{1, 2, 3}) if err == nil { t.Error("expected error for data too short") } } // -------- H1+H2 regression guards -------- func TestDecodePhysicalRecord_RejectZeroLength(t *testing.T) { buf := make([]byte, PhysicalRecordHeaderSize) binary.LittleEndian.PutUint16(buf[4:6], 0) buf[6] = RecFull crc := crc32.Checksum(buf[4:7], crc32cTable) binary.LittleEndian.PutUint32(buf[0:4], crc) _, _, err := DecodePhysicalRecord(buf) if err == nil { t.Fatal("expected error for length=0") } if !strings.Contains(err.Error(), "length") { t.Errorf("error should mention length, got: %v", err) } } func TestDecodePhysicalRecord_RejectInvalidType(t *testing.T) { payload := []byte("test") buf := make([]byte, PhysicalRecordHeaderSize+len(payload)) binary.LittleEndian.PutUint16(buf[4:6], uint16(len(payload))) buf[6] = RecInvalid copy(buf[7:], payload) crc := crc32.Checksum(buf[4:], crc32cTable) binary.LittleEndian.PutUint32(buf[0:4], crc) _, _, err := DecodePhysicalRecord(buf) if err == nil { t.Fatal("expected error for type=Invalid(0)") } if !strings.Contains(err.Error(), "type") { t.Errorf("error should mention type, got: %v", err) } } func TestDecodePhysicalRecord_RejectUnknownType(t *testing.T) { payload := []byte("test") buf := make([]byte, PhysicalRecordHeaderSize+len(payload)) binary.LittleEndian.PutUint16(buf[4:6], uint16(len(payload))) buf[6] = RecLast + 1 copy(buf[7:], payload) crc := crc32.Checksum(buf[4:], crc32cTable) binary.LittleEndian.PutUint32(buf[0:4], crc) _, _, err := DecodePhysicalRecord(buf) if err == nil { t.Fatal("expected error for type > RecLast") } if !strings.Contains(err.Error(), "type") { t.Errorf("error should mention type, got: %v", err) } } func TestDecodePhysicalRecord_ValidRecordsUnaffected(t *testing.T) { for _, recType := range []uint8{RecFull, RecFirst, RecMiddle, RecLast} { payload := []byte("test-payload") encoded := EncodePhysicalRecord(recType, payload) rec, consumed, err := DecodePhysicalRecord(encoded) if err != nil { t.Errorf("type %d: %v", recType, err) } if rec.Type != recType { t.Errorf("Type = %d, want %d", rec.Type, recType) } if consumed != len(encoded) { t.Errorf("consumed = %d, want %d", consumed, len(encoded)) } } } // Oracle nice-to-have: integration test verifying ParseBlock wraps H1/H2 // errors as TailCorruptionError. func TestParseBlockWrapsInvalidRecordAsTailCorruption(t *testing.T) { // Build a block containing a valid record followed by a length=0 record // with non-zero header (so ParseBlock doesn't treat it as zero padding). validRec := EncodePhysicalRecord(RecFull, []byte("valid")) invalidRec := make([]byte, PhysicalRecordHeaderSize) binary.LittleEndian.PutUint16(invalidRec[4:6], 0) // length=0 invalidRec[6] = RecFull // non-zero type crc := crc32.Checksum(invalidRec[4:7], crc32cTable) binary.LittleEndian.PutUint32(invalidRec[0:4], crc) block := append(validRec, invalidRec...) _, err := ParseBlock(block) if err == nil { t.Fatal("expected TailCorruptionError for invalid record in block") } if !IsTailCorruption(err) { t.Errorf("expected TailCorruptionError, got: %v", err) } }