Files
tensor/io/hdf5_continuation_pins_test.go
petrbalvin af4ee19703
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s
feat: initial release
Assisted-by: GLM 5.3 Flash
2026-09-03 10:00:00 +02:00

162 lines
5.6 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package io
import (
"encoding/binary"
"math"
"strings"
"testing"
)
// Regression pins: HDF5 headers the reference library writes
// once they outgrow their first block, byte orders and unallocated
// storage the reader used to answer silently, and the CSV byte order
// mark every spreadsheet writes.
// h5Link renders a continuation link message body: the block address
// and its length, eight bytes each in the hostile layout.
func h5Link(addr, length uint64) []byte {
b := make([]byte, 16)
binary.LittleEndian.PutUint64(b, addr)
binary.LittleEndian.PutUint64(b[8:], length)
return b
}
// h5ContinuationBlock writes a flat message-list block at off and
// returns the end offset, everything eight-aligned.
func h5ContinuationBlock(f []byte, off int, msgs ...h5Msg) int {
for _, m := range msgs {
binary.LittleEndian.PutUint16(f[off:], m.typ)
binary.LittleEndian.PutUint16(f[off+2:], uint16(len(m.body)))
copy(f[off+8:], m.body)
off += alignUp(8+len(m.body), 8)
}
return off
}
// TestLoadHDF5ChainedContinuation: the header walk followed exactly
// one continuation block and dropped every message of the second and
// later ones, so a legal attribute-rich file lost its datasets. Here
// the dataspace sits in the header, the datatype in the first
// continuation block and the layout in the second: only a walk that
// follows the whole chain can assemble the dataset.
func TestLoadHDF5ChainedContinuation(t *testing.T) {
const blockA, blockB, dataAt, n = 448, 640, 800, 1024
f := h5HostileFile(n)
end := h5ObjectHeader(f, 96,
h5Msg{hdf5MsgDataspace, h5Dataspace(2)},
h5Msg{hdf5MsgContinuation, h5Link(blockA, 112)},
)
if end > blockA {
t.Fatalf("the header runs to %d, past the first block at %d", end, blockA)
}
endA := h5ContinuationBlock(f, blockA,
h5Msg{hdf5MsgDatatype, h5FloatType(8)},
h5Msg{hdf5MsgContinuation, h5Link(blockB, 88)},
)
if endA > blockB {
t.Fatalf("block A runs to %d, past block B at %d", endA, blockB)
}
h5ContinuationBlock(f, blockB,
h5Msg{hdf5MsgDataLayout, h5ContiguousLayout(dataAt, 16)},
)
binary.LittleEndian.PutUint64(f[dataAt:], math.Float64bits(1.5))
binary.LittleEndian.PutUint64(f[dataAt+8:], math.Float64bits(-2.5))
sets, err := LoadHDF5(writeHostile(t, "chained.h5", f))
if err != nil {
t.Fatalf("LoadHDF5: %v", err)
}
if len(sets) != 1 {
t.Fatalf("datasets = %d, want 1", len(sets))
}
v0 := sets[0].Values.FloatAt(0)
if v0 != 1.5 || sets[0].Values.FloatAt(1) != -2.5 {
t.Fatalf("values = %v, %v, want 1.5 and -2.5", v0, sets[0].Values.FloatAt(1))
}
}
// TestLoadHDF5ContinuationCycle: a continuation block listing itself
// must be an error, not an infinite walk.
func TestLoadHDF5ContinuationCycle(t *testing.T) {
const blockA, n = 448, 640
f := h5HostileFile(n)
h5ObjectHeader(f, 96,
h5Msg{hdf5MsgDataspace, h5Dataspace(2)},
h5Msg{hdf5MsgContinuation, h5Link(blockA, 40)},
)
// The block's only entry is a link back to itself.
h5ContinuationBlock(f, blockA,
h5Msg{hdf5MsgContinuation, h5Link(blockA, 40)},
)
if _, err := LoadHDF5(writeHostile(t, "cycle.h5", f)); err == nil || !strings.Contains(err.Error(), "twice") {
t.Fatalf("a self-referencing continuation block: err = %v", err)
}
}
// TestLoadHDF5BigEndianRefusal: the byte-order bit of the datatype was
// parsed and never checked, so a big-endian dataset decoded as
// byte-swapped noise with no error.
func TestLoadHDF5BigEndianRefusal(t *testing.T) {
const dataAt, n = 448, 512
f := h5HostileFile(n)
beType := h5FloatType(8)
beType[1] = 0x01 // class bit field: bit 0 set means big-endian
h5ObjectHeader(f, 96,
h5Msg{hdf5MsgDataspace, h5Dataspace(2)},
h5Msg{hdf5MsgDatatype, beType},
h5Msg{hdf5MsgDataLayout, h5ContiguousLayout(dataAt, 16)},
)
if _, err := LoadHDF5(writeHostile(t, "bigendian.h5", f)); err == nil || !strings.Contains(err.Error(), "big-endian") {
t.Fatalf("a big-endian dataset: err = %v", err)
}
}
// TestLoadHDF5UnallocatedContiguous: the undefined storage address on
// a contiguous dataset refused the whole file; an empty dataset there
// is legal (nothing was ever allocated) and must load as empty, while
// a non-empty one names what is missing.
func TestLoadHDF5UnallocatedContiguous(t *testing.T) {
t.Run("empty", func(t *testing.T) {
f := h5HostileFile(512)
h5ObjectHeader(f, 96,
h5Msg{hdf5MsgDataspace, h5Dataspace(0)},
h5Msg{hdf5MsgDatatype, h5FloatType(8)},
h5Msg{hdf5MsgDataLayout, h5ContiguousLayout(math.MaxUint64, 0)},
)
sets, err := LoadHDF5(writeHostile(t, "empty.h5", f))
if err != nil {
t.Fatalf("an empty unallocated dataset: %v", err)
}
if len(sets) != 1 || sets[0].Values.Len() != 0 {
t.Fatalf("datasets = %d, want one empty dataset", len(sets))
}
})
t.Run("non-empty", func(t *testing.T) {
f := h5HostileFile(512)
h5ObjectHeader(f, 96,
h5Msg{hdf5MsgDataspace, h5Dataspace(2)},
h5Msg{hdf5MsgDatatype, h5FloatType(8)},
h5Msg{hdf5MsgDataLayout, h5ContiguousLayout(math.MaxUint64, 16)},
)
if _, err := LoadHDF5(writeHostile(t, "noalloc.h5", f)); err == nil || !strings.Contains(err.Error(), "never allocated") {
t.Fatalf("a non-empty unallocated dataset: err = %v", err)
}
})
}
// TestLoadCSVSkipBOM: a leading UTF-8 byte order mark used to glue
// itself onto the first field and fail the whole load with a strconv
// error.
func TestLoadCSVSkipBOM(t *testing.T) {
const in = "\xEF\xBB\xBF1.5,2.5\n3,4\n"
a, err := LoadCSVReader(strings.NewReader(in), false)
if err != nil {
t.Fatalf("LoadCSVReader with a BOM: %v", err)
}
if a.FloatAt(0) != 1.5 || a.FloatAt(1) != 2.5 || a.FloatAt(2) != 3 || a.FloatAt(3) != 4 {
t.Fatalf("values = %v", a)
}
}