Files
interpres/fuzz_target_test.go
petrbalvin 2e61ad0ba9
Test / test (push) Successful in 1m49s
feat!: rework the public API to json/v2-style variadic options
Assisted-by: GLM 5.3 Flash
2026-09-22 18:42:18 +02:00

143 lines
4.2 KiB
Go

// 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",
"when = 07:32:00\n[tab.nested]\n",
"small = 300\n[[arr]]\nflt = 2.5\n",
}
for _, s := range seeds {
f.Add([]byte(s))
}
f.Fuzz(func(t *testing.T, data []byte) {
doc := fuzzDocument(data)
var tgt fuzzDoc
tgtErr := Unmarshal(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)
}
})
}