875 lines
27 KiB
Go
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)
|
|
}
|
|
}
|