Files
seaweedfs/weed/format/layout_test.go
Chris Lu 4c7e5afbfe format: bound the encoded adapter name symmetrically
DecodeLayout rejected names over 256 bytes while Encode accepted them,
so an oversized name encoded fine and then failed every decode. Enforce
the bound in Validate, shared by both directions.
2026-08-10 00:59:38 -07:00

193 lines
6.4 KiB
Go

package format
import (
"strings"
"testing"
)
func TestLayoutEncodeDecodeRoundTrip(t *testing.T) {
layout := &Layout{
Format: "hls-ts",
ExtentSizes: []int64{188 * 3, 188 * 2, 188 * 7},
Align: 188,
Payload: []byte{1, 2, 3},
}
encoded, err := layout.Encode()
if err != nil {
t.Fatalf("Encode() error = %v", err)
}
decoded, err := DecodeLayout(encoded)
if err != nil {
t.Fatalf("DecodeLayout() error = %v", err)
}
if decoded.Format != layout.Format || decoded.Align != layout.Align {
t.Fatalf("decoded = %+v, want %+v", decoded, layout)
}
if len(decoded.ExtentSizes) != len(layout.ExtentSizes) {
t.Fatalf("extent count = %d, want %d", len(decoded.ExtentSizes), len(layout.ExtentSizes))
}
for i := range layout.ExtentSizes {
if decoded.ExtentSizes[i] != layout.ExtentSizes[i] {
t.Fatalf("extent %d = %d, want %d", i, decoded.ExtentSizes[i], layout.ExtentSizes[i])
}
}
if string(decoded.Payload) != string(layout.Payload) {
t.Fatalf("payload = %v, want %v", decoded.Payload, layout.Payload)
}
}
func TestDecodeLayoutRejectsCorruptInput(t *testing.T) {
layout := &Layout{Format: "parquet", ExtentSizes: []int64{10, 20}, Align: 1}
encoded, err := layout.Encode()
if err != nil {
t.Fatalf("Encode() error = %v", err)
}
for cut := 0; cut < len(encoded); cut++ {
if _, err := DecodeLayout(encoded[:cut]); err == nil {
t.Fatalf("DecodeLayout() accepted truncation at %d", cut)
}
}
if _, err := DecodeLayout(append(append([]byte{}, encoded...), 0)); err == nil {
t.Fatalf("DecodeLayout() accepted trailing bytes")
}
}
func TestLayoutValidate(t *testing.T) {
tests := []struct {
name string
layout Layout
fileSize int64
wantErr string
}{
{"valid", Layout{Format: "x", ExtentSizes: []int64{5, 5}, Align: 1}, 10, ""},
{"skip size check", Layout{Format: "x", ExtentSizes: []int64{5}, Align: 1}, -1, ""},
{"wrong total", Layout{Format: "x", ExtentSizes: []int64{5, 5}, Align: 1}, 11, "but the file has"},
{"zero extent", Layout{Format: "x", ExtentSizes: []int64{5, 0}, Align: 1}, -1, "invalid size"},
{"no extents", Layout{Format: "x", Align: 1}, -1, "no extents"},
{"bad align", Layout{Format: "x", ExtentSizes: []int64{5}, Align: 0}, -1, "align"},
{"no name", Layout{ExtentSizes: []int64{5}, Align: 1}, -1, "format name"},
{"name too long", Layout{Format: strings.Repeat("x", MaxFormatNameBytes+1), ExtentSizes: []int64{5}, Align: 1}, -1, "too long"},
}
for _, test := range tests {
err := test.layout.Validate(test.fileSize)
if test.wantErr == "" {
if err != nil {
t.Fatalf("%s: Validate() error = %v", test.name, err)
}
continue
}
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("%s: Validate() error = %v, want %q", test.name, err, test.wantErr)
}
}
}
func TestExtentRange(t *testing.T) {
layout := &Layout{Format: "x", ExtentSizes: []int64{10, 20, 30}, Align: 1}
offset, size, ok := layout.ExtentRange(1)
if !ok || offset != 10 || size != 20 {
t.Fatalf("ExtentRange(1) = (%d, %d, %v), want (10, 20, true)", offset, size, ok)
}
if _, _, ok := layout.ExtentRange(3); ok {
t.Fatalf("ExtentRange(3) accepted out-of-range index")
}
if _, _, ok := layout.ExtentRange(-1); ok {
t.Fatalf("ExtentRange(-1) accepted negative index")
}
}
// collectChunks walks the cutter the way the upload loop does.
func collectChunks(t *testing.T, cutter *Cutter) [][2]int64 {
t.Helper()
var chunks [][2]int64
var offset int64
for {
size := cutter.NextChunkSize(offset)
if size <= 0 {
return chunks
}
chunks = append(chunks, [2]int64{offset, size})
offset += size
}
}
func TestCutterKeepsExtentBoundaries(t *testing.T) {
layout := &Layout{Format: "x", ExtentSizes: []int64{5, 4}, Align: 1}
chunks := collectChunks(t, layout.Cutter(16))
want := [][2]int64{{0, 5}, {5, 4}}
if len(chunks) != len(want) {
t.Fatalf("chunks = %v, want %v", chunks, want)
}
for i := range want {
if chunks[i] != want[i] {
t.Fatalf("chunk %d = %v, want %v", i, chunks[i], want[i])
}
}
}
func TestCutterSplitsOversizedExtentsOnAlign(t *testing.T) {
// maxChunkSize 5 with align 2 quantizes down to 4-byte interior cuts.
layout := &Layout{Format: "x", ExtentSizes: []int64{10, 3}, Align: 2}
chunks := collectChunks(t, layout.Cutter(5))
want := [][2]int64{{0, 4}, {4, 4}, {8, 2}, {10, 3}}
if len(chunks) != len(want) {
t.Fatalf("chunks = %v, want %v", chunks, want)
}
for i := range want {
if chunks[i] != want[i] {
t.Fatalf("chunk %d = %v, want %v", i, chunks[i], want[i])
}
}
}
func TestCutterAlignLargerThanChunkLimit(t *testing.T) {
// Align above maxChunkSize still cuts on whole atoms.
layout := &Layout{Format: "x", ExtentSizes: []int64{20}, Align: 8}
chunks := collectChunks(t, layout.Cutter(5))
want := [][2]int64{{0, 8}, {8, 8}, {16, 4}}
if len(chunks) != len(want) {
t.Fatalf("chunks = %v, want %v", chunks, want)
}
for i := range want {
if chunks[i] != want[i] {
t.Fatalf("chunk %d = %v, want %v", i, chunks[i], want[i])
}
}
}
func TestCutterUnlimitedKeepsOneChunkPerExtent(t *testing.T) {
layout := &Layout{Format: "x", ExtentSizes: []int64{10, 3}, Align: 188}
chunks := collectChunks(t, layout.Cutter(0))
want := [][2]int64{{0, 10}, {10, 3}}
if len(chunks) != len(want) {
t.Fatalf("chunks = %v, want %v", chunks, want)
}
}
// A hostile layout may declare an enormous extent; the cutter must stay O(1)
// per query instead of materializing every interior cut.
func TestCutterHugeExtentStaysLazy(t *testing.T) {
const quantum = 4 << 20 // 4MiB, already a multiple of align 1
layout := &Layout{Format: "x", ExtentSizes: []int64{1 << 50, 188}, Align: 188}
cutter := layout.Cutter(quantum)
alignedQuantum := int64(quantum - quantum%188)
if got := cutter.NextChunkSize(0); got != alignedQuantum {
t.Fatalf("NextChunkSize(0) = %d, want %d", got, alignedQuantum)
}
if got := cutter.NextChunkSize(alignedQuantum * 1000); got != alignedQuantum {
t.Fatalf("mid-extent chunk = %d, want %d", got, alignedQuantum)
}
// the final interior chunk stops at the extent boundary
last := (int64(1<<50) / alignedQuantum) * alignedQuantum
if got := cutter.NextChunkSize(last); got != int64(1<<50)-last {
t.Fatalf("tail chunk = %d, want %d", got, int64(1<<50)-last)
}
// the next extent still cuts independently
if got := cutter.NextChunkSize(1 << 50); got != 188 {
t.Fatalf("second extent chunk = %d, want 188", got)
}
if got := cutter.NextChunkSize(1<<50 + 188); got != 0 {
t.Fatalf("past end = %d, want 0", got)
}
}