Files
gasm-sdk/parser/fuzz_test.go
T
petrbalvin e02918c17b
Test / test (push) Successful in 2m55s
fix(ci): keep the push suite inside the runner's memory and time budget
Assisted-by: GLM 5.3 Flash
2026-10-02 17:20:31 +02:00

121 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package parser
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
)
// corpusFiles seeds a fuzz target with the repository's kernels, so a plain
// `go test` run replays every seed as a regression case and CI exercises them
// without any fuzzing budget.
func corpusFiles(f *testing.F) {
for _, pattern := range []string{
"../testdata/*.s",
"../testdata/verify/*.s",
} {
files, _ := filepath.Glob(pattern)
for _, path := range files {
if b, err := os.ReadFile(path); err == nil {
f.Add(string(b))
}
}
}
}
// FuzzParse hammers the parser with arbitrary input. The contract: no panic,
// always a usable file whether or not diagnostics were reported, and every
// diagnostic carries a real position and a non-empty message.
func FuzzParse(f *testing.F) {
corpusFiles(f)
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tRET\n")
f.Add("garbage ### ??? ::: \xff\xfe\n")
f.Add("#define A(x) x+1\nTEXT ·f(SB), $0\n\tA(2)\n\tRET\n")
f.Add("DATA t<>+0(SB)/8, $1\nGLOBL t<>(SB), RODATA, $8\n")
f.Add("TEXT ·f(SB), $0\n\tJMP (AX)\n\tCALL (BX)\n\tRET\n")
f.Add("\xef\xbb\xbfTEXT ·f(SB), $0\n") // BOM
f.Add("CALL internal∕runtime∕atomic·Xchg(SB)\n") // U+2215 path, U+00B7 name
f.Add("TEXT ·f(SB), $0\n\tJMP -3(PC)\n\tRET\n") // negative PC jump
f.Add("DATA d<>+0(SB)/8, $0xFFFFFFFFFFFFFFFF\n") // unsigned 64-bit immediate
f.Add("TEXT ·f(SB), $0-0xFFFFFFFFFFFFFFFF\n\tRET\n") // argument area overflow
f.Add("TEXT ·f(SB), $0\n\tMOVD $(1<<0|1<<9), R0\n\tRET\n") // constant expression
f.Add("TEXT ·f(SB), $0\n\tADD $-8, R10; SUB $~63, R11\n\tRET\n")
// Constant-expression nesting: an immediate and a displacement read
// through the recursive folder, so parenthesis groups past the folder's
// depth bound probe its fallback on every plain run. The seeds sit just
// above the bound (maxFoldDepth), not in the millions: a seed corpus
// runs on every `go test` including the small-memory CI runner, and an
// eight-million-token seed costs the process close to a gigabyte before
// the bound even engages. The megascale belongs to the fuzz timebox.
f.Add("TEXT ·f(SB), $0\n\tMOVD $" + strings.Repeat("(", 1500) + "1" + strings.Repeat(")", 1500) + ", R0\n\tRET\n")
f.Add("TEXT ·f(SB), $0\n\tMOVQ " + strings.Repeat("(", 2000) + "1" + strings.Repeat(")", 2000) + "(AX), BX\n\tRET\n")
f.Add("TEXT ·f(SB), $0\n\tMOVD $" + strings.Repeat("~", 5000) + "1, R0\n\tRET\n")
f.Fuzz(func(t *testing.T, src string) {
file, errs := Parse("fuzz.s", src)
if file == nil {
t.Fatal("Parse returned a nil file")
}
for _, err := range errs {
var perr Error
if !errors.As(err, &perr) {
t.Fatalf("diagnostic %v is not a parser Error", err)
}
if !perr.Pos.IsValid() {
t.Fatalf("diagnostic %q carries no position", perr.Msg)
}
if strings.TrimSpace(perr.Msg) == "" {
t.Fatalf("diagnostic at %v carries no message", perr.Pos)
}
}
})
}
// FuzzParseExpand hammers the preprocessing path, macro expansion and include
// splicing included, with arbitrary input. The contract is FuzzParse's: no
// panic, a usable file, and diagnostics that name a place and a reason.
func FuzzParseExpand(f *testing.F) {
corpusFiles(f)
f.Add("#define A 1\nTEXT ·f(SB), $0\n\tMOVD $A, R0\n\tRET\n")
f.Add("#define A(x) x+1\nA(2)\n")
f.Add("#define A(x) ((x))\nA(A(A(1)))\n")
f.Add("#define A\nTEXT ·f(SB), $0\n\tRET\n") // empty body
f.Add("#define A A\nA\n") // self-reference
f.Add("#define A B\n#define B A\nA\n") // mutual recursion
f.Add("#ifdef X\n#else\n#endif\n")
f.Add("#ifndef X\none\n#else\ntwo\n#endif\n")
f.Add("#undef Y\n")
f.Add("#include \"textflag.h\"\n")
f.Add("#include \"no/such/header.h\"\n")
f.Add("#line 7 \"f.s\"\n")
f.Add("#define M(x) \\\n\tADD $x, R0 \\\n\tSUB $x, R1\nM(1)\n") // continuations
// A body that repeats its argument multiplies every nesting level; the
// expansion must stop at the work budget, not produce the exponential
// result it names.
f.Add("#define A(x) x x x x x x x x x x\nA(A(A(A(A(A(A(A(A(A(1))))))))))\n")
f.Fuzz(func(t *testing.T, src string) {
file, errs := ParseWithOptions("fuzz.s", src, Options{Expand: true})
if file == nil {
t.Fatal("ParseWithOptions returned a nil file")
}
for _, err := range errs {
var perr Error
if !errors.As(err, &perr) {
t.Fatalf("diagnostic %v is not a parser Error", err)
}
if !perr.Pos.IsValid() {
t.Fatalf("diagnostic %q carries no position", perr.Msg)
}
if strings.TrimSpace(perr.Msg) == "" {
t.Fatalf("diagnostic at %v carries no message", perr.Pos)
}
}
})
}