Files
interpres/target_test.go
2026-09-22 21:15:00 +02:00

875 lines
27 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package interpres
import (
"errors"
"maps"
"net"
"reflect"
"strings"
"sync/atomic"
"testing"
"time"
)
type targetNested struct {
X int `toml:"x"`
Y string `toml:"y"`
}
type targetCfg struct {
Num int `toml:"num"`
Small uint8 `toml:"small"`
Tags []string `toml:"tags"`
Lims map[string]any `toml:"lims"`
Tab targetNested `toml:"tab"`
Arr []targetNested `toml:"arr"`
Other string `toml:"other"`
}
// TestTargetedStrictFindings pins the strict findings of the targeted parse
// to the tree decode's own texts, paths included. Every case here was first
// surfaced by FuzzTargetedDecode.
func TestTargetedStrictFindings(t *testing.T) {
tests := []struct {
name string
doc string
want string
}{
{
name: "unknown key in a header table",
doc: "[tab]\nother = \"o\"\n",
want: `interpres: tab: unknown field "other" for interpres.targetNested`,
},
{
name: "unknown nested header without the parent header",
doc: "[tab.nested]\nx = 1\n",
want: `interpres: tab: unknown field "nested" for interpres.targetNested`,
},
{
name: "unknown key in an array-of-tables element",
doc: "[[arr]]\nother = \"o\"\n",
want: `interpres: arr[0]: unknown field "other" for interpres.targetNested`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cfg targetCfg
err := Unmarshal([]byte(tt.doc), &cfg, RejectUnknownFields(true))
if err == nil {
t.Fatalf("no error, want %q", tt.want)
}
if err.Error() != tt.want {
t.Errorf("message = %q, want %q", err.Error(), tt.want)
}
})
}
}
// TestTargetedParseErrors pins the parse-stage errors the targeted skeleton
// raises, whose texts and lines are the tree parser's own.
func TestTargetedParseErrors(t *testing.T) {
tests := []struct {
name string
doc string
want string
}{
{
name: "header on an assigned scalar",
doc: "zz = 1\n[zz]\nx = 4\n",
want: "interpres: line 2: key \"zz\" is not a table",
},
{
name: "dotted key on an assigned scalar",
doc: "zz = 1\nzz.x = 2\n",
want: "interpres: line 2: key \"zz\" is not a table",
},
{
name: "duplicate unknown keys",
doc: "zz = 1\nzz = 2\n",
want: "interpres: line 2: duplicate key \"zz\"",
},
{
name: "duplicate inside an unknown table",
doc: "[zz]\nk = 1\nk = 2\n",
want: "interpres: line 3: duplicate key \"k\"",
},
{
name: "duplicate across a sink's dotted keys",
doc: "[zz]\na.b = 1\na.b = 2\n",
want: "interpres: line 3: duplicate key \"b\"",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cfg targetCfg
err := Unmarshal([]byte(tt.doc), &cfg)
if err == nil {
t.Fatalf("no error, want %q", tt.want)
}
if err.Error() != tt.want {
t.Errorf("message = %q, want %q", err.Error(), tt.want)
}
})
}
}
// TestTargetedSilentShapes covers the documents the targeted parse accepts
// with the values the tree decode gives.
func TestTargetedSilentShapes(t *testing.T) {
t.Run("dotted key after an unknown nested header", func(t *testing.T) {
// [tab.nested] is unknown and sinks; tab.x then lands in tab, and the
// sink's own x is a different key, the tree's shape exactly.
var cfg, ref targetCfg
in := []byte("[tab]\nx = 1\n[tab.nested]\n")
if err := Unmarshal(in, &cfg); err != nil {
t.Fatalf("decode: %v", err)
}
if err := treeDecodeInto(in, &ref); err != nil {
t.Fatalf("reference: %v", err)
}
if !reflect.DeepEqual(cfg, ref) {
t.Errorf("values disagree: targeted %+v, tree %+v", cfg, ref)
}
if cfg.Tab.X != 1 {
t.Errorf("tab.x = %d, want 1", cfg.Tab.X)
}
})
t.Run("unknown keys are ignored without strict", func(t *testing.T) {
var cfg, ref targetCfg
in := []byte("num = 5\nz1 = 1\n[zz]\nk = 1\n")
if err := Unmarshal(in, &cfg); err != nil {
t.Fatalf("decode: %v", err)
}
if err := treeDecodeInto(in, &ref); err != nil {
t.Fatalf("reference: %v", err)
}
if !reflect.DeepEqual(cfg, ref) {
t.Errorf("values disagree: targeted %+v, tree %+v", cfg, ref)
}
if cfg.Num != 5 {
t.Errorf("num = %d, want 5", cfg.Num)
}
})
t.Run("an inline table into a map field", func(t *testing.T) {
var cfg targetCfg
in := []byte("lims = { cpu = 4, deep = { a = true } }\n")
if err := Unmarshal(in, &cfg); err != nil {
t.Fatalf("decode: %v", err)
}
if cfg.Lims["cpu"] != int64(4) {
t.Errorf("lims = %v", cfg.Lims)
}
})
t.Run("an overflow falls back to the decode error", func(t *testing.T) {
var cfg targetCfg
err := Unmarshal([]byte("small = 300\n"), &cfg)
want := "interpres: small: integer 300 overflows uint8"
if err == nil || err.Error() != want {
t.Errorf("err = %v, want %q", err, want)
}
})
t.Run("too many array-of-tables elements falls back", func(t *testing.T) {
type Item struct {
N int `toml:"n"`
}
var cfg struct {
Items [2]Item `toml:"items"`
}
err := Unmarshal([]byte("[[items]]\nn = 1\n[[items]]\nn = 2\n[[items]]\nn = 3\n"), &cfg)
want := "interpres: items: cannot assign 3 elements to [2]interpres.Item"
if err == nil || err.Error() != want {
t.Errorf("err = %v, want %q", err, want)
}
})
t.Run("a UseNumber tree keeps literals in the targeted path", func(t *testing.T) {
var cfg struct {
Rate Number `toml:"rate"`
}
if err := Unmarshal([]byte("rate = 1_000\n"), &cfg, NumbersAsLiterals(true)); err != nil {
t.Fatal(err)
}
if cfg.Rate != "1_000" {
t.Errorf("rate = %q, want 1_000", cfg.Rate)
}
})
t.Run("dotted keys fill a map field", func(t *testing.T) {
var cfg targetCfg
in := []byte("lims.a.b = true\nlims.c = 3\n")
if err := Unmarshal(in, &cfg); err != nil {
t.Fatalf("decode: %v", err)
}
if cfg.Lims["c"] != int64(3) {
t.Errorf("lims = %v", cfg.Lims)
}
})
t.Run("an inline table cannot be extended", func(t *testing.T) {
var cfg targetCfg
in := []byte("lims = { a = 1 }\n[lims.deep]\nb = 2\n")
err := Unmarshal(in, &cfg)
if err == nil || !strings.Contains(err.Error(), "cannot extend inline table") {
t.Errorf("err = %v, want the inline-table extension error", err)
}
})
}
// TestTargetedShapesMatrix walks a document per destination shape, both
// through the targeted path and the tree reference, so the two agree on
// every branch the skeleton carries.
func TestTargetedShapesMatrix(t *testing.T) {
docs := []string{
// Scalars of every kind, arrays, maps, tables, arrays of tables.
"num = 7\nflt = 1.25\nstr = \"s\"\nflag = false\nsmall = 9\ntags = [\"a\"]\nlims = { a = 1 }\n\n[tab]\nx = 1\ny = \"t\"\n\n[[arr]]\nx = 2\ny = \"u\"\n\n[[arr]]\nx = 3\ny = \"v\"\n",
// Dotted keys through nested tables and maps.
"tab.x = 1\ntab.y = \"s\"\nlims.a.b = true\nlims.c = 3\nnum = 2\n",
// Inline tables nested in arrays, mixed value arrays.
"lims = { a = { b = 1 } }\ntags = []\nother = \"o\"\n",
// A sub-table of an array of tables, then a second element.
"[[arr]]\nx = 1\n[arr.nested]\ny = \"n\"\n[[arr]]\ny = \"m\"\n",
// Negative and signed numbers, exponents, radix forms into floats.
"flt = -3.5e2\nnum = -42\nflt = +1.0\n",
// A quoted key and a defined-string-shaped value.
"\"quoted key\" = 1\nstr = \"multi\"\n",
}
for i, doc := range docs {
var ref, tgt targetCfg
refErr := treeDecodeInto([]byte(doc), &ref)
tgtErr := Unmarshal([]byte(doc), &tgt)
if (refErr == nil) != (tgtErr == nil) {
t.Errorf("doc %d: error presence disagrees: tree %v, targeted %v", i, refErr, tgtErr)
continue
}
if refErr != nil {
continue
}
if !reflect.DeepEqual(ref, tgt) {
t.Errorf("doc %d: values disagree:\ntree: %#v\ntargeted: %#v", i, ref, tgt)
}
}
}
// TestTargetedFallbackContracts pins the documents that must fall back and
// produce the tree decode's exact error.
func TestTargetedFallbackContracts(t *testing.T) {
type Item struct {
N int `toml:"n"`
}
tests := []struct {
name string
doc string
want string
}{
{
name: "uint8 overflow",
doc: "small = 300\n",
want: "interpres: small: integer 300 overflows uint8",
},
{
name: "negative into uint",
doc: "small = -1\n",
want: "interpres: small: cannot assign negative -1 to uint8",
},
{
name: "a table into a scalar",
doc: "num = { a = 1 }\n",
want: "interpres: num: cannot assign table to int",
},
{
name: "an integer into a string field",
doc: "other = 5\n",
want: "interpres: other: cannot assign integer to string",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cfg targetCfg
err := Unmarshal([]byte(tt.doc), &cfg)
if err == nil || err.Error() != tt.want {
t.Errorf("err = %v, want %q", err, tt.want)
}
})
}
_ = Item{}
}
// TestTargetedHeaderOnAssignedScalar pins the parse error a header raises
// when the key already holds a scalar, before any fallback can happen.
func TestTargetedHeaderOnAssignedScalarArray(t *testing.T) {
var cfg targetCfg
in := []byte("arr = []\n[[arr]]\nx = 1\n")
err := Unmarshal(in, &cfg)
want := "interpres: line 2: key \"arr\" is not an array of tables"
if err == nil || err.Error() != want {
t.Errorf("err = %v, want %q", err, want)
}
}
// TestTargetedBranchParity walks the fallback branches of the targeted
// skeleton: every document here takes the tree path on a rerun, and must
// carry the tree decode's exact error text.
func TestTargetedBranchParity(t *testing.T) {
tests := []struct {
name string
doc string
want string
}{
{
name: "a header over a value array",
doc: "tags = [\"x\"]\n[tags]\na = 1\n",
want: "interpres: line 2: key \"tags\" is not a table",
},
{
name: "an array header over a value array",
doc: "tags = [\"x\"]\n[[tags]]\na = 1\n",
want: "interpres: line 2: key \"tags\" is not an array of tables",
},
{
name: "an array header over a datetime field",
doc: "when = 1979-05-27T07:32:00Z\n[[when]]\nx = 1\n",
want: "interpres: line 2: key \"when\" is not an array of tables",
},
{
name: "a boolean into a string field",
doc: "other = true\n",
want: "interpres: other: cannot assign bool to string",
},
{
name: "a leading-zero integer",
doc: "num = 01\n",
want: "interpres: line 1: leading zeros are not allowed in numbers",
},
{
name: "an int64-overflowing integer",
doc: "num = 99999999999999999999\n",
want: "interpres: line 1: integer \"99999999999999999999\" out of range",
},
{
name: "a malformed boolean",
doc: "flag = tru\n",
want: "interpres: line 1: invalid value",
},
{
name: "a negative number into an unsigned field",
doc: "small = -5\n",
want: "interpres: small: cannot assign negative -5 to uint8",
},
{
name: "an integer into a string field via the generic path",
doc: "other = 5\n",
want: "interpres: other: cannot assign integer to string",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cfg targetCfg
err := Unmarshal([]byte(tt.doc), &cfg, RejectUnknownFields(true))
if tt.want == "" {
if err != nil {
t.Fatalf("err = %v, want nil", err)
}
return
}
if err == nil || err.Error() != tt.want {
t.Errorf("err = %v, want %q", err, tt.want)
}
})
}
}
// TestTargetedDecodeHookFields keeps the custom decode hooks of scalar-typed
// fields working in the targeted path.
func TestTargetedDecodeHookFields(t *testing.T) {
type Cfg struct {
IP net.IP `toml:"ip"`
Dur time.Duration `toml:"dur"`
Unm *scalarUnmarshaler `toml:"unm"`
}
var cfg Cfg
in := []byte("ip = \"192.0.2.1\"\ndur = \"1h30m\"\nunm = \"hello\"\n")
if err := Unmarshal(in, &cfg); err != nil {
t.Fatal(err)
}
if cfg.IP.String() != "192.0.2.1" {
t.Errorf("ip = %v", cfg.IP)
}
if cfg.Dur != 90*time.Minute {
t.Errorf("dur = %v", cfg.Dur)
}
if cfg.Unm == nil || cfg.Unm.val != "hello" {
t.Errorf("unm = %+v", cfg.Unm)
}
}
// TestTargetedOddShapes pins the fallback and value shapes the matrix does
// not reach: space-separated date-times, non-string map keys and repeated
// dotted map keys.
func TestTargetedOddShapes(t *testing.T) {
t.Run("a space-separated date-time", func(t *testing.T) {
type Cfg struct {
When time.Time `toml:"when"`
}
var cfg, ref Cfg
doc := []byte("when = 1979-05-27 07:32:00Z\n")
if err := Unmarshal(doc, &cfg); err != nil {
t.Fatal(err)
}
if err := treeDecodeInto(doc, &ref); err != nil {
t.Fatal(err)
}
if !cfg.When.Equal(ref.When) {
t.Errorf("when = %v, want %v", cfg.When, ref.When)
}
})
t.Run("a map with a non-string key falls back", func(t *testing.T) {
type Cfg struct {
M map[int]string `toml:"m"`
}
var cfg, ref Cfg
doc := []byte("m = { a = 1 }\n")
err := Unmarshal(doc, &cfg)
refErr := treeDecodeInto(doc, &ref)
if err == nil || refErr == nil {
t.Fatalf("err = %v, refErr = %v, want both to fail", err, refErr)
}
if err.Error() != refErr.Error() {
t.Errorf("errors disagree: targeted %q, tree %q", err, refErr)
}
})
t.Run("a repeated dotted map key is a duplicate", func(t *testing.T) {
var cfg targetCfg
doc := []byte("lims.a.b = 1\nlims.a.b = 2\n")
err := Unmarshal(doc, &cfg)
want := "interpres: line 2: duplicate key \"b\""
if err == nil || err.Error() != want {
t.Errorf("err = %v, want %q", err, want)
}
})
t.Run("an underscored integer takes the token path", func(t *testing.T) {
var cfg targetCfg
doc := []byte("num = 1_000\n")
if err := Unmarshal(doc, &cfg); err != nil {
t.Fatal(err)
}
if cfg.Num != 1000 {
t.Errorf("num = %d, want 1000", cfg.Num)
}
})
t.Run("an 18-digit integer takes the fast path", func(t *testing.T) {
var cfg struct {
Big int64 `toml:"big"`
}
doc := []byte("big = 999999999999999999\n")
if err := Unmarshal(doc, &cfg); err != nil {
t.Fatal(err)
}
if cfg.Big != 999999999999999999 {
t.Errorf("big = %d", cfg.Big)
}
})
}
// TestTargetedMapTableShapes covers the map-entry branches of the targeted
// skeleton: entries that become tables, entries that refuse them, and the
// duplicate checks across them.
func TestTargetedMapTableShapes(t *testing.T) {
tests := []struct {
name string
doc string
want string
}{
{
name: "a header opens a map entry table",
doc: "lims.c = 1\n[lims.d]\nk = 1\n",
want: "",
},
{
name: "a header over an assigned map entry",
doc: "lims.a = 1\n[lims.a]\nk = 1\n",
want: "interpres: line 2: key \"a\" is not a table",
},
{
name: "a dotted key over an assigned map entry",
doc: "lims.a = 1\nlims.a.b = 2\n",
want: "interpres: line 2: key \"a\" is not a table",
},
{
name: "a duplicate plain map entry",
doc: "lims.a = 1\nlims.a = 2\n",
want: "interpres: line 2: duplicate key \"a\"",
},
{
name: "an array of tables inside a map entry",
doc: "lims.c = 1\n[[lims.items]]\nk = 1\n",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cfg, ref targetCfg
err := Unmarshal([]byte(tt.doc), &cfg)
refErr := treeDecodeInto([]byte(tt.doc), &ref)
if (err == nil) != (refErr == nil) {
t.Fatalf("error presence disagrees: tree %v, targeted %v", refErr, err)
}
if err != nil {
if err.Error() != refErr.Error() {
t.Fatalf("errors disagree:\ntree: %v\ntargeted: %v", refErr, err)
}
return
}
if !reflect.DeepEqual(cfg, ref) {
t.Errorf("values disagree: targeted %+v, tree %+v", cfg, ref)
}
})
}
}
// TestTargetedNestedMapDescents pins the descents into a map of maps that
// meet entries the document built earlier: a dotted key twice through the
// same sub-table, a header into a dotted-built sub-table, and a typed array
// under a map key. Each shape once panicked on a reflect Elem of a map.
func TestTargetedNestedMapDescents(t *testing.T) {
t.Run("dotted key through one sub-table twice", func(t *testing.T) {
var cfg struct {
M map[string]map[string]any `toml:"m"`
}
err := Unmarshal([]byte("m.a.b = 1\nm.a.c = 2\n"), &cfg)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.M["a"]["b"] != int64(1) || cfg.M["a"]["c"] != int64(2) {
t.Errorf("m = %#v", cfg.M)
}
})
t.Run("header under a dotted-built sub-table", func(t *testing.T) {
var cfg struct {
M map[string]map[string]any `toml:"m"`
}
err := Unmarshal([]byte("m.a.b = 1\n[m.a.deep]\nx = 2\n"), &cfg)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.M["a"]["b"] != int64(1) || cfg.M["a"]["deep"].(map[string]any)["x"] != int64(2) {
t.Errorf("m = %#v", cfg.M)
}
})
t.Run("typed array under a map key", func(t *testing.T) {
var cfg struct {
M map[string][]map[string]any `toml:"m"`
}
err := Unmarshal([]byte("[[m.arr]]\nx = 1\n\n[[m.arr]]\ny = 2\n"), &cfg)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(cfg.M["arr"]) != 2 || cfg.M["arr"][1]["y"] != int64(2) {
t.Errorf("m = %#v", cfg.M)
}
})
}
// TestTargetedPointerElementSlice pins that an array of tables over a slice
// of pointer elements fills the pointed-to structs.
func TestTargetedPointerElementSlice(t *testing.T) {
type item struct {
N int `toml:"n"`
}
var cfg struct {
Items []*item `toml:"items"`
}
err := Unmarshal([]byte("[[items]]\nn = 1\n\n[[items]]\nn = 2\n"), &cfg)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(cfg.Items) != 2 || cfg.Items[0] == nil || cfg.Items[1].N != 2 {
t.Errorf("items = %#v", cfg.Items)
}
}
// TestTargetedArrayScopeResets pins that a new element of an array of tables
// starts a fresh definition scope, the contract the changelog documents.
func TestTargetedArrayScopeResets(t *testing.T) {
doc := "[[a]]\nb.c = 1\n\n[[a]]\n\n[a.b]\nx = 1\n"
var ref, tgt targetCfg
refErr := treeDecodeInto([]byte(doc), &ref)
if refErr != nil {
t.Fatalf("tree decode: %v", refErr)
}
if err := Unmarshal([]byte(doc), &tgt); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !reflect.DeepEqual(ref, tgt) {
t.Errorf("targeted = %#v, tree = %#v", tgt, ref)
}
}
// TestTargetedUnknownArrayElements pins that every element of an unknown
// array of tables is a fresh namespace, and a sub-table header reaches the
// last element the way the tree parser's does.
func TestTargetedUnknownArrayElements(t *testing.T) {
doc := "[[zz]]\nk = 1\n\n[[zz]]\nk = 2\n\n[zz.sub]\nx = 3\n"
var ref, tgt targetCfg
refErr := treeDecodeInto([]byte(doc), &ref)
tgtErr := Unmarshal([]byte(doc), &tgt)
if (refErr == nil) != (tgtErr == nil) {
t.Fatalf("error presence disagrees: tree %v, targeted %v", refErr, tgtErr)
}
if refErr != nil {
return
}
if !reflect.DeepEqual(ref, tgt) {
t.Errorf("targeted = %#v, tree = %#v", tgt, ref)
}
// A dotted key may not enter the array: the tree's own rule.
var dotted targetCfg
dErr := Unmarshal([]byte("[[zz]]\nk = 1\nzz.x = 2\n"), &dotted)
refDotted := treeDecodeInto([]byte("[[zz]]\nk = 1\nzz.x = 2\n"), &dotted)
if (dErr == nil) != (refDotted == nil) {
t.Errorf("dotted into an array: targeted %v, tree %v", dErr, refDotted)
}
}
// TestTargetedFixedArrayUnderFill pins that a fixed-size array the document
// under-fills is the length mismatch the tree decode raises, with the
// field's path.
func TestTargetedFixedArrayUnderFill(t *testing.T) {
type item struct {
N int `toml:"n"`
}
var cfg struct {
Items [2]item `toml:"items"`
}
err := Unmarshal([]byte("[[items]]\nn = 1\n"), &cfg)
if err == nil {
t.Fatal("unmarshal accepted an under-filled array")
}
want := `interpres: items: cannot assign 1 elements to [2]interpres.item`
if err.Error() != want {
t.Errorf("err = %v\nwant %q", err, want)
}
}
// TestTargetedPrefilledSliceReplaced pins that a prefilled slice is replaced
// by the document's elements on both paths, not appended to.
func TestTargetedPrefilledSliceReplaced(t *testing.T) {
type item struct {
N int `toml:"n"`
}
doc := []byte("[[items]]\nn = 1\n")
var ref struct {
Items []item `toml:"items"`
}
ref.Items = []item{{N: 9}}
if err := treeDecodeInto(doc, &ref); err != nil {
t.Fatalf("tree decode: %v", err)
}
var tgt struct {
Items []item `toml:"items"`
}
tgt.Items = []item{{N: 9}}
if err := Unmarshal(doc, &tgt); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !reflect.DeepEqual(ref, tgt) {
t.Errorf("targeted = %#v, tree = %#v", tgt, ref)
}
if len(tgt.Items) != 1 || tgt.Items[0].N != 1 {
t.Errorf("items = %#v, want the prefilled element replaced", tgt.Items)
}
}
// TestTargetedHeaderOverValueArrayKeepsCase pins that a value array assigned
// under a differently cased key than the field's name still blocks the
// array-of-tables header over it, the tree parse error.
func TestTargetedHeaderOverValueArrayKeepsCase(t *testing.T) {
var cfg struct {
Arr []targetNested `toml:"arr"`
}
err := Unmarshal([]byte("Arr = [{x = 1}]\n[[Arr]]\nx = 2\n"), &cfg)
if err == nil || err.Error() != `interpres: line 2: key "Arr" is not an array of tables` {
t.Errorf("err = %v, want the parse error over the assigned field", err)
}
}
// TestTargetedDottedInlineFreezePath pins that an inline table assigned by a
// dotted key freezes the whole path the statement wrote: a later header
// under that path is the extension error, and a key outside it stays free.
func TestTargetedDottedInlineFreezePath(t *testing.T) {
var cfg targetCfg
err := Unmarshal([]byte("m.a.b = {x = 1}\nb.y = 2\n"), &cfg)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
err = Unmarshal([]byte("m.a.b = {x = 1}\n[m.a.b]\ny = 2\n"), &cfg)
want := `interpres: line 2: cannot extend inline table "m.a.b"`
if err == nil || err.Error() != want {
t.Errorf("err = %v\nwant %q", err, want)
}
}
// TestTargetedStrictThroughDottedKeys pins that strict and required findings
// survive the transient tables a dotted descent builds.
func TestTargetedStrictThroughDottedKeys(t *testing.T) {
var cfg targetCfg
err := Unmarshal([]byte("tab.zz = 1\n"), &cfg, RejectUnknownFields(true))
if err == nil || !strings.Contains(err.Error(), `unknown field "zz"`) {
t.Errorf("err = %v, want the strict failure through the dotted key", err)
}
if err == nil || !strings.HasPrefix(err.Error(), "interpres: tab:") {
t.Errorf("err = %v, want the path through the dotted key", err)
}
}
// TestTargetedRequiredThroughDottedKeys pins that a required tag is honoured
// when the table is reached only through dotted keys.
func TestTargetedRequiredThroughDottedKeys(t *testing.T) {
type nested struct {
X int `toml:"x,required"`
Y int `toml:"y"`
}
var cfg struct {
Tab nested `toml:"tab"`
}
err := Unmarshal([]byte("tab.y = 1\n"), &cfg)
if err == nil || !strings.Contains(err.Error(), `missing required key "x"`) {
t.Errorf("err = %v, want the missing required key through the dotted key", err)
}
}
// TestTargetedOrderedMapSliceFallsBack pins that a slice of OrderedMap
// elements takes the tree path, whose fill keeps the written order.
func TestTargetedOrderedMapSliceFallsBack(t *testing.T) {
var cfg struct {
Items []OrderedMap `toml:"items"`
}
err := Unmarshal([]byte("[[items]]\nk = \"v\"\n"), &cfg)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(cfg.Items) != 1 || cfg.Items[0].Keys()[0] != "k" {
t.Errorf("items = %#v, want the element filled in written order", cfg.Items)
}
}
// hookMap is a named map type whose decode hook counts its calls.
type hookMap map[string]any
var hookMapCalls atomic.Int32
func (h *hookMap) UnmarshalTOML(data any) error {
hookMapCalls.Add(1)
m, _ := data.(map[string]any)
if *h == nil {
*h = hookMap{}
}
maps.Copy((*h), m)
return nil
}
// TestTargetedMapFieldHookGetsWholeTable pins that a named map field with a
// decode hook receives the whole parsed table, even in its header form.
func TestTargetedMapFieldHookGetsWholeTable(t *testing.T) {
type cfg struct {
M hookMap `toml:"m"`
}
var c cfg
hookMapCalls.Store(0)
err := Unmarshal([]byte("[m]\na = 1\nb = 2\n"), &c)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if hookMapCalls.Load() != 1 {
t.Errorf("hook calls = %d, want exactly one with the whole table", hookMapCalls.Load())
}
if c.M["a"] != int64(1) || c.M["b"] != int64(2) {
t.Errorf("m = %#v", c.M)
}
}
// errHook fails every decode with a fixed error and counts its calls.
type errHook struct{ calls *int }
func (e *errHook) UnmarshalTOML(any) error {
if e.calls != nil {
*e.calls++
}
return errors.New("boom")
}
// TestTargetedHookErrorRunsOnce pins that a failing hook's error is the
// tree path's own, wrapped with the key, and that the hook is not run a
// second time by a fallback.
func TestTargetedHookErrorRunsOnce(t *testing.T) {
calls := 0
cfg := struct {
F errHook `toml:"f"`
}{F: errHook{calls: &calls}}
err := Unmarshal([]byte("f = 1\n"), &cfg)
if err == nil || err.Error() != "interpres: f: unmarshal: boom" {
t.Errorf("err = %v, want the wrapped hook failure", err)
}
if calls != 1 {
t.Errorf("hook calls = %d, want one", calls)
}
}
// TestTargetedUnknownBeforeRequired pins the report order the tree decode
// produces: an unknown key wins over a missing required one.
func TestTargetedUnknownBeforeRequired(t *testing.T) {
type inner struct {
X int `toml:"x,required"`
}
var cfg struct {
Tab inner `toml:"tab"`
}
err := Unmarshal([]byte("[tab]\nzz = 1\n"), &cfg, RejectUnknownFields(true))
if err == nil || !strings.Contains(err.Error(), `unknown field "zz"`) {
t.Errorf("err = %v, want the unknown key reported before the required one", err)
}
}
// TestTargetedStrictPathStableAcrossHeaders pins that the path a strict
// finding wraps does not alias the parser's key buffer: the table that owns
// the unknown key keeps its name after a later header.
func TestTargetedStrictPathStableAcrossHeaders(t *testing.T) {
var cfg targetCfg
err := Unmarshal([]byte("[tab]\nzz = 1\n\n[lims]\nx = 1\n"), &cfg, RejectUnknownFields(true))
if err == nil || !strings.HasPrefix(err.Error(), "interpres: tab:") {
t.Errorf("err = %v, want the finding on tab, not the later header", err)
}
}
// TestTargetedPrefilledMapFieldMergesUnderHeader pins that a prefilled map
// field merges the document's header-form table into it on both paths, the
// rule the root map has always followed.
func TestTargetedPrefilledMapFieldMergesUnderHeader(t *testing.T) {
doc := []byte("[lims]\nnew = 3\n")
var ref, tgt targetCfg
ref.Lims = map[string]any{"keep": "yes"}
if err := treeDecodeInto(doc, &ref); err != nil {
t.Fatalf("tree decode: %v", err)
}
tgt.Lims = map[string]any{"keep": "yes"}
if err := Unmarshal(doc, &tgt); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !reflect.DeepEqual(ref, tgt) {
t.Errorf("targeted = %#v, tree = %#v", tgt, ref)
}
if tgt.Lims["keep"] != "yes" || tgt.Lims["new"] != int64(3) {
t.Errorf("lims = %#v, want the merge", tgt.Lims)
}
}
// TestTargetedNumberTokenValidatesUTF8 pins that the token route the
// targeted parse takes reports invalid UTF-8 with the scanner's own message
// and position.
func TestTargetedNumberTokenValidatesUTF8(t *testing.T) {
var cfg targetCfg
err := Unmarshal([]byte("num = 12\xff\n"), &cfg)
if err == nil || !strings.Contains(err.Error(), "invalid UTF-8 in value at byte offset 8") {
t.Errorf("err = %v, want the UTF-8 complaint on the invalid byte", err)
}
}