673 lines
17 KiB
Go
673 lines
17 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package interpres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestSyntaxErrorMessage(t *testing.T) {
|
|
err := &SyntaxError{Line: 7, Msg: "expected '=' after key"}
|
|
want := "interpres: line 7: expected '=' after key"
|
|
if got := err.Error(); got != want {
|
|
t.Errorf("Error() = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestParseRejectsInvalidUTF8(t *testing.T) {
|
|
_, err := Parse([]byte("v = \"\xff\"\n"))
|
|
if err == nil {
|
|
t.Fatal("expected a UTF-8 validation error")
|
|
}
|
|
se, ok := err.(*SyntaxError)
|
|
if !ok {
|
|
t.Fatalf("err is %T, want *SyntaxError", err)
|
|
}
|
|
if !strings.Contains(se.Msg, "UTF-8") {
|
|
t.Errorf("Msg = %q, want it to mention UTF-8", se.Msg)
|
|
}
|
|
if se.Line != 1 {
|
|
t.Errorf("Line = %d, want 1", se.Line)
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalIntoMap(t *testing.T) {
|
|
var m map[string]any
|
|
if err := Unmarshal([]byte(`name = "x"
|
|
count = 3
|
|
`), &m); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if m["name"] != "x" {
|
|
t.Errorf("name = %#v", m["name"])
|
|
}
|
|
if m["count"] != int64(3) {
|
|
t.Errorf("count = %#v (%T)", m["count"], m["count"])
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalIntoMapNested(t *testing.T) {
|
|
var m map[string]any
|
|
if err := Unmarshal([]byte("[a]\nb = 2\n"), &m); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
a, ok := m["a"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("a = %T, want map[string]any", m["a"])
|
|
}
|
|
if a["b"] != int64(2) {
|
|
t.Errorf("a.b = %#v", a["b"])
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalPrefilledMap(t *testing.T) {
|
|
m := map[string]any{"keep": "yes"}
|
|
if err := Unmarshal([]byte(`name = "x"`), &m); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if m["keep"] != "yes" {
|
|
t.Errorf("keep = %#v", m["keep"])
|
|
}
|
|
if m["name"] != "x" {
|
|
t.Errorf("name = %#v", m["name"])
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalTableToNonStructOrMap(t *testing.T) {
|
|
var s string
|
|
if err := Unmarshal([]byte("v = 1\n"), &s); err == nil {
|
|
t.Fatal("expected an error for table-to-scalar")
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalIntoAny(t *testing.T) {
|
|
// A non-nil any destination must accept the parsed tree.
|
|
var x any
|
|
if err := Unmarshal([]byte("[a]\nb = 2\n"), &x); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
tree, ok := x.(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("x is %T, want map[string]any", x)
|
|
}
|
|
a, ok := tree["a"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("a is %T, want map[string]any", tree["a"])
|
|
}
|
|
if a["b"] != int64(2) {
|
|
t.Errorf("a.b = %#v", a["b"])
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalIntoNilAny(t *testing.T) {
|
|
// A nil any target must still receive the parsed tree without
|
|
// panicking.
|
|
var x any
|
|
if err := Unmarshal([]byte("v = 1\n"), &x); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if x == nil {
|
|
t.Fatal("x is still nil after Unmarshal")
|
|
}
|
|
tree, ok := x.(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("x is %T, want map[string]any", x)
|
|
}
|
|
if tree["v"] != int64(1) {
|
|
t.Errorf("v = %#v", tree["v"])
|
|
}
|
|
}
|
|
|
|
func TestParseContextHonoursCancellation(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if _, err := ParseContext(ctx, []byte("a = 1\n")); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("ParseContext returned %v, want context.Canceled", err)
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalContextHonoursCancellation(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
var cfg map[string]any
|
|
if err := UnmarshalContext(ctx, []byte("a = 1\n"), &cfg); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("UnmarshalContext returned %v, want context.Canceled", err)
|
|
}
|
|
}
|
|
|
|
func TestDecoderDecodeContextHonoursCancellation(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
var cfg map[string]any
|
|
err := NewDecoder().DecodeContext(ctx, []byte("a = 1\n"), &cfg)
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("DecodeContext returned %v, want context.Canceled", err)
|
|
}
|
|
}
|
|
|
|
func TestContextRoundTrip(t *testing.T) {
|
|
// The *Context variants with a Background context must produce the same
|
|
// result as the non-context variants for ordinary inputs.
|
|
in := []byte(`title = "x"
|
|
count = 3
|
|
`)
|
|
if _, err := ParseContext(context.Background(), in); err != nil {
|
|
t.Fatalf("ParseContext: %v", err)
|
|
}
|
|
var out struct {
|
|
Title string `toml:"title"`
|
|
Count int `toml:"count"`
|
|
}
|
|
if err := UnmarshalContext(context.Background(), in, &out); err != nil {
|
|
t.Fatalf("UnmarshalContext: %v", err)
|
|
}
|
|
if out.Title != "x" || out.Count != 3 {
|
|
t.Errorf("out = %#v", out)
|
|
}
|
|
if err := NewDecoder().DecodeContext(context.Background(), in, &map[string]any{}); err != nil {
|
|
t.Fatalf("Decoder.DecodeContext: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalIntToUintOverflow(t *testing.T) {
|
|
type C struct {
|
|
X uint8 `toml:"x"`
|
|
}
|
|
var c C
|
|
err := Unmarshal([]byte("x = 300\n"), &c)
|
|
if err == nil {
|
|
t.Fatal("expected overflow error")
|
|
}
|
|
if !strings.Contains(err.Error(), "overflow") {
|
|
t.Errorf("err = %v, want substring 'overflow'", err)
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalIntToUint16Boundary(t *testing.T) {
|
|
type C struct {
|
|
X uint16 `toml:"x"`
|
|
}
|
|
// Exactly 65535 fits, 65536 does not.
|
|
var ok C
|
|
if err := Unmarshal([]byte("x = 65535\n"), &ok); err != nil {
|
|
t.Fatalf("65535 should fit uint16, got %v", err)
|
|
}
|
|
if ok.X != 65535 {
|
|
t.Errorf("X = %d, want 65535", ok.X)
|
|
}
|
|
|
|
var bad C
|
|
if err := Unmarshal([]byte("x = 65536\n"), &bad); err == nil {
|
|
t.Fatal("65536 must not fit uint16")
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalIntToUint64FitsMaxInt64(t *testing.T) {
|
|
type C struct {
|
|
X uint64 `toml:"x"`
|
|
}
|
|
var c C
|
|
tok := "x = 9223372036854775807\n" // math.MaxInt64
|
|
if err := Unmarshal([]byte(tok), &c); err != nil {
|
|
t.Fatalf("MaxInt64 should fit uint64, got %v", err)
|
|
}
|
|
if c.X != math.MaxInt64 {
|
|
t.Errorf("X = %d, want %d", uint64(c.X), uint64(math.MaxInt64))
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalNegativeIntToUint(t *testing.T) {
|
|
type C struct {
|
|
X uint8 `toml:"x"`
|
|
}
|
|
var c C
|
|
err := Unmarshal([]byte("x = -1\n"), &c)
|
|
if err == nil {
|
|
t.Fatal("expected negative-to-uint error")
|
|
}
|
|
if !strings.Contains(err.Error(), "negative") {
|
|
t.Errorf("err = %v, want substring 'negative'", err)
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalIntToFloat(t *testing.T) {
|
|
type C struct {
|
|
X float64 `toml:"x"`
|
|
}
|
|
var c C
|
|
if err := Unmarshal([]byte("x = 5\n"), &c); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if c.X != 5.0 {
|
|
t.Errorf("X = %v, want 5.0", c.X)
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalIntToStringFails(t *testing.T) {
|
|
type C struct {
|
|
X string `toml:"x"`
|
|
}
|
|
var c C
|
|
if err := Unmarshal([]byte(`x = 5`), &c); err == nil {
|
|
t.Fatal("expected type-mismatch error")
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalStringToIntFails(t *testing.T) {
|
|
type C struct {
|
|
X int `toml:"x"`
|
|
}
|
|
var c C
|
|
if err := Unmarshal([]byte(`x = "hello"`), &c); err == nil {
|
|
t.Fatal("expected type-mismatch error")
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalArrayToScalarFails(t *testing.T) {
|
|
type C struct {
|
|
X int `toml:"x"`
|
|
}
|
|
var c C
|
|
if err := Unmarshal([]byte("x = [1, 2]\n"), &c); err == nil {
|
|
t.Fatal("expected type-mismatch error")
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalArrayOfTablesToScalarFails(t *testing.T) {
|
|
type Item struct {
|
|
Name string `toml:"name"`
|
|
}
|
|
type C struct {
|
|
X Item `toml:"x"`
|
|
}
|
|
var c C
|
|
if err := Unmarshal([]byte("[[x]]\nname = \"a\"\n"), &c); err == nil {
|
|
t.Fatal("expected type-mismatch error")
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalFloatToIntFails(t *testing.T) {
|
|
type C struct {
|
|
X int `toml:"x"`
|
|
}
|
|
var c C
|
|
if err := Unmarshal([]byte("x = 1.5\n"), &c); err == nil {
|
|
t.Fatal("expected type-mismatch error")
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalBoolToIntFails(t *testing.T) {
|
|
type C struct {
|
|
X int `toml:"x"`
|
|
}
|
|
var c C
|
|
if err := Unmarshal([]byte("x = true\n"), &c); err == nil {
|
|
t.Fatal("expected type-mismatch error")
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalNonPointerRejected(t *testing.T) {
|
|
var v int
|
|
if err := Unmarshal([]byte("x = 1\n"), v); err == nil {
|
|
t.Fatal("expected error for non-pointer target")
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalAssignErrorWrapped(t *testing.T) {
|
|
// assignStruct wraps inner assignment errors with the key name.
|
|
type C struct {
|
|
Inner struct {
|
|
X int `toml:"x"`
|
|
} `toml:"inner"`
|
|
}
|
|
var c C
|
|
err := Unmarshal([]byte("[inner]\nx = \"oops\"\n"), &c)
|
|
if err == nil {
|
|
t.Fatal("expected an assignment error")
|
|
}
|
|
if !strings.Contains(err.Error(), "x") {
|
|
t.Errorf("err = %v, want it to mention key x", err)
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalAssignMapErrorWrapped(t *testing.T) {
|
|
// assignMap wraps inner errors with the key of the bad element.
|
|
m := map[string]int{}
|
|
err := Unmarshal([]byte("[s]\nx = 1\n"), &m)
|
|
if err == nil {
|
|
t.Fatal("expected an assignment error")
|
|
}
|
|
if !strings.Contains(err.Error(), "s") {
|
|
t.Errorf("err = %v, want it to mention key 's'", err)
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalAssignSliceErrorWrapped(t *testing.T) {
|
|
// assignSlice wraps inner errors with the bad element's index.
|
|
type C struct {
|
|
Items []int `toml:"items"`
|
|
}
|
|
var c C
|
|
err := Unmarshal([]byte(`items = [1, "oops"]`), &c)
|
|
if err == nil {
|
|
t.Fatal("expected an assignment error")
|
|
}
|
|
if !strings.Contains(err.Error(), "[1]") {
|
|
t.Errorf("err = %v, want it to mention index [1]", err)
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalAssignTimeToWrongTypeFails(t *testing.T) {
|
|
// assign maps time.Time to time.Time only.
|
|
type C struct {
|
|
T string `toml:"t"`
|
|
}
|
|
var c C
|
|
err := Unmarshal([]byte("t = 2026-01-01T00:00:00Z\n"), &c)
|
|
if err == nil {
|
|
t.Fatal("expected time-to-string assignment to fail")
|
|
}
|
|
}
|
|
|
|
func TestMarshalerUsesCustomEncoding(t *testing.T) {
|
|
// A type that implements Marshaler can be encoded through a wrapping struct.
|
|
val := inlineMarshaler(func() (any, error) {
|
|
return map[string]any{"k": "v"}, nil
|
|
})
|
|
type wrap struct {
|
|
Inner inlineMarshaler `toml:"inner"`
|
|
}
|
|
out, err := Marshal(wrap{Inner: val})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
got := string(out)
|
|
if !strings.Contains(got, "[inner]") || !strings.Contains(got, "k = \"v\"") {
|
|
t.Errorf("out = %q, want a [inner] table with k = \"v\"", got)
|
|
}
|
|
}
|
|
|
|
type inlineMarshaler func() (any, error)
|
|
|
|
func (i inlineMarshaler) MarshalTOML() (any, error) { return i() }
|
|
|
|
// --- Unmarshaler -----------------------------------------------------------
|
|
|
|
// receiverSetter implements *Unmarshaler by reshaping a parsed table.
|
|
type receiverSetter struct {
|
|
Field string
|
|
Received any
|
|
}
|
|
|
|
func (r *receiverSetter) UnmarshalTOML(data any) error {
|
|
m, ok := data.(map[string]any)
|
|
if !ok {
|
|
return fmt.Errorf("interpres: receiverSetter expects table, got %T", data)
|
|
}
|
|
if v, ok := m["field"].(string); ok {
|
|
r.Field = v
|
|
}
|
|
r.Received = data
|
|
return nil
|
|
}
|
|
|
|
func TestUnmarshalerByPointer(t *testing.T) {
|
|
type Cfg struct {
|
|
R receiverSetter `toml:"r"`
|
|
}
|
|
var cfg Cfg
|
|
if err := Unmarshal([]byte(`[r]
|
|
field = "x"
|
|
`), &cfg); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if cfg.R.Field != "x" {
|
|
t.Errorf("Field = %q, want \"x\"", cfg.R.Field)
|
|
}
|
|
if cfg.R.Received == nil {
|
|
t.Error("Receiver did not see parsed data")
|
|
}
|
|
}
|
|
|
|
type scalarUnmarshaler struct{ val string }
|
|
|
|
func (s *scalarUnmarshaler) UnmarshalTOML(data any) error {
|
|
str, ok := data.(string)
|
|
if !ok {
|
|
return fmt.Errorf("interpres: scalarUnmarshaler expects string, got %T", data)
|
|
}
|
|
s.val = str
|
|
return nil
|
|
}
|
|
|
|
func TestUnmarshalerReceivesRawScalar(t *testing.T) {
|
|
// Place the Unmarshaler-implementing field inside a wrapper struct so
|
|
// the decoder dispatches the scalar value to its UnmarshalTOML.
|
|
type Cfg struct {
|
|
S scalarUnmarshaler `toml:"s"`
|
|
}
|
|
var cfg Cfg
|
|
if err := Unmarshal([]byte(`s = "hello"`), &cfg); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if cfg.S.val != "hello" {
|
|
t.Errorf("cfg.S.val = %q, want \"hello\"", cfg.S.val)
|
|
}
|
|
}
|
|
|
|
type failingUnmarshaler struct{}
|
|
|
|
func (f *failingUnmarshaler) UnmarshalTOML(_ any) error { return errors.New("boom") }
|
|
|
|
func TestUnmarshalerErrorPropagates(t *testing.T) {
|
|
type Cfg struct {
|
|
F failingUnmarshaler `toml:"f"`
|
|
}
|
|
var cfg Cfg
|
|
if err := Unmarshal([]byte("f = 1"), &cfg); err == nil {
|
|
t.Fatal("expected error from UnmarshalTOML")
|
|
} else if !strings.Contains(err.Error(), "boom") {
|
|
t.Errorf("err = %v, want substring \"boom\"", err)
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalerTakesPrecedenceOverDefault(t *testing.T) {
|
|
// Even when the field is a scalar type and the value is a table,
|
|
// UnmarshalTOML wins, because the receiver decides.
|
|
type Cfg struct {
|
|
R receiverSetter `toml:"r"`
|
|
}
|
|
var cfg Cfg
|
|
in := []byte(`[r]
|
|
field = "y"
|
|
`)
|
|
if err := Unmarshal(in, &cfg); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if cfg.R.Field != "y" {
|
|
t.Errorf("Field = %q, want \"y\"", cfg.R.Field)
|
|
}
|
|
}
|
|
|
|
// --- embedded field symmetry -----------------------------------------------
|
|
|
|
type RoundTripBase struct {
|
|
ID int `toml:"id"`
|
|
Name string `toml:"name"`
|
|
}
|
|
|
|
type RoundTripDerived struct {
|
|
RoundTripBase
|
|
X string `toml:"x"`
|
|
}
|
|
|
|
func TestUnmarshalEmbeddedStructRoundTrip(t *testing.T) {
|
|
orig := RoundTripDerived{ID: 1, Name: "b", X: "x"}
|
|
out, err := Marshal(orig)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
var back RoundTripDerived
|
|
if err := Unmarshal(out, &back); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if back != orig {
|
|
t.Fatalf("round-trip mismatch:\nwas: %+v\nnow: %+v", orig, back)
|
|
}
|
|
}
|
|
|
|
type RoundTripPtrCfg struct {
|
|
*RoundTripBase
|
|
X string `toml:"x"`
|
|
}
|
|
|
|
func TestUnmarshalEmbeddedPointerStruct(t *testing.T) {
|
|
var cfg RoundTripPtrCfg
|
|
if err := Unmarshal([]byte("id = 7\nname = \"n\"\nx = \"x\"\n"), &cfg); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if cfg.RoundTripBase == nil || cfg.ID != 7 || cfg.Name != "n" || cfg.X != "x" {
|
|
t.Fatalf("decoded: %+v", cfg)
|
|
}
|
|
}
|
|
|
|
type RoundTripExtra map[string]int
|
|
|
|
type RoundTripMapCfg struct {
|
|
RoundTripExtra
|
|
X string `toml:"x"`
|
|
}
|
|
|
|
func TestUnmarshalEmbeddedMap(t *testing.T) {
|
|
var cfg RoundTripMapCfg
|
|
if err := Unmarshal([]byte("alpha = 1\nx = \"x\"\n"), &cfg); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if cfg.RoundTripExtra["alpha"] != 1 || cfg.X != "x" {
|
|
t.Fatalf("decoded: %+v", cfg)
|
|
}
|
|
|
|
orig := RoundTripMapCfg{RoundTripExtra: RoundTripExtra{"a": 1}, X: "x"}
|
|
out, err := Marshal(orig)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
var back RoundTripMapCfg
|
|
if err := Unmarshal(out, &back); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if back.X != "x" || back.RoundTripExtra["a"] != 1 {
|
|
t.Fatalf("round-trip mismatch: %+v", back)
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalEmbeddedNameClashShallowerWins(t *testing.T) {
|
|
type Inner struct {
|
|
Name string `toml:"name"`
|
|
Deep string `toml:"deep"`
|
|
}
|
|
type Outer struct {
|
|
Inner
|
|
Name string `toml:"name"`
|
|
}
|
|
var v Outer
|
|
if err := Unmarshal([]byte("name = \"outer\"\ndeep = \"d\"\n"), &v); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if v.Name != "outer" || v.Deep != "d" {
|
|
t.Fatalf("decoded: %+v", v)
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalNameClashEqualDepthLaterWins(t *testing.T) {
|
|
// At equal depth the field declared later resolves the name, matching the
|
|
// documented rule.
|
|
type C struct {
|
|
First string `toml:"v"`
|
|
Second int `toml:"v"`
|
|
}
|
|
var c C
|
|
if err := Unmarshal([]byte("v = 1\n"), &c); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if c.Second != 1 {
|
|
t.Fatalf("decoded: %+v, want the later field to take the value", c)
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalUnknownKeyWithoutEmbeddedMap(t *testing.T) {
|
|
var cfg RoundTripDerived
|
|
if err := Unmarshal([]byte("rogue = 1\n"), &cfg); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if cfg.ID != 0 || cfg.X != "" {
|
|
t.Fatalf("decoded: %+v", cfg)
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalStrictEmbeddedMapStaysStrict(t *testing.T) {
|
|
type Cfg struct {
|
|
RoundTripExtra
|
|
Name string `toml:"name"`
|
|
}
|
|
dec := NewDecoder().DisallowUnknownFields()
|
|
err := dec.Decode([]byte("name = \"n\"\nrogue = 1\n"), &Cfg{})
|
|
if err == nil || !strings.Contains(err.Error(), "unknown field") {
|
|
t.Fatalf("expected unknown field error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDecodeErrorCarriesPath(t *testing.T) {
|
|
type Item struct {
|
|
Name string `toml:"name"`
|
|
Weight uint8 `toml:"weight"`
|
|
}
|
|
type Cfg struct {
|
|
Tags []string `toml:"tags"`
|
|
Items []Item `toml:"items"`
|
|
}
|
|
var cfg Cfg
|
|
err := Unmarshal([]byte("[[items]]\nname = \"a\"\nweight = 300\n"), &cfg)
|
|
if err == nil {
|
|
t.Fatal("expected an overflow error")
|
|
}
|
|
de, ok := errors.AsType[*DecodeError](err)
|
|
if !ok {
|
|
t.Fatalf("expected a *DecodeError, got %T: %v", err, err)
|
|
}
|
|
want := []string{"items", "[0]", "weight"}
|
|
if !slices.Equal(de.Path, want) {
|
|
t.Fatalf("Path = %v, want %v", de.Path, want)
|
|
}
|
|
if de.Err == nil || !strings.Contains(de.Err.Error(), "overflows uint8") {
|
|
t.Fatalf("Err = %v", de.Err)
|
|
}
|
|
// The rendered message keeps its shape: segments joined with ": ".
|
|
wantMsg := "items: [0]: weight: interpres: integer 300 overflows uint8"
|
|
if err.Error() != wantMsg {
|
|
t.Fatalf("message = %q, want %q", err.Error(), wantMsg)
|
|
}
|
|
}
|
|
|
|
func TestDecodeErrorOnMapDestination(t *testing.T) {
|
|
var m map[string]uint8
|
|
err := Unmarshal([]byte("count = -1\n"), &m)
|
|
if err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
de, ok := errors.AsType[*DecodeError](err)
|
|
if !ok {
|
|
t.Fatalf("expected a *DecodeError, got %T: %v", err, err)
|
|
}
|
|
if !slices.Equal(de.Path, []string{"count"}) {
|
|
t.Fatalf("Path = %v", de.Path)
|
|
}
|
|
}
|