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: Add unit test for traverseOrphans #663

Merged
merged 13 commits into from
Jan 23, 2023
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.18
require (
github.com/confio/ics23/go v0.9.0
github.com/cosmos/cosmos-db v0.0.0-20220822060143-23a8145386c0
github.com/emicklei/dot v1.2.0
github.com/golang/mock v1.6.0
github.com/golangci/golangci-lint v1.50.1
github.com/stretchr/testify v1.8.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
github.com/emicklei/dot v1.2.0 h1:WjL422LPltH/ThM9AJQ8HJXEMw9SOxLrglppg/1pFYU=
github.com/emicklei/dot v1.2.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
Expand Down
90 changes: 90 additions & 0 deletions nodedb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,96 @@ func TestIsFastStorageEnabled_False(t *testing.T) {
require.NoError(t, err)
}

func assertOrphansAndBranches(t *testing.T, ndb *nodeDB, version int64, branches int, orphanKeys [][]byte) {
var branchCount, orphanIndex int
err := ndb.traverseOrphans(version, func(node *Node) error {
if node.isLeaf() {
require.Equal(t, orphanKeys[orphanIndex], node.key)
orphanIndex++
} else {
branchCount++
}
return nil
})

require.NoError(t, err)
require.Equal(t, branches, branchCount)
}

func TestNodeDB_traverseOrphans(t *testing.T) {
tree, _ := getTestTree(0)
var up bool
var err error

// version 1
for i := 0; i < 20; i++ {
up, err = tree.Set([]byte{byte(i)}, []byte{byte(i)})
require.False(t, up)
require.NoError(t, err)
}
_, _, err = tree.SaveVersion()
require.NoError(t, err)
// note: assertions were constructed by hand after inspecting the output of the graphviz below.
// WriteDOTGraphToFile("/tmp/tree_one.dot", tree.ImmutableTree)

// version 2
up, err = tree.Set([]byte{byte(19)}, []byte{byte(0)})
require.True(t, up)
require.NoError(t, err)
_, _, err = tree.SaveVersion()
require.NoError(t, err)
// WriteDOTGraphToFile("/tmp/tree_two.dot", tree.ImmutableTree)

assertOrphansAndBranches(t, tree.ndb, 1, 5, [][]byte{{byte(19)}})

// version 3
k, up, err := tree.Remove([]byte{byte(0)})
require.Equal(t, []byte{byte(0)}, k)
require.True(t, up)
require.NoError(t, err)

_, _, err = tree.SaveVersion()
require.NoError(t, err)
// WriteDOTGraphToFile("/tmp/tree_three.dot", tree.ImmutableTree)

assertOrphansAndBranches(t, tree.ndb, 2, 4, [][]byte{{byte(0)}})

// version 4
k, up, err = tree.Remove([]byte{byte(1)})
require.Equal(t, []byte{byte(1)}, k)
require.True(t, up)
require.NoError(t, err)
k, up, err = tree.Remove([]byte{byte(19)})
require.Equal(t, []byte{byte(0)}, k)
require.True(t, up)
require.NoError(t, err)

_, _, err = tree.SaveVersion()
require.NoError(t, err)
// WriteDOTGraphToFile("/tmp/tree_four.dot", tree.ImmutableTree)

assertOrphansAndBranches(t, tree.ndb, 3, 7, [][]byte{{byte(1)}, {byte(19)}})

// version 5
k, up, err = tree.Remove([]byte{byte(10)})
require.Equal(t, []byte{byte(10)}, k)
require.True(t, up)
require.NoError(t, err)
k, up, err = tree.Remove([]byte{byte(9)})
require.Equal(t, []byte{byte(9)}, k)
require.True(t, up)
require.NoError(t, err)
up, err = tree.Set([]byte{byte(12)}, []byte{byte(0)})
require.True(t, up)
require.NoError(t, err)

_, _, err = tree.SaveVersion()
require.NoError(t, err)
// WriteDOTGraphToFile("/tmp/tree_five.dot", tree.ImmutableTree)

assertOrphansAndBranches(t, tree.ndb, 4, 8, [][]byte{{byte(9)}, {byte(10)}, {byte(12)}})
}

func makeHashes(b *testing.B, seed int64) [][]byte {
b.StopTimer()
rnd := rand.NewSource(seed)
Expand Down
70 changes: 70 additions & 0 deletions tree_dotgraph.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package iavl

import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"text/template"

ibytes "github.com/cosmos/iavl/internal/bytes"
"github.com/emicklei/dot"
)

type graphEdge struct {
Expand Down Expand Up @@ -106,3 +109,70 @@ func WriteDOTGraph(w io.Writer, tree *ImmutableTree, paths []PathToLeaf) {
func mkLabel(label string, pt int, face string) string {
return fmt.Sprintf("<font face='%s' point-size='%d'>%s</font><br />", face, pt, label)
}

// WriteDOTGraphToFile writes the DOT graph to the given filename. Read like:
// $ dot /tmp/tree_one.dot -Tpng | display
func WriteDOTGraphToFile(filename string, tree *ImmutableTree) {
f1, _ := os.Create(filename)
defer f1.Close()
writer := bufio.NewWriter(f1)
WriteDotGraphv2(writer, tree)
err := writer.Flush()
if err != nil {
panic(err)
}
}

// WriteDotGraphv2 writes a DOT graph to the given writer. WriteDOTGraph failed to produce valid DOT
// graphs for large trees. This function is a rewrite of WriteDOTGraph that produces valid DOT graphs
func WriteDotGraphv2(w io.Writer, tree *ImmutableTree) {
graph := dot.NewGraph(dot.Directed)

var traverse func(node *Node, parent *dot.Node, direction string)
traverse = func(node *Node, parent *dot.Node, direction string) {
var label string
if node.isLeaf() {
label = fmt.Sprintf("%v:%v\nv%v", node.key, node.value, node.version)
} else {
label = fmt.Sprintf("%v:%v\nv%v", node.subtreeHeight, node.key, node.version)
}

n := graph.Node(label)
if parent != nil {
parent.Edge(n, direction)
}

var leftNode, rightNode *Node

if node.leftNode != nil {
leftNode = node.leftNode
} else if node.leftHash != nil {
in, err := node.getLeftNode(tree)
if err == nil {
leftNode = in
}
}

if node.rightNode != nil {
rightNode = node.rightNode
} else if node.rightHash != nil {
in, err := node.getRightNode(tree)
if err == nil {
rightNode = in
}
}

if leftNode != nil {
traverse(leftNode, &n, "l")
}
if rightNode != nil {
traverse(rightNode, &n, "r")
}
}

traverse(tree.root, nil, "")
_, err := w.Write([]byte(graph.String()))
if err != nil {
panic(err)
}
}