526 lines
15 KiB
Go
526 lines
15 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package interpres
|
|
|
|
import (
|
|
"net"
|
|
"reflect"
|
|
"strings"
|
|
"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 := NewDecoder().DisallowUnknownFields().Decode([]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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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 := NewDecoder().Decode([]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 := NewDecoder().Decode(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 := NewDecoder().Decode(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 := NewDecoder().Decode(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 := NewDecoder().Decode([]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 := NewDecoder().UseNumber().Decode([]byte("rate = 1_000\n"), &cfg); 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 := NewDecoder().Decode(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 := NewDecoder().Decode(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)
|
|
dec := NewDecoder()
|
|
tgtErr := dec.Decode([]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 := NewDecoder().Decode([]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 := NewDecoder().Decode(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 := NewDecoder().DisallowUnknownFields().Decode([]byte(tt.doc), &cfg)
|
|
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 := NewDecoder().Decode(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 := NewDecoder().Decode(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 := NewDecoder().Decode(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 := NewDecoder().Decode(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 := NewDecoder().Decode(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 := NewDecoder().Decode(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 := NewDecoder().Decode([]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)
|
|
}
|
|
})
|
|
}
|
|
}
|