Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tm2/pkg/iavl): add FuzzIterateRange and modernize FuzzMutableTree #3548

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 199 additions & 30 deletions tm2/pkg/iavl/tree_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package iavl

import (
"encoding/json"
"fmt"
"io"
"io/fs"
"math/rand"
"os"
"path/filepath"
"strings"
"testing"

"github.com/gnolang/gno/tm2/pkg/db/memdb"
Expand All @@ -14,68 +20,76 @@ import (

// A program is a list of instructions.
type program struct {
instructions []instruction
Instructions []instruction `json:"instructions"`
}

func (p *program) Execute(tree *MutableTree) (err error) {
var errLine int

defer func() {
if r := recover(); r != nil {
var str string

for i, instr := range p.instructions {
prefix := " "
if i == errLine {
prefix = ">> "
}
str += prefix + instr.String() + "\n"
r := recover()
if r == nil {
return
}

// These are simply input errors and shouldn't be reported as actual logical issues.
if containsAny(fmt.Sprint(r), "Unrecognized op:", "Attempt to store nil value at key") {
return
}

var str string

for i, instr := range p.Instructions {
prefix := " "
if i == errLine {
prefix = ">> "
}
err = fmt.Errorf("Program panicked with: %s\n%s", r, str)
str += prefix + instr.String() + "\n"
}
err = fmt.Errorf("Program panicked with: %s\n%s", r, str)
}()

for i, instr := range p.instructions {
for i, instr := range p.Instructions {
errLine = i
instr.Execute(tree)
}
return
}

func (p *program) addInstruction(i instruction) {
p.instructions = append(p.instructions, i)
p.Instructions = append(p.Instructions, i)
}

func (p *program) size() int {
return len(p.instructions)
return len(p.Instructions)
}

type instruction struct {
op string
k, v []byte
version int64
Op string
K, V []byte
Version int64
}

func (i instruction) Execute(tree *MutableTree) {
switch i.op {
switch i.Op {
case "SET":
tree.Set(i.k, i.v)
tree.Set(i.K, i.V)
case "REMOVE":
tree.Remove(i.k)
tree.Remove(i.K)
case "SAVE":
tree.SaveVersion()
case "DELETE":
tree.DeleteVersion(i.version)
tree.DeleteVersion(i.Version)
default:
panic("Unrecognized op: " + i.op)
panic("Unrecognized op: " + i.Op)
}
}

func (i instruction) String() string {
if i.version > 0 {
return fmt.Sprintf("%-8s %-8s %-8s %-8d", i.op, i.k, i.v, i.version)
if i.Version > 0 {
return fmt.Sprintf("%-8s %-8s %-8s %-8d", i.Op, i.K, i.V, i.Version)
}
return fmt.Sprintf("%-8s %-8s %-8s", i.op, i.k, i.v)
return fmt.Sprintf("%-8s %-8s %-8s", i.Op, i.K, i.V)
}

// Generate a random program of the given size.
Expand All @@ -88,15 +102,15 @@ func genRandomProgram(size int) *program {

switch rand.Int() % 7 {
case 0, 1, 2:
p.addInstruction(instruction{op: "SET", k: k, v: v})
p.addInstruction(instruction{Op: "SET", K: k, V: v})
case 3, 4:
p.addInstruction(instruction{op: "REMOVE", k: k})
p.addInstruction(instruction{Op: "REMOVE", K: k})
case 5:
p.addInstruction(instruction{op: "SAVE", version: int64(nextVersion)})
p.addInstruction(instruction{Op: "SAVE", Version: int64(nextVersion)})
nextVersion++
case 6:
if rv := rand.Int() % nextVersion; rv < nextVersion && rv > 0 {
p.addInstruction(instruction{op: "DELETE", version: int64(rv)})
p.addInstruction(instruction{Op: "DELETE", Version: int64(rv)})
}
}
}
Expand All @@ -107,19 +121,174 @@ func genRandomProgram(size int) *program {
func TestMutableTreeFuzz(t *testing.T) {
t.Parallel()

runThenGenerateMutableTreeFuzzSeeds(t, false)
}

var pathForMutableTreeProgramSeeds = filepath.Join("testdata", "corpora", "mutable_tree_programs")

func runThenGenerateMutableTreeFuzzSeeds(tb testing.TB, writeSeedsToFileSystem bool) {
tb.Helper()

if testing.Short() {
tb.Skip("Running in -short mode")
}

maxIterations := testFuzzIterations
progsPerIteration := 100000
iterations := 0

if writeSeedsToFileSystem {
if err := os.MkdirAll(pathForMutableTreeProgramSeeds, 0o755); err != nil {
tb.Fatal(err)
}
}

for size := 5; iterations < maxIterations; size++ {
for i := 0; i < progsPerIteration/size; i++ {
tree := NewMutableTree(memdb.NewMemDB(), 0)
program := genRandomProgram(size)
err := program.Execute(tree)
if err != nil {
t.Fatalf("Error after %d iterations (size %d): %s\n%s", iterations, size, err.Error(), tree.String())
tb.Fatalf("Error after %d iterations (size %d): %s\n%s", iterations, size, err.Error(), tree.String())
}
iterations++

if !writeSeedsToFileSystem {
continue
}

// Otherwise write them to the testdata/corpra directory.
programJSON, err := json.Marshal(program)
if err != nil {
tb.Fatal(err)
}
path := filepath.Join(pathForMutableTreeProgramSeeds, fmt.Sprintf("%d", i+1))
if err := os.WriteFile(path, programJSON, 0o755); err != nil {
tb.Fatal(err)
}
}
}
}

type treeRange struct {
Start []byte
End []byte
Forward bool
}

var basicRecords = []struct {
key, value string
}{
{"abc", "123"},
{"low", "high"},
{"fan", "456"},
{"foo", "a"},
{"foobaz", "c"},
{"good", "bye"},
{"foobang", "d"},
{"foobar", "b"},
{"food", "e"},
{"foml", "f"},
}

// Allows hooking into Go's fuzzers and then for continuous fuzzing
// enriched with coverage guided mutations, instead of naive mutations.
func FuzzIterateRange(f *testing.F) {
if testing.Short() {
f.Skip("Skipping in -short mode")
}

// 1. Add the seeds.
seeds := []*treeRange{
{[]byte("foo"), []byte("goo"), true},
{[]byte("aaa"), []byte("abb"), true},
{nil, []byte("flap"), true},
{[]byte("foob"), nil, true},
{[]byte("very"), nil, true},
{[]byte("very"), nil, false},
{[]byte("fooba"), []byte("food"), true},
{[]byte("fooba"), []byte("food"), false},
{[]byte("g"), nil, false},
}
for _, seed := range seeds {
blob, err := json.Marshal(seed)
if err != nil {
f.Fatal(err)
}
f.Add(blob)
}

db := memdb.NewMemDB()
tree := NewMutableTree(db, 0)
for _, br := range basicRecords {
tree.Set([]byte(br.key), []byte(br.value))
}

var trav traverser

// 2. Run the fuzzer.
f.Fuzz(func(t *testing.T, rangeJSON []byte) {
tr := new(treeRange)
if err := json.Unmarshal(rangeJSON, tr); err != nil {
return
}

tree.IterateRange(tr.Start, tr.End, tr.Forward, trav.view)
})
}

func containsAny(s string, anyOf ...string) bool {
for _, q := range anyOf {
if strings.Contains(s, q) {
return true
}
}
return false
}

func FuzzMutableTreeInstructions(f *testing.F) {
if testing.Short() {
f.Skip("Skipping in -short mode")
}

// 0. Generate then add the seeds.
runThenGenerateMutableTreeFuzzSeeds(f, true)

// 1. Add the seeds.
dir := os.DirFS("testdata")
err := fs.WalkDir(dir, ".", func(path string, de fs.DirEntry, err error) error {
if de.IsDir() {
return err
}

ff, err := dir.Open(path)
if err != nil {
return err
}
defer ff.Close()

blob, err := io.ReadAll(ff)
if err != nil {
return err
}
f.Add(blob)
return nil
})
if err != nil {
f.Fatal(err)
}

// 2. Run the fuzzer.
f.Fuzz(func(t *testing.T, programJSON []byte) {
program := new(program)
if err := json.Unmarshal(programJSON, program); err != nil {
return
}

tree := NewMutableTree(memdb.NewMemDB(), 0)
err := program.Execute(tree)
if err != nil {
t.Fatal(err)
}
})
}
Loading