perf(decode): parse struct destinations without the value tree
Test / test (push) Successful in 1m50s
Test / test (push) Successful in 1m50s
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package interpres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fuzzNested struct {
|
||||
X int `toml:"x"`
|
||||
Y string `toml:"y"`
|
||||
}
|
||||
|
||||
type fuzzDoc struct {
|
||||
Num int `toml:"num"`
|
||||
Flt float64 `toml:"flt"`
|
||||
Str string `toml:"str"`
|
||||
Flag bool `toml:"flag"`
|
||||
Small uint8 `toml:"small"`
|
||||
When time.Time `toml:"when"`
|
||||
Tags []string `toml:"tags"`
|
||||
Lims map[string]any `toml:"lims"`
|
||||
Tab fuzzNested `toml:"tab"`
|
||||
Arr []fuzzNested `toml:"arr"`
|
||||
Other string `toml:"other"`
|
||||
}
|
||||
|
||||
// fuzzStmts is the statement pool the generated documents draw from: every
|
||||
// destination kind the targeted parse handles, beside the shapes that make
|
||||
// it fall back (overflow, unknown tables, duplicate keys).
|
||||
var fuzzStmts = []string{
|
||||
`num = 1`, `num = 300`, `small = 300`, `small = 7`,
|
||||
`flt = 2.5`, `str = "x"`, `flag = true`,
|
||||
`when = 1979-05-27T07:32:00Z`,
|
||||
`tags = ["a", "b"]`, `tags = []`, `lims = { k = 1 }`,
|
||||
`[tab]`, `tab.x = 1`, `tab.y = "s"`, `x = 2`, `y = "t"`,
|
||||
`[[arr]]`, `x = 3`, `y = "u"`,
|
||||
`[tab.nested]`, `x = 4`,
|
||||
`other = "o"`, `zz = 1`, `[zz]`, `k = 1`,
|
||||
`num = 2`,
|
||||
}
|
||||
|
||||
func fuzzDocument(data []byte) []byte {
|
||||
var b strings.Builder
|
||||
for i, by := range data {
|
||||
if i > 0 {
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
b.WriteString(fuzzStmts[int(by)%len(fuzzStmts)])
|
||||
}
|
||||
return []byte(b.String())
|
||||
}
|
||||
|
||||
// treeDecodeInto is the reference decode: the ordinary tree path, non-strict
|
||||
// like the fuzz decode; the strict contracts have their own deterministic
|
||||
// tests.
|
||||
func treeDecodeInto(data []byte, v any) error {
|
||||
dec := newDecoder()
|
||||
tree, _, err := parseWithOptions(context.Background(), data, parseOptions{}, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dec.decode(tree, v)
|
||||
}
|
||||
|
||||
// decodeFinding normalises an error for the comparison. Decode-stage
|
||||
// findings several tables may produce (an unknown field, a missing required
|
||||
// key) compare as their class alone: the tree decode picks the reporting
|
||||
// table by map order and so does not promise one. Everything else compares
|
||||
// as its exact text.
|
||||
func decodeFinding(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
if de, ok := errors.AsType[*DecodeError](err); ok {
|
||||
if strings.Contains(de.Err.Error(), "unknown field") {
|
||||
return "unknown"
|
||||
}
|
||||
if strings.Contains(de.Err.Error(), "missing required key") {
|
||||
return "required"
|
||||
}
|
||||
return de.Path.String() + ": " + de.Err.Error()
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
// FuzzTargetedDecode holds the targeted parse to the tree decode as its
|
||||
// reference: for every generated document the two paths must agree on the
|
||||
// error class and on the decoded value.
|
||||
func FuzzTargetedDecode(f *testing.F) {
|
||||
seeds := []string{
|
||||
"num = 1\nstr = \"x\"\n[tab]\nx = 2\n[[arr]]\nx = 3\n",
|
||||
"small = 300\n",
|
||||
"[tab]\ntab.x = 1\n",
|
||||
"lims = { k = 1 }\ntags = [\"a\"]\n",
|
||||
"[[arr]]\ny = \"u\"\n[zz]\nk = 1\n",
|
||||
}
|
||||
for _, s := range seeds {
|
||||
f.Add([]byte(s))
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
doc := fuzzDocument(data)
|
||||
var tgt fuzzDoc
|
||||
tgtErr := NewDecoder().Decode(doc, &tgt)
|
||||
if tgtErr != nil {
|
||||
// A document with several decode-stage findings reports a different
|
||||
// one per run (the tree decode walks its maps in random order), so the
|
||||
// reference gets a few chances to produce the finding the targeted
|
||||
// side carries. The targeted error is either the tree's own or the
|
||||
// fallback already reran the tree.
|
||||
for i := range 8 {
|
||||
var ref fuzzDoc
|
||||
refErr := treeDecodeInto(doc, &ref)
|
||||
if refErr == nil {
|
||||
t.Fatalf("reference succeeded on retry %d, targeted failed: %v\ndoc:\n%s", i, tgtErr, doc)
|
||||
}
|
||||
if decodeFinding(refErr) == decodeFinding(tgtErr) {
|
||||
return
|
||||
}
|
||||
if i == 7 {
|
||||
t.Fatalf("errors disagree after retries:\ntargeted: %v\nlast tree: %v\ndoc:\n%s", tgtErr, refErr, doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
var ref fuzzDoc
|
||||
refErr := treeDecodeInto(doc, &ref)
|
||||
if refErr != nil {
|
||||
t.Fatalf("reference failed, targeted succeeded: %v\ndoc:\n%s", refErr, doc)
|
||||
}
|
||||
if !reflect.DeepEqual(ref, tgt) {
|
||||
t.Fatalf("values disagree:\ntree: %#v\ntargeted: %#v\ndoc:\n%s", ref, tgt, doc)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user