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