fix(encode): pointer table arrays, whole-minute offsets and emission checks

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-22 21:15:00 +02:00
parent b45f4d65da
commit 0ded34da3c
2 changed files with 210 additions and 140 deletions
+113 -1
View File
@@ -67,7 +67,7 @@ func TestMarshalFloatSpecials(t *testing.T) {
}
}
func TestMarshalFloatNormalizesNegativeZero(t *testing.T) {
func TestMarshalFloatNormalisesNegativeZero(t *testing.T) {
// The output contract normalises negative zero to "0.0".
type Cfg struct {
Z float64 `toml:"z"`
@@ -2284,3 +2284,115 @@ func TestEmitFieldComments(t *testing.T) {
}
})
}
// errWriter fails every write with a fixed error.
type errWriter struct{ err error }
func (w errWriter) Write([]byte) (int, error) { return 0, w.err }
// TestMarshalWrite covers the streaming entry: the happy path with options
// and a failing writer.
func TestMarshalWrite(t *testing.T) {
var buf bytes.Buffer
err := MarshalWrite(&buf, map[string]any{"b": 2, "a": 1})
if err != nil {
t.Fatalf("MarshalWrite: %v", err)
}
// A map carries no order, so the writer uses the sorted one.
if buf.String() != "a = 1\nb = 2\n" {
t.Errorf("output = %q", buf.String())
}
writeErr := errors.New("boom")
if err := MarshalWrite(errWriter{writeErr}, map[string]any{"a": 1}); !errors.Is(err, writeErr) {
t.Errorf("err = %v, want the write error wrapped", err)
}
}
// TestMarshalRejectsUnsupportedKinds pins the clear error a field of a kind
// TOML cannot carry raises, through the struct walk.
func TestMarshalRejectsUnsupportedKinds(t *testing.T) {
tests := []struct {
name string
value any
}{
{"func", struct {
F func() `toml:"f"`
}{}},
{"chan", struct {
C chan int `toml:"c"`
}{}},
{"complex", struct {
Z complex128 `toml:"z"`
}{}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := Marshal(tt.value)
if err == nil {
t.Fatalf("Marshal accepted %#v", tt.value)
}
if !strings.Contains(err.Error(), "cannot encode") {
t.Errorf("err = %v, want the cannot-encode complaint", err)
}
})
}
}
// TestMarshalOmitsEmptyPointerTableSlice pins that an empty slice of pointer
// tables is omitted, the rule its non-pointer form already follows.
func TestMarshalOmitsEmptyPointerTableSlice(t *testing.T) {
type item struct {
N int `toml:"n"`
}
out, err := Marshal(struct {
Items []*item `toml:"items"`
}{})
if err != nil {
t.Fatalf("Marshal: %v", err)
}
if len(out) != 0 {
t.Errorf("output = %q, want the empty array of tables omitted", out)
}
}
// TestMarshalRejectsNonWholeMinuteOffset pins that a zone offset carrying
// seconds is refused instead of silently losing them.
func TestMarshalRejectsNonWholeMinuteOffset(t *testing.T) {
z := time.FixedZone("", 57*60+44)
_, err := Marshal(struct {
Stamp time.Time `toml:"stamp"`
}{Stamp: time.Date(1890, 1, 1, 12, 0, 0, 0, z)})
if err == nil || !strings.Contains(err.Error(), "not a whole number of minutes") {
t.Errorf("err = %v, want the whole-minute offset complaint", err)
}
_, err = Marshal(struct {
Stamp OffsetDateTime `toml:"stamp"`
}{Stamp: OffsetDateTime{time.Date(1890, 1, 1, 12, 0, 0, 0, z)}})
if err == nil || !strings.Contains(err.Error(), "not a whole number of minutes") {
t.Errorf("err = %v, want the whole-minute offset complaint for the wrapper", err)
}
}
// cancelOnMarshal cancels the context the encode runs under, the moment its
// method is called, so the emission that follows is already past the walk's
// own checks.
type cancelOnMarshal struct {
cancel context.CancelFunc
}
func (c cancelOnMarshal) MarshalTOML() (any, error) {
c.cancel()
return int64(1), nil
}
// TestMarshalContextCancelsDuringEmission pins that a context cancelled
// between the walk and the emission stops the encode instead of writing the
// whole document out.
func TestMarshalContextCancelsDuringEmission(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
value := map[string]any{"k": cancelOnMarshal{cancel}}
if _, err := MarshalContext(ctx, value); !errors.Is(err, context.Canceled) {
t.Errorf("err = %v, want the cancellation", err)
}
}