2026-08-19 09:47:00 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
|
|
package interpres
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"math"
|
2026-09-19 02:41:09 +02:00
|
|
|
"net"
|
2026-09-17 21:20:47 +02:00
|
|
|
"slices"
|
2026-08-19 09:47:00 +02:00
|
|
|
"strings"
|
|
|
|
|
"testing"
|
2026-09-19 02:41:09 +02:00
|
|
|
"time"
|
2026-08-19 09:47:00 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-09-20 22:15:10 +02:00
|
|
|
// The scan validates UTF-8 where it meets the byte, so the reported line
|
|
|
|
|
// is the invalid byte's own, wherever in the document it sits.
|
|
|
|
|
cases := []struct {
|
|
|
|
|
name string
|
|
|
|
|
doc string
|
|
|
|
|
line int
|
|
|
|
|
}{
|
|
|
|
|
{"in a basic string", "v = \"\xff\"\n", 1},
|
|
|
|
|
{"in a literal string", "v = '\xff'\n", 1},
|
|
|
|
|
{"in a multiline string", "v = \"\"\"\n\xff\"\"\"\n", 2},
|
|
|
|
|
{"in a comment", "v = 1\n# caf\xe9\xff\n", 2},
|
|
|
|
|
{"in a bare key", "va\xfflue = 1\n", 1},
|
|
|
|
|
{"as a statement", "\xff = 1\n", 1},
|
|
|
|
|
{"in a bare value", "v = \xff1\n", 1},
|
|
|
|
|
{"after a value", "v = 1 \xff\n", 1},
|
|
|
|
|
{"after the first line", "a = 1\nb = \"\xff\"\n", 2},
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
2026-09-20 22:15:10 +02:00
|
|
|
for _, c := range cases {
|
|
|
|
|
_, err := ParseMap([]byte(c.doc))
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatalf("%s: expected a UTF-8 validation error", c.name)
|
|
|
|
|
}
|
|
|
|
|
se, ok := err.(*SyntaxError)
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatalf("%s: err is %T, want *SyntaxError", c.name, err)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(se.Msg, "UTF-8") {
|
|
|
|
|
t.Errorf("%s: Msg = %q, want it to mention UTF-8", c.name, se.Msg)
|
|
|
|
|
}
|
|
|
|
|
if se.Line != c.line {
|
|
|
|
|
t.Errorf("%s: Line = %d, want %d", c.name, se.Line, c.line)
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()
|
2026-09-20 10:40:57 +02:00
|
|
|
if _, err := ParseMapContext(ctx, []byte("a = 1\n")); !errors.Is(err, context.Canceled) {
|
2026-08-19 09:47:00 +02:00
|
|
|
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
|
|
|
|
|
`)
|
2026-09-20 10:40:57 +02:00
|
|
|
if _, err := ParseMapContext(context.Background(), in); err != nil {
|
2026-08-19 09:47:00 +02:00
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 23:10:24 +02:00
|
|
|
func TestUnmarshalFloat32Overflow(t *testing.T) {
|
|
|
|
|
// A finite float64 beyond the float32 range must not decode silently as
|
|
|
|
|
// an infinity.
|
|
|
|
|
type C struct {
|
|
|
|
|
X float32 `toml:"x"`
|
|
|
|
|
}
|
|
|
|
|
var c C
|
|
|
|
|
err := Unmarshal([]byte("x = 1e300\n"), &c)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected overflow error for float32")
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(err.Error(), "overflow") {
|
|
|
|
|
t.Errorf("err = %v, want substring 'overflow'", err.Error())
|
|
|
|
|
}
|
|
|
|
|
// Infinities themselves pass through, and in-range values are untouched.
|
|
|
|
|
var ok C
|
|
|
|
|
if err := Unmarshal([]byte("x = inf\n"), &ok); err != nil {
|
|
|
|
|
t.Fatalf("inf should decode into float32, got %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !math.IsInf(float64(ok.X), 1) {
|
|
|
|
|
t.Errorf("X = %v, want +Inf", ok.X)
|
|
|
|
|
}
|
|
|
|
|
if err := Unmarshal([]byte("x = 1.5\n"), &ok); err != nil || ok.X != 1.5 {
|
|
|
|
|
t.Fatalf("1.5 should decode into float32, got %v (X=%v)", err, ok.X)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-19 09:47:00 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-17 20:04:41 +02:00
|
|
|
|
|
|
|
|
// --- 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 23:07:53 +02:00
|
|
|
// A struct embedding a pointer to itself is legal Go; decoding into it must
|
|
|
|
|
// terminate. The schema walk used to recurse through the embedded type
|
|
|
|
|
// forever.
|
|
|
|
|
func TestUnmarshalSelfEmbeddedPointerStructTerminates(t *testing.T) {
|
|
|
|
|
type SelfLink struct {
|
|
|
|
|
*SelfLink
|
|
|
|
|
X int `toml:"x"`
|
|
|
|
|
Y string `toml:"y"`
|
|
|
|
|
}
|
|
|
|
|
var n SelfLink
|
|
|
|
|
if err := Unmarshal([]byte("x = 1\ny = \"s\"\n"), &n); err != nil {
|
|
|
|
|
t.Fatalf("unmarshal: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if n.X != 1 || n.Y != "s" {
|
|
|
|
|
t.Fatalf("decoded: %+v", n)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A nil self pointer on the encode side stays skippable, as any nil
|
|
|
|
|
// embedded pointer is.
|
|
|
|
|
out, err := Marshal(SelfLink{X: 2})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("marshal: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if want := "x = 2\ny = \"\"\n"; string(out) != want {
|
|
|
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 20:04:41 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 23:07:27 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 20:04:41 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-17 21:20:47 +02:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-19 02:40:51 +02:00
|
|
|
|
|
|
|
|
func TestUnmarshalIntoDefinedScalarTypes(t *testing.T) {
|
|
|
|
|
// A defined type whose underlying kind is string or bool takes the value.
|
|
|
|
|
// A bare reflect Set panics on such a type, because a string is not
|
|
|
|
|
// assignable to a defined string type without a conversion.
|
|
|
|
|
type Name string
|
|
|
|
|
type Flag bool
|
|
|
|
|
type Cfg struct {
|
|
|
|
|
N Name `toml:"n"`
|
|
|
|
|
F Flag `toml:"f"`
|
|
|
|
|
}
|
|
|
|
|
var cfg Cfg
|
|
|
|
|
if err := Unmarshal([]byte("n = \"x\"\nf = true\n"), &cfg); err != nil {
|
|
|
|
|
t.Fatalf("unmarshal: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if cfg.N != "x" {
|
|
|
|
|
t.Errorf("N = %q, want \"x\"", cfg.N)
|
|
|
|
|
}
|
|
|
|
|
if !cfg.F {
|
|
|
|
|
t.Error("F = false, want true")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-19 02:41:09 +02:00
|
|
|
|
|
|
|
|
// --- encoding.TextUnmarshaler and time.Duration ----------------------------
|
|
|
|
|
|
|
|
|
|
// textReceiver implements encoding.TextUnmarshaler on the pointer receiver.
|
|
|
|
|
type textReceiver struct{ Text string }
|
|
|
|
|
|
|
|
|
|
func (t *textReceiver) UnmarshalText(text []byte) error {
|
|
|
|
|
t.Text = "got:" + string(text)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// upperText is a defined string type whose UnmarshalText transforms the
|
|
|
|
|
// content, so a plain string assignment would leave the wrong value behind.
|
|
|
|
|
type upperText string
|
|
|
|
|
|
|
|
|
|
func (u *upperText) UnmarshalText(text []byte) error {
|
|
|
|
|
*u = upperText(strings.ToUpper(string(text)))
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// failingTextUnmarshaler fails the decode from UnmarshalText.
|
|
|
|
|
type failingTextUnmarshaler struct{}
|
|
|
|
|
|
|
|
|
|
func (f *failingTextUnmarshaler) UnmarshalText(_ []byte) error { return errors.New("text boom") }
|
|
|
|
|
|
|
|
|
|
// textAndTOMLReceiver implements both decode interfaces; the TOML method wins.
|
|
|
|
|
type textAndTOMLReceiver struct{ From string }
|
|
|
|
|
|
|
|
|
|
func (t *textAndTOMLReceiver) UnmarshalTOML(any) error { t.From = "toml"; return nil }
|
|
|
|
|
|
|
|
|
|
func (t *textAndTOMLReceiver) UnmarshalText([]byte) error { t.From = "text"; return nil }
|
|
|
|
|
|
|
|
|
|
func TestTextUnmarshalerByPointer(t *testing.T) {
|
|
|
|
|
type Cfg struct {
|
|
|
|
|
R textReceiver `toml:"r"`
|
|
|
|
|
}
|
|
|
|
|
var cfg Cfg
|
|
|
|
|
if err := Unmarshal([]byte(`r = "hello"`), &cfg); err != nil {
|
|
|
|
|
t.Fatalf("unmarshal: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if cfg.R.Text != "got:hello" {
|
|
|
|
|
t.Errorf("Text = %q, want \"got:hello\"", cfg.R.Text)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTextUnmarshalerWinsOverKindAssignment(t *testing.T) {
|
|
|
|
|
type Cfg struct {
|
|
|
|
|
U upperText `toml:"u"`
|
|
|
|
|
}
|
|
|
|
|
var cfg Cfg
|
|
|
|
|
if err := Unmarshal([]byte(`u = "abc"`), &cfg); err != nil {
|
|
|
|
|
t.Fatalf("unmarshal: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if cfg.U != "ABC" {
|
|
|
|
|
t.Errorf("U = %q, want \"ABC\"", cfg.U)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTextUnmarshalerForNetIP(t *testing.T) {
|
|
|
|
|
type Cfg struct {
|
|
|
|
|
V4 net.IP `toml:"v4"`
|
|
|
|
|
V6 net.IP `toml:"v6"`
|
|
|
|
|
IPs []net.IP `toml:"ips"`
|
|
|
|
|
}
|
|
|
|
|
in := "v4 = \"192.0.2.1\"\nv6 = \"2001:db8::68\"\nips = [\"198.51.100.7\", \"203.0.113.9\"]\n"
|
|
|
|
|
var cfg Cfg
|
|
|
|
|
if err := Unmarshal([]byte(in), &cfg); err != nil {
|
|
|
|
|
t.Fatalf("unmarshal: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got := cfg.V4.String(); got != "192.0.2.1" {
|
|
|
|
|
t.Errorf("V4 = %q, want \"192.0.2.1\"", got)
|
|
|
|
|
}
|
|
|
|
|
if got := cfg.V6.String(); got != "2001:db8::68" {
|
|
|
|
|
t.Errorf("V6 = %q, want \"2001:db8::68\"", got)
|
|
|
|
|
}
|
|
|
|
|
if len(cfg.IPs) != 2 || cfg.IPs[0].String() != "198.51.100.7" || cfg.IPs[1].String() != "203.0.113.9" {
|
|
|
|
|
t.Errorf("IPs = %v, want two addresses", cfg.IPs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTextUnmarshalerSeesStringsOnly(t *testing.T) {
|
|
|
|
|
// An integer keeps its own rule: the text method is not consulted, and the
|
|
|
|
|
// value does not reach the receiver.
|
|
|
|
|
type Cfg struct {
|
|
|
|
|
R textReceiver `toml:"r"`
|
|
|
|
|
}
|
|
|
|
|
var cfg Cfg
|
|
|
|
|
err := Unmarshal([]byte("r = 1\n"), &cfg)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected an integer to be rejected for a text receiver")
|
|
|
|
|
}
|
|
|
|
|
if cfg.R.Text != "" {
|
|
|
|
|
t.Errorf("Text = %q, want it untouched", cfg.R.Text)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUnmarshalTOMLWinsOverTextUnmarshaler(t *testing.T) {
|
|
|
|
|
type Cfg struct {
|
|
|
|
|
B textAndTOMLReceiver `toml:"b"`
|
|
|
|
|
}
|
|
|
|
|
var cfg Cfg
|
|
|
|
|
if err := Unmarshal([]byte(`b = "x"`), &cfg); err != nil {
|
|
|
|
|
t.Fatalf("unmarshal: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if cfg.B.From != "toml" {
|
|
|
|
|
t.Errorf("From = %q, want \"toml\"", cfg.B.From)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTextUnmarshalerErrorCarriesPath(t *testing.T) {
|
|
|
|
|
type Inner struct {
|
|
|
|
|
F failingTextUnmarshaler `toml:"f"`
|
|
|
|
|
}
|
|
|
|
|
type Cfg struct {
|
|
|
|
|
Inner Inner `toml:"inner"`
|
|
|
|
|
}
|
|
|
|
|
var cfg Cfg
|
|
|
|
|
err := Unmarshal([]byte("[inner]\nf = \"x\"\n"), &cfg)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected an error from UnmarshalText")
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(err.Error(), "unmarshal text: text boom") {
|
|
|
|
|
t.Errorf("err = %v, want the text error wrapped", err)
|
|
|
|
|
}
|
|
|
|
|
de, ok := errors.AsType[*DecodeError](err)
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatalf("expected a *DecodeError, got %T: %v", err, err)
|
|
|
|
|
}
|
|
|
|
|
if !slices.Equal(de.Path, []string{"inner", "f"}) {
|
|
|
|
|
t.Fatalf("Path = %v, want [inner f]", de.Path)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTextUnmarshalerReportsBadText(t *testing.T) {
|
|
|
|
|
var cfg struct {
|
|
|
|
|
IP net.IP `toml:"ip"`
|
|
|
|
|
}
|
|
|
|
|
err := Unmarshal([]byte(`ip = "not-an-ip"`), &cfg)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected an error for a malformed address")
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(err.Error(), "unmarshal text:") {
|
|
|
|
|
t.Errorf("err = %v, want it wrapped as a text error", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUnmarshalDurations(t *testing.T) {
|
|
|
|
|
type Cfg struct {
|
|
|
|
|
FromText time.Duration `toml:"from_text"`
|
|
|
|
|
FromInt time.Duration `toml:"from_int"`
|
|
|
|
|
Fraction time.Duration `toml:"fraction"`
|
|
|
|
|
}
|
|
|
|
|
in := "from_text = \"1h30m\"\nfrom_int = 5400000000000\nfraction = \"1.5s\"\n"
|
|
|
|
|
var cfg Cfg
|
|
|
|
|
if err := Unmarshal([]byte(in), &cfg); err != nil {
|
|
|
|
|
t.Fatalf("unmarshal: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if cfg.FromText != 90*time.Minute {
|
|
|
|
|
t.Errorf("FromText = %v, want %v", cfg.FromText, 90*time.Minute)
|
|
|
|
|
}
|
|
|
|
|
if cfg.FromInt != 90*time.Minute {
|
|
|
|
|
t.Errorf("FromInt = %v, want %v", cfg.FromInt, 90*time.Minute)
|
|
|
|
|
}
|
|
|
|
|
if cfg.Fraction != 1500*time.Millisecond {
|
|
|
|
|
t.Errorf("Fraction = %v, want %v", cfg.Fraction, 1500*time.Millisecond)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUnmarshalDurationRejectsMalformedText(t *testing.T) {
|
|
|
|
|
var cfg struct {
|
|
|
|
|
D time.Duration `toml:"d"`
|
|
|
|
|
}
|
|
|
|
|
err := Unmarshal([]byte("d = \"90\"\n"), &cfg)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected an error for a duration without a unit")
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(err.Error(), "invalid duration") {
|
|
|
|
|
t.Errorf("err = %v, want an invalid-duration message", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestQuotedStringNeverBecomesDateTime(t *testing.T) {
|
|
|
|
|
// The date-time types take a bare timestamp only, so the text path is
|
|
|
|
|
// excluded for them and a quoted string stays a string.
|
|
|
|
|
var stamp struct {
|
|
|
|
|
S time.Time `toml:"s"`
|
|
|
|
|
}
|
|
|
|
|
err := Unmarshal([]byte("s = \"2026-06-26T10:00:00Z\"\n"), &stamp)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected a quoted string to be rejected for time.Time")
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(err.Error(), "cannot assign string") {
|
|
|
|
|
t.Errorf("err = %v, want a cannot-assign message", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var day struct {
|
|
|
|
|
D LocalDate `toml:"d"`
|
|
|
|
|
}
|
|
|
|
|
if err := Unmarshal([]byte("d = \"1979-05-27\"\n"), &day); err == nil {
|
|
|
|
|
t.Fatal("expected a quoted string to be rejected for LocalDate")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-19 19:36:24 +02:00
|
|
|
|
|
|
|
|
func TestDecoderMaxDepth(t *testing.T) {
|
|
|
|
|
deep := func(n int) []byte {
|
|
|
|
|
return []byte("v = " + strings.Repeat("[", n) + strings.Repeat("]", n) + "\n")
|
|
|
|
|
}
|
|
|
|
|
var cfg struct {
|
|
|
|
|
V any `toml:"v"`
|
|
|
|
|
}
|
|
|
|
|
if err := NewDecoder().MaxDepth(4).Decode(deep(4), &cfg); err != nil {
|
|
|
|
|
t.Fatalf("at the limit: %v", err)
|
|
|
|
|
}
|
|
|
|
|
err := NewDecoder().MaxDepth(4).Decode(deep(5), &cfg)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected a nesting error")
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(err.Error(), "limit of 4") {
|
|
|
|
|
t.Errorf("err = %v, want it to name the limit", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDecoderMaxInputSize(t *testing.T) {
|
|
|
|
|
doc := []byte("v = \"ab\"\n")
|
|
|
|
|
var cfg struct {
|
|
|
|
|
V string `toml:"v"`
|
|
|
|
|
}
|
|
|
|
|
if err := NewDecoder().MaxInputSize(len(doc)).Decode(doc, &cfg); err != nil {
|
|
|
|
|
t.Fatalf("at the limit: %v", err)
|
|
|
|
|
}
|
|
|
|
|
err := NewDecoder().MaxInputSize(len(doc)-1).Decode(doc, &cfg)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected a size error")
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(err.Error(), "over the limit of 8") {
|
|
|
|
|
t.Errorf("err = %v, want it to name the limit", err)
|
|
|
|
|
}
|
|
|
|
|
// Parse carries the nesting default and no size limit.
|
2026-09-20 10:40:57 +02:00
|
|
|
if _, err := ParseMap(doc); err != nil {
|
2026-09-19 19:36:24 +02:00
|
|
|
t.Fatalf("parse: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- OffsetDateTime --------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
func TestOffsetDateTimeIsTheParsedType(t *testing.T) {
|
|
|
|
|
// A document's offset date-time arrives as the wrapper, and a plain
|
|
|
|
|
// time.Time destination still takes it, so a timestamp field needs no
|
|
|
|
|
// change to keep working.
|
|
|
|
|
in := []byte("stamp = 2026-06-26T10:00:00-07:00\n")
|
|
|
|
|
want := time.Date(2026, 6, 26, 10, 0, 0, 0, time.FixedZone("", -7*3600))
|
|
|
|
|
|
|
|
|
|
var plain struct {
|
|
|
|
|
Stamp time.Time `toml:"stamp"`
|
|
|
|
|
}
|
|
|
|
|
if err := Unmarshal(in, &plain); err != nil {
|
|
|
|
|
t.Fatalf("unmarshal: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !plain.Stamp.Equal(want) {
|
|
|
|
|
t.Errorf("time.Time destination = %v, want %v", plain.Stamp, want)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var wrapped struct {
|
|
|
|
|
Stamp OffsetDateTime `toml:"stamp"`
|
|
|
|
|
}
|
|
|
|
|
if err := Unmarshal(in, &wrapped); err != nil {
|
|
|
|
|
t.Fatalf("unmarshal: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !wrapped.Stamp.Time.Equal(want) {
|
|
|
|
|
t.Errorf("OffsetDateTime destination = %v, want %v", wrapped.Stamp.Time, want)
|
|
|
|
|
}
|
|
|
|
|
if got := wrapped.Stamp.String(); got != "2026-06-26T10:00-07:00" {
|
|
|
|
|
t.Errorf("String() = %q, want 2026-06-26T10:00-07:00", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestOffsetDateTimeFromHandBuiltTree(t *testing.T) {
|
|
|
|
|
// A tree built by hand may carry a plain time.Time, which is the other
|
|
|
|
|
// source of the offset kind; both date-time destinations take it.
|
|
|
|
|
tree := map[string]any{"stamp": time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC)}
|
|
|
|
|
want := time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC)
|
|
|
|
|
|
|
|
|
|
var plain struct {
|
|
|
|
|
Stamp time.Time `toml:"stamp"`
|
|
|
|
|
}
|
|
|
|
|
if err := newDecoder().decode(tree, &plain); err != nil {
|
|
|
|
|
t.Fatalf("decode: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !plain.Stamp.Equal(want) {
|
|
|
|
|
t.Errorf("time.Time destination = %v, want %v", plain.Stamp, want)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var wrapped struct {
|
|
|
|
|
Stamp OffsetDateTime `toml:"stamp"`
|
|
|
|
|
}
|
|
|
|
|
if err := newDecoder().decode(tree, &wrapped); err != nil {
|
|
|
|
|
t.Fatalf("decode: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !wrapped.Stamp.Time.Equal(want) {
|
|
|
|
|
t.Errorf("OffsetDateTime destination = %v, want %v", wrapped.Stamp.Time, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// dateKindReceiver records the Go type UnmarshalTOML was handed.
|
|
|
|
|
type dateKindReceiver struct{ Kind string }
|
|
|
|
|
|
|
|
|
|
func (r *dateKindReceiver) UnmarshalTOML(data any) error {
|
|
|
|
|
r.Kind = fmt.Sprintf("%T", data)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUnmarshalerReceivesOffsetDateTime(t *testing.T) {
|
|
|
|
|
// The interface sees the wrapper, which names the date-time kind on its
|
|
|
|
|
// own; the local kinds keep their own wrappers.
|
|
|
|
|
var cfg struct {
|
|
|
|
|
O dateKindReceiver `toml:"o"`
|
|
|
|
|
L dateKindReceiver `toml:"l"`
|
|
|
|
|
}
|
|
|
|
|
in := []byte("o = 2026-06-26T10:00:00Z\nl = 2026-06-26T10:00:00\n")
|
|
|
|
|
if err := Unmarshal(in, &cfg); err != nil {
|
|
|
|
|
t.Fatalf("unmarshal: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if cfg.O.Kind != "interpres.OffsetDateTime" {
|
|
|
|
|
t.Errorf("offset kind = %q, want interpres.OffsetDateTime", cfg.O.Kind)
|
|
|
|
|
}
|
|
|
|
|
if cfg.L.Kind != "interpres.LocalDateTime" {
|
|
|
|
|
t.Errorf("local kind = %q, want interpres.LocalDateTime", cfg.L.Kind)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-21 23:49:39 +02:00
|
|
|
|
|
|
|
|
func TestDecoderUseNumber(t *testing.T) {
|
|
|
|
|
data := []byte(`hex = 0x1f
|
|
|
|
|
sep = 1_000
|
|
|
|
|
signed = +1.0
|
|
|
|
|
exp = 1e6
|
|
|
|
|
posinf = inf
|
|
|
|
|
negzero = -0.0
|
|
|
|
|
plain = 42
|
|
|
|
|
frac = 2.5
|
|
|
|
|
`)
|
|
|
|
|
t.Run("the tree keeps the literal", func(t *testing.T) {
|
|
|
|
|
var tree map[string]any
|
|
|
|
|
if err := NewDecoder().UseNumber().Decode(data, &tree); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for lit, key := range map[string]string{
|
|
|
|
|
"0x1f": "hex", "1_000": "sep", "+1.0": "signed", "1e6": "exp",
|
|
|
|
|
"inf": "posinf", "-0.0": "negzero", "42": "plain", "2.5": "frac",
|
|
|
|
|
} {
|
|
|
|
|
got, ok := tree[key].(Number)
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Errorf("%s = %T, want Number", key, tree[key])
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if string(got) != lit {
|
|
|
|
|
t.Errorf("%s = %q, want %q", key, got, lit)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
t.Run("typed fields take the evaluated value", func(t *testing.T) {
|
|
|
|
|
var cfg struct {
|
|
|
|
|
Hex Number `toml:"hex"`
|
|
|
|
|
Plain int64 `toml:"plain"`
|
|
|
|
|
Frac float64 `toml:"frac"`
|
|
|
|
|
Rate time.Duration
|
|
|
|
|
}
|
|
|
|
|
dec := NewDecoder().UseNumber()
|
|
|
|
|
if err := dec.Decode([]byte("hex = 0x1f\nplain = 42\nfrac = 2.5\nRate = 1_000\n"), &cfg); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if cfg.Hex != "0x1f" {
|
|
|
|
|
t.Errorf("hex = %q, want 0x1f", cfg.Hex)
|
|
|
|
|
}
|
|
|
|
|
if cfg.Plain != 42 {
|
|
|
|
|
t.Errorf("plain = %d, want 42", cfg.Plain)
|
|
|
|
|
}
|
|
|
|
|
if cfg.Frac != 2.5 {
|
|
|
|
|
t.Errorf("frac = %g, want 2.5", cfg.Frac)
|
|
|
|
|
}
|
|
|
|
|
if cfg.Rate != 1000 {
|
|
|
|
|
t.Errorf("rate = %s, want 1µs", cfg.Rate)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
t.Run("invalid numbers are still parse errors", func(t *testing.T) {
|
|
|
|
|
for _, in := range []string{"a = 01\n", "a = 1__0\n", "a = 1x\n"} {
|
|
|
|
|
var tree map[string]any
|
|
|
|
|
if err := NewDecoder().UseNumber().Decode([]byte(in), &tree); err == nil {
|
|
|
|
|
t.Errorf("%q decoded without an error", in)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
t.Run("without UseNumber the tree holds the evaluated kinds", func(t *testing.T) {
|
|
|
|
|
var tree map[string]any
|
|
|
|
|
if err := NewDecoder().Decode([]byte("hex = 0x1f\nfrac = 2.5\n"), &tree); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := tree["hex"].(int64); !ok || v != 31 {
|
|
|
|
|
t.Errorf("hex = %#v, want int64 31", tree["hex"])
|
|
|
|
|
}
|
|
|
|
|
if v, ok := tree["frac"].(float64); !ok || v != 2.5 {
|
|
|
|
|
t.Errorf("frac = %#v, want float64 2.5", tree["frac"])
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNumberMethods(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
lit Number
|
|
|
|
|
wantI int64
|
|
|
|
|
wantF float64
|
|
|
|
|
intErr bool
|
|
|
|
|
}{
|
|
|
|
|
{lit: "42", wantI: 42, wantF: 42},
|
|
|
|
|
{lit: "0x1f", wantI: 31, wantF: 31},
|
|
|
|
|
{lit: "1_000", wantI: 1000, wantF: 1000},
|
|
|
|
|
{lit: "+1.0", wantF: 1, intErr: true},
|
|
|
|
|
{lit: "1e6", wantF: 1e6, intErr: true},
|
|
|
|
|
{lit: "inf", wantF: math.Inf(1), intErr: true},
|
|
|
|
|
{lit: "-2.5", wantF: -2.5, intErr: true},
|
|
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
i, err := tt.lit.Int64()
|
|
|
|
|
if tt.intErr && err == nil {
|
|
|
|
|
t.Errorf("%q.Int64() succeeded with %d, want an error", tt.lit, i)
|
|
|
|
|
}
|
|
|
|
|
if !tt.intErr {
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("%q.Int64() = %v", tt.lit, err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if i != tt.wantI {
|
|
|
|
|
t.Errorf("%q.Int64() = %d, want %d", tt.lit, i, tt.wantI)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
f, err := tt.lit.Float64()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("%q.Float64() = %v", tt.lit, err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if f != tt.wantF {
|
|
|
|
|
t.Errorf("%q.Float64() = %g, want %g", tt.lit, f, tt.wantF)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, lit := range []Number{"01", "1__0", "abc", ""} {
|
|
|
|
|
if _, err := lit.Float64(); err == nil {
|
|
|
|
|
t.Errorf("%q.Float64() succeeded, want an error", lit)
|
|
|
|
|
}
|
|
|
|
|
if _, err := lit.Int64(); err == nil {
|
|
|
|
|
t.Errorf("%q.Int64() succeeded, want an error", lit)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-21 23:55:58 +02:00
|
|
|
|
|
|
|
|
func TestSyntaxErrorPosition(t *testing.T) {
|
|
|
|
|
src := []byte("alpha = 1\nbeta x = 2\n")
|
|
|
|
|
_, err := ParseMap(src)
|
|
|
|
|
se, ok := errors.AsType[*SyntaxError](err)
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatalf("err = %v, want a SyntaxError", err)
|
|
|
|
|
}
|
|
|
|
|
if se.Line != 2 {
|
|
|
|
|
t.Errorf("Line = %d, want 2", se.Line)
|
|
|
|
|
}
|
|
|
|
|
if want := strings.Index(string(src), "x"); se.Offset != want {
|
|
|
|
|
t.Errorf("Offset = %d, want %d", se.Offset, want)
|
|
|
|
|
}
|
|
|
|
|
if se.Column != 6 {
|
|
|
|
|
t.Errorf("Column = %d, want 6", se.Column)
|
|
|
|
|
}
|
|
|
|
|
want := "beta x = 2\n ^"
|
|
|
|
|
if got := se.SourceLine(src); got != want {
|
|
|
|
|
t.Errorf("SourceLine =\n%s\nwant:\n%s", got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSyntaxErrorUTF8Offset(t *testing.T) {
|
|
|
|
|
src := []byte("a = \"ok\"\nb = \"\xff\xfe\"\n")
|
|
|
|
|
_, err := ParseMap(src)
|
|
|
|
|
se, ok := errors.AsType[*SyntaxError](err)
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatalf("err = %v, want a SyntaxError", err)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(se.Msg, "byte offset 14") {
|
|
|
|
|
t.Errorf("Msg = %q, want it to name byte offset 14", se.Msg)
|
|
|
|
|
}
|
|
|
|
|
if se.Offset != 14 {
|
|
|
|
|
t.Errorf("Offset = %d, want 14", se.Offset)
|
|
|
|
|
}
|
|
|
|
|
want := "b = \"\xff\xfe\"\n ^"
|
|
|
|
|
if got := se.SourceLine(src); got != want {
|
|
|
|
|
t.Errorf("SourceLine =\n%q\nwant:\n%q", got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSourceLineEdgePositions(t *testing.T) {
|
|
|
|
|
src := []byte("a = 1\n")
|
|
|
|
|
e := &SyntaxError{Line: 1, Msg: "no position"}
|
|
|
|
|
if got, want := e.SourceLine(src), "a = 1\n^"; got != want {
|
|
|
|
|
t.Errorf("SourceLine(zero offset) =\n%q\nwant:\n%q", got, want)
|
|
|
|
|
}
|
|
|
|
|
e = &SyntaxError{Line: 2, Offset: 100, Msg: "past the end"}
|
|
|
|
|
if got, want := e.SourceLine(src), "\n^"; got != want {
|
|
|
|
|
t.Errorf("SourceLine(offset past end) =\n%q\nwant:\n%q", got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|