fix(decode): keep the targeted parse on the tree path's contract
Assisted-by: GLM 5.3
This commit is contained in:
+350
@@ -4,9 +4,12 @@
|
||||
package interpres
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"maps"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -522,3 +525,350 @@ func TestTargetedMapTableShapes(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user