// Copyright (c) 2026 Petr BalvĂ­n (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 := 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) } }) } }