File: //proc/self/root/opt/go/pkg/mod/github.com/hashicorp/
[email protected]/iradix_test.go
package iradix
import (
"fmt"
"math/rand"
"reflect"
"sort"
"testing"
"testing/quick"
"github.com/hashicorp/go-uuid"
)
func CopyTree(t *Tree) *Tree {
nt := &Tree{
root: CopyNode(t.root),
size: t.size,
}
return nt
}
func CopyNode(n *Node) *Node {
nn := &Node{}
if n.mutateCh != nil {
nn.mutateCh = n.mutateCh
}
if n.prefix != nil {
nn.prefix = make([]byte, len(n.prefix))
copy(nn.prefix, n.prefix)
}
if n.leaf != nil {
nn.leaf = CopyLeaf(n.leaf)
}
if len(n.edges) != 0 {
nn.edges = make([]edge, len(n.edges))
for idx, edge := range n.edges {
nn.edges[idx].label = edge.label
nn.edges[idx].node = CopyNode(edge.node)
}
}
return nn
}
func CopyLeaf(l *leafNode) *leafNode {
ll := &leafNode{
mutateCh: l.mutateCh,
key: l.key,
val: l.val,
}
return ll
}
func TestRadix_HugeTxn(t *testing.T) {
r := New()
// Insert way more nodes than the cache can fit
txn1 := r.Txn()
var expect []string
for i := 0; i < defaultModifiedCache*100; i++ {
gen, err := uuid.GenerateUUID()
if err != nil {
t.Fatalf("err: %v", err)
}
txn1.Insert([]byte(gen), i)
expect = append(expect, gen)
}
r = txn1.Commit()
sort.Strings(expect)
// Collect the output, should be sorted
var out []string
fn := func(k []byte, v interface{}) bool {
out = append(out, string(k))
return false
}
r.Root().Walk(fn)
// Verify the match
if len(out) != len(expect) {
t.Fatalf("length mis-match: %d vs %d", len(out), len(expect))
}
for i := 0; i < len(out); i++ {
if out[i] != expect[i] {
t.Fatalf("mis-match: %v %v", out[i], expect[i])
}
}
}
func TestRadix(t *testing.T) {
var min, max string
inp := make(map[string]interface{})
for i := 0; i < 1000; i++ {
gen, err := uuid.GenerateUUID()
if err != nil {
t.Fatalf("err: %v", err)
}
inp[gen] = i
if gen < min || i == 0 {
min = gen
}
if gen > max || i == 0 {
max = gen
}
}
r := New()
rCopy := CopyTree(r)
for k, v := range inp {
newR, _, _ := r.Insert([]byte(k), v)
if !reflect.DeepEqual(r, rCopy) {
t.Errorf("r: %#v rc: %#v", r, rCopy)
t.Errorf("r: %#v rc: %#v", r.root, rCopy.root)
t.Fatalf("structure modified %d", newR.Len())
}
r = newR
rCopy = CopyTree(r)
}
if r.Len() != len(inp) {
t.Fatalf("bad length: %v %v", r.Len(), len(inp))
}
for k, v := range inp {
out, ok := r.Get([]byte(k))
if !ok {
t.Fatalf("missing key: %v", k)
}
if out != v {
t.Fatalf("value mis-match: %v %v", out, v)
}
}
// Check min and max
outMin, _, _ := r.Root().Minimum()
if string(outMin) != min {
t.Fatalf("bad minimum: %v %v", outMin, min)
}
outMax, _, _ := r.Root().Maximum()
if string(outMax) != max {
t.Fatalf("bad maximum: %v %v", outMax, max)
}
// Copy the full tree before delete
orig := r
origCopy := CopyTree(r)
for k, v := range inp {
tree, out, ok := r.Delete([]byte(k))
r = tree
if !ok {
t.Fatalf("missing key: %v", k)
}
if out != v {
t.Fatalf("value mis-match: %v %v", out, v)
}
}
if r.Len() != 0 {
t.Fatalf("bad length: %v", r.Len())
}
if !reflect.DeepEqual(orig, origCopy) {
t.Fatalf("structure modified")
}
}
func TestRoot(t *testing.T) {
r := New()
r, _, ok := r.Delete(nil)
if ok {
t.Fatalf("bad")
}
r, _, ok = r.Insert(nil, true)
if ok {
t.Fatalf("bad")
}
val, ok := r.Get(nil)
if !ok || val != true {
t.Fatalf("bad: %#v", val)
}
r, val, ok = r.Delete(nil)
if !ok || val != true {
t.Fatalf("bad: %v", val)
}
}
func TestInsert_UpdateFeedback(t *testing.T) {
r := New()
txn1 := r.Txn()
for i := 0; i < 10; i++ {
var old interface{}
var didUpdate bool
old, didUpdate = txn1.Insert([]byte("helloworld"), i)
if i == 0 {
if old != nil || didUpdate {
t.Fatalf("bad: %d %v %v", i, old, didUpdate)
}
} else {
if old == nil || old.(int) != i-1 || !didUpdate {
t.Fatalf("bad: %d %v %v", i, old, didUpdate)
}
}
}
}
func TestDelete(t *testing.T) {
r := New()
s := []string{"", "A", "AB"}
for _, ss := range s {
r, _, _ = r.Insert([]byte(ss), true)
}
var ok bool
for _, ss := range s {
r, _, ok = r.Delete([]byte(ss))
if !ok {
t.Fatalf("bad %q", ss)
}
}
}
func TestDeletePrefix(t *testing.T) {
type exp struct {
desc string
treeNodes []string
prefix string
expectedOut []string
}
//various test cases where DeletePrefix should succeed
cases := []exp{
{
"prefix not a node in tree",
[]string{
"",
"test/test1",
"test/test2",
"test/test3",
"R",
"RA"},
"test",
[]string{
"",
"R",
"RA",
},
},
{
"prefix matches a node in tree",
[]string{
"",
"test",
"test/test1",
"test/test2",
"test/test3",
"test/testAAA",
"R",
"RA",
},
"test",
[]string{
"",
"R",
"RA",
},
},
{
"longer prefix, but prefix is not a node in tree",
[]string{
"",
"test/test1",
"test/test2",
"test/test3",
"test/testAAA",
"R",
"RA",
},
"test/test",
[]string{
"",
"R",
"RA",
},
},
{
"prefix only matches one node",
[]string{
"",
"AB",
"ABC",
"AR",
"R",
"RA",
},
"AR",
[]string{
"",
"AB",
"ABC",
"R",
"RA",
},
},
}
for _, testCase := range cases {
t.Run(testCase.desc, func(t *testing.T) {
r := New()
for _, ss := range testCase.treeNodes {
r, _, _ = r.Insert([]byte(ss), true)
}
if got, want := r.Len(), len(testCase.treeNodes); got != want {
t.Fatalf("Unexpected tree length after insert, got %d want %d ", got, want)
}
r, ok := r.DeletePrefix([]byte(testCase.prefix))
if !ok {
t.Fatalf("DeletePrefix should have returned true for tree %v, deleting prefix %v", testCase.treeNodes, testCase.prefix)
}
if got, want := r.Len(), len(testCase.expectedOut); got != want {
t.Fatalf("Bad tree length, got %d want %d tree %v, deleting prefix %v ", got, want, testCase.treeNodes, testCase.prefix)
}
verifyTree(t, testCase.expectedOut, r)
//Delete a non-existant node
r, ok = r.DeletePrefix([]byte("CCCCC"))
if ok {
t.Fatalf("Expected DeletePrefix to return false ")
}
verifyTree(t, testCase.expectedOut, r)
})
}
}
func TestTrackMutate_DeletePrefix(t *testing.T) {
r := New()
keys := []string{
"foo",
"foo/bar/baz",
"foo/baz/bar",
"foo/zip/zap",
"bazbaz",
"zipzap",
}
for _, k := range keys {
r, _, _ = r.Insert([]byte(k), nil)
}
if r.Len() != len(keys) {
t.Fatalf("bad len: %v %v", r.Len(), len(keys))
}
rootWatch, _, _ := r.Root().GetWatch(nil)
if rootWatch == nil {
t.Fatalf("Should have returned a watch")
}
nodeWatch1, _, _ := r.Root().GetWatch([]byte("foo/bar/baz"))
if nodeWatch1 == nil {
t.Fatalf("Should have returned a watch")
}
nodeWatch2, _, _ := r.Root().GetWatch([]byte("foo/baz/bar"))
if nodeWatch2 == nil {
t.Fatalf("Should have returned a watch")
}
nodeWatch3, _, _ := r.Root().GetWatch([]byte("foo/zip/zap"))
if nodeWatch3 == nil {
t.Fatalf("Should have returned a watch")
}
unknownNodeWatch, _, _ := r.Root().GetWatch([]byte("bazbaz"))
if unknownNodeWatch == nil {
t.Fatalf("Should have returned a watch")
}
// Verify that deleting prefixes triggers the right set of watches
txn := r.Txn()
txn.TrackMutate(true)
ok := txn.DeletePrefix([]byte("foo"))
if !ok {
t.Fatalf("Expected delete prefix to return true")
}
if hasAnyClosedMutateCh(r) {
t.Fatalf("Transaction was not committed, no channel should have been closed")
}
txn.Commit()
// Verify that all the leaf nodes we set up watches for above get triggered from the delete prefix call
select {
case <-rootWatch:
default:
t.Fatalf("root watch was not triggered")
}
select {
case <-nodeWatch1:
default:
t.Fatalf("node watch was not triggered")
}
select {
case <-nodeWatch2:
default:
t.Fatalf("node watch was not triggered")
}
select {
case <-nodeWatch3:
default:
t.Fatalf("node watch was not triggered")
}
select {
case <-unknownNodeWatch:
t.Fatalf("Unrelated node watch was triggered during a prefix delete")
default:
}
}
func verifyTree(t *testing.T, expected []string, r *Tree) {
root := r.Root()
out := []string{}
fn := func(k []byte, v interface{}) bool {
out = append(out, string(k))
return false
}
root.Walk(fn)
if !reflect.DeepEqual(expected, out) {
t.Fatalf("Unexpected contents of tree after delete prefix: expected %v, but got %v", expected, out)
}
}
func TestLongestPrefix(t *testing.T) {
r := New()
keys := []string{
"",
"foo",
"foobar",
"foobarbaz",
"foobarbazzip",
"foozip",
}
for _, k := range keys {
r, _, _ = r.Insert([]byte(k), nil)
}
if r.Len() != len(keys) {
t.Fatalf("bad len: %v %v", r.Len(), len(keys))
}
type exp struct {
inp string
out string
}
cases := []exp{
{"a", ""},
{"abc", ""},
{"fo", ""},
{"foo", "foo"},
{"foob", "foo"},
{"foobar", "foobar"},
{"foobarba", "foobar"},
{"foobarbaz", "foobarbaz"},
{"foobarbazzi", "foobarbaz"},
{"foobarbazzip", "foobarbazzip"},
{"foozi", "foo"},
{"foozip", "foozip"},
{"foozipzap", "foozip"},
}
root := r.Root()
for _, test := range cases {
m, _, ok := root.LongestPrefix([]byte(test.inp))
if !ok {
t.Fatalf("no match: %v", test)
}
if string(m) != test.out {
t.Fatalf("mis-match: %v %v", m, test)
}
}
}
func TestWalkPrefix(t *testing.T) {
r := New()
keys := []string{
"foobar",
"foo/bar/baz",
"foo/baz/bar",
"foo/zip/zap",
"zipzap",
}
for _, k := range keys {
r, _, _ = r.Insert([]byte(k), nil)
}
if r.Len() != len(keys) {
t.Fatalf("bad len: %v %v", r.Len(), len(keys))
}
type exp struct {
inp string
out []string
}
cases := []exp{
{
"f",
[]string{"foobar", "foo/bar/baz", "foo/baz/bar", "foo/zip/zap"},
},
{
"foo",
[]string{"foobar", "foo/bar/baz", "foo/baz/bar", "foo/zip/zap"},
},
{
"foob",
[]string{"foobar"},
},
{
"foo/",
[]string{"foo/bar/baz", "foo/baz/bar", "foo/zip/zap"},
},
{
"foo/b",
[]string{"foo/bar/baz", "foo/baz/bar"},
},
{
"foo/ba",
[]string{"foo/bar/baz", "foo/baz/bar"},
},
{
"foo/bar",
[]string{"foo/bar/baz"},
},
{
"foo/bar/baz",
[]string{"foo/bar/baz"},
},
{
"foo/bar/bazoo",
[]string{},
},
{
"z",
[]string{"zipzap"},
},
}
root := r.Root()
for _, test := range cases {
out := []string{}
fn := func(k []byte, v interface{}) bool {
out = append(out, string(k))
return false
}
root.WalkPrefix([]byte(test.inp), fn)
sort.Strings(out)
sort.Strings(test.out)
if !reflect.DeepEqual(out, test.out) {
t.Fatalf("mis-match: %v %v", out, test.out)
}
}
}
func TestWalkPath(t *testing.T) {
r := New()
keys := []string{
"foo",
"foo/bar",
"foo/bar/baz",
"foo/baz/bar",
"foo/zip/zap",
"zipzap",
}
for _, k := range keys {
r, _, _ = r.Insert([]byte(k), nil)
}
if r.Len() != len(keys) {
t.Fatalf("bad len: %v %v", r.Len(), len(keys))
}
type exp struct {
inp string
out []string
}
cases := []exp{
{
"f",
[]string{},
},
{
"foo",
[]string{"foo"},
},
{
"foo/",
[]string{"foo"},
},
{
"foo/ba",
[]string{"foo"},
},
{
"foo/bar",
[]string{"foo", "foo/bar"},
},
{
"foo/bar/baz",
[]string{"foo", "foo/bar", "foo/bar/baz"},
},
{
"foo/bar/bazoo",
[]string{"foo", "foo/bar", "foo/bar/baz"},
},
{
"z",
[]string{},
},
}
root := r.Root()
for _, test := range cases {
out := []string{}
fn := func(k []byte, v interface{}) bool {
out = append(out, string(k))
return false
}
root.WalkPath([]byte(test.inp), fn)
sort.Strings(out)
sort.Strings(test.out)
if !reflect.DeepEqual(out, test.out) {
t.Fatalf("mis-match: %v %v", out, test.out)
}
}
}
func TestIteratePrefix(t *testing.T) {
r := New()
keys := []string{
"foo/bar/baz",
"foo/baz/bar",
"foo/zip/zap",
"foobar",
"zipzap",
}
for _, k := range keys {
r, _, _ = r.Insert([]byte(k), nil)
}
if r.Len() != len(keys) {
t.Fatalf("bad len: %v %v", r.Len(), len(keys))
}
type exp struct {
inp string
out []string
}
cases := []exp{
{
"",
keys,
},
{
"f",
[]string{
"foo/bar/baz",
"foo/baz/bar",
"foo/zip/zap",
"foobar",
},
},
{
"foo",
[]string{
"foo/bar/baz",
"foo/baz/bar",
"foo/zip/zap",
"foobar",
},
},
{
"foob",
[]string{"foobar"},
},
{
"foo/",
[]string{"foo/bar/baz", "foo/baz/bar", "foo/zip/zap"},
},
{
"foo/b",
[]string{"foo/bar/baz", "foo/baz/bar"},
},
{
"foo/ba",
[]string{"foo/bar/baz", "foo/baz/bar"},
},
{
"foo/bar",
[]string{"foo/bar/baz"},
},
{
"foo/bar/baz",
[]string{"foo/bar/baz"},
},
{
"foo/bar/bazoo",
[]string{},
},
{
"z",
[]string{"zipzap"},
},
}
root := r.Root()
for idx, test := range cases {
iter := root.Iterator()
if test.inp != "" {
iter.SeekPrefix([]byte(test.inp))
}
// Consume all the keys
out := []string{}
for {
key, _, ok := iter.Next()
if !ok {
break
}
out = append(out, string(key))
}
if !reflect.DeepEqual(out, test.out) {
t.Fatalf("mis-match: %d %v %v", idx, out, test.out)
}
}
}
func TestMergeChildNilEdges(t *testing.T) {
r := New()
r, _, _ = r.Insert([]byte("foobar"), 42)
r, _, _ = r.Insert([]byte("foozip"), 43)
r, _, _ = r.Delete([]byte("foobar"))
root := r.Root()
out := []string{}
fn := func(k []byte, v interface{}) bool {
out = append(out, string(k))
return false
}
root.Walk(fn)
expect := []string{"foozip"}
sort.Strings(out)
sort.Strings(expect)
if !reflect.DeepEqual(out, expect) {
t.Fatalf("mis-match: %v %v", out, expect)
}
}
func TestMergeChildVisibility(t *testing.T) {
r := New()
r, _, _ = r.Insert([]byte("foobar"), 42)
r, _, _ = r.Insert([]byte("foobaz"), 43)
r, _, _ = r.Insert([]byte("foozip"), 10)
txn1 := r.Txn()
txn2 := r.Txn()
// Ensure we get the expected value foobar and foobaz
if val, ok := txn1.Get([]byte("foobar")); !ok || val != 42 {
t.Fatalf("bad: %v", val)
}
if val, ok := txn1.Get([]byte("foobaz")); !ok || val != 43 {
t.Fatalf("bad: %v", val)
}
if val, ok := txn2.Get([]byte("foobar")); !ok || val != 42 {
t.Fatalf("bad: %v", val)
}
if val, ok := txn2.Get([]byte("foobaz")); !ok || val != 43 {
t.Fatalf("bad: %v", val)
}
// Delete of foozip will cause a merge child between the
// "foo" and "ba" nodes.
if val, ok := txn2.Delete([]byte("foozip")); !ok || val != 10 {
t.Fatalf("bad: %v", val)
}
// Insert of "foobaz" will update the slice of the "fooba" node
// in-place to point to the new "foobaz" node. This in-place update
// will cause the visibility of the update to leak into txn1 (prior
// to the fix).
if val, ok := txn2.Insert([]byte("foobaz"), 44); !ok || val != 43 {
t.Fatalf("bad: %v", val)
}
// Ensure we get the expected value foobar and foobaz
if val, ok := txn1.Get([]byte("foobar")); !ok || val != 42 {
t.Fatalf("bad: %v", val)
}
if val, ok := txn1.Get([]byte("foobaz")); !ok || val != 43 {
t.Fatalf("bad: %v", val)
}
if val, ok := txn2.Get([]byte("foobar")); !ok || val != 42 {
t.Fatalf("bad: %v", val)
}
if val, ok := txn2.Get([]byte("foobaz")); !ok || val != 44 {
t.Fatalf("bad: %v", val)
}
// Commit txn2
r = txn2.Commit()
// Ensure we get the expected value foobar and foobaz
if val, ok := txn1.Get([]byte("foobar")); !ok || val != 42 {
t.Fatalf("bad: %v", val)
}
if val, ok := txn1.Get([]byte("foobaz")); !ok || val != 43 {
t.Fatalf("bad: %v", val)
}
if val, ok := r.Get([]byte("foobar")); !ok || val != 42 {
t.Fatalf("bad: %v", val)
}
if val, ok := r.Get([]byte("foobaz")); !ok || val != 44 {
t.Fatalf("bad: %v", val)
}
}
// isClosed returns true if the given channel is closed.
func isClosed(ch chan struct{}) bool {
select {
case <-ch:
return true
default:
return false
}
}
// hasAnyClosedMutateCh scans the given tree and returns true if there are any
// closed mutate channels on any nodes or leaves.
func hasAnyClosedMutateCh(r *Tree) bool {
for iter := r.root.rawIterator(); iter.Front() != nil; iter.Next() {
n := iter.Front()
if isClosed(n.mutateCh) {
return true
}
if n.isLeaf() && isClosed(n.leaf.mutateCh) {
return true
}
}
return false
}
func TestTrackMutate_SeekPrefixWatch(t *testing.T) {
for i := 0; i < 3; i++ {
r := New()
keys := []string{
"foo/bar/baz",
"foo/baz/bar",
"foo/zip/zap",
"foobar",
"zipzap",
}
for _, k := range keys {
r, _, _ = r.Insert([]byte(k), nil)
}
if r.Len() != len(keys) {
t.Fatalf("bad len: %v %v", r.Len(), len(keys))
}
iter := r.Root().Iterator()
rootWatch := iter.SeekPrefixWatch([]byte("nope"))
iter = r.Root().Iterator()
parentWatch := iter.SeekPrefixWatch([]byte("foo"))
iter = r.Root().Iterator()
leafWatch := iter.SeekPrefixWatch([]byte("foobar"))
iter = r.Root().Iterator()
missingWatch := iter.SeekPrefixWatch([]byte("foobarbaz"))
iter = r.Root().Iterator()
otherWatch := iter.SeekPrefixWatch([]byte("foo/b"))
// Write to a sub-child should trigger the leaf!
txn := r.Txn()
txn.TrackMutate(true)
txn.Insert([]byte("foobarbaz"), nil)
switch i {
case 0:
r = txn.Commit()
case 1:
r = txn.CommitOnly()
txn.Notify()
default:
r = txn.CommitOnly()
txn.slowNotify()
}
if hasAnyClosedMutateCh(r) {
t.Fatalf("bad")
}
// Verify root and parent triggered, and leaf affected
select {
case <-rootWatch:
default:
t.Fatalf("bad")
}
select {
case <-parentWatch:
default:
t.Fatalf("bad")
}
select {
case <-leafWatch:
default:
t.Fatalf("bad")
}
select {
case <-missingWatch:
default:
t.Fatalf("bad")
}
select {
case <-otherWatch:
t.Fatalf("bad")
default:
}
iter = r.Root().Iterator()
rootWatch = iter.SeekPrefixWatch([]byte("nope"))
iter = r.Root().Iterator()
parentWatch = iter.SeekPrefixWatch([]byte("foo"))
iter = r.Root().Iterator()
leafWatch = iter.SeekPrefixWatch([]byte("foobar"))
iter = r.Root().Iterator()
missingWatch = iter.SeekPrefixWatch([]byte("foobarbaz"))
// Delete to a sub-child should trigger the leaf!
txn = r.Txn()
txn.TrackMutate(true)
txn.Delete([]byte("foobarbaz"))
switch i {
case 0:
r = txn.Commit()
case 1:
r = txn.CommitOnly()
txn.Notify()
default:
r = txn.CommitOnly()
txn.slowNotify()
}
if hasAnyClosedMutateCh(r) {
t.Fatalf("bad")
}
// Verify root and parent triggered, and leaf affected
select {
case <-rootWatch:
default:
t.Fatalf("bad")
}
select {
case <-parentWatch:
default:
t.Fatalf("bad")
}
select {
case <-leafWatch:
default:
t.Fatalf("bad")
}
select {
case <-missingWatch:
default:
t.Fatalf("bad")
}
select {
case <-otherWatch:
t.Fatalf("bad")
default:
}
}
}
func TestTrackMutate_GetWatch(t *testing.T) {
for i := 0; i < 3; i++ {
r := New()
keys := []string{
"foo/bar/baz",
"foo/baz/bar",
"foo/zip/zap",
"foobar",
"zipzap",
}
for _, k := range keys {
r, _, _ = r.Insert([]byte(k), nil)
}
if r.Len() != len(keys) {
t.Fatalf("bad len: %v %v", r.Len(), len(keys))
}
rootWatch, _, ok := r.Root().GetWatch(nil)
if rootWatch == nil {
t.Fatalf("bad")
}
parentWatch, _, ok := r.Root().GetWatch([]byte("foo"))
if parentWatch == nil {
t.Fatalf("bad")
}
leafWatch, _, ok := r.Root().GetWatch([]byte("foobar"))
if !ok {
t.Fatalf("should be found")
}
if leafWatch == nil {
t.Fatalf("bad")
}
otherWatch, _, ok := r.Root().GetWatch([]byte("foo/b"))
if otherWatch == nil {
t.Fatalf("bad")
}
// Write to a sub-child should not trigger the leaf!
txn := r.Txn()
txn.TrackMutate(true)
txn.Insert([]byte("foobarbaz"), nil)
switch i {
case 0:
r = txn.Commit()
case 1:
r = txn.CommitOnly()
txn.Notify()
default:
r = txn.CommitOnly()
txn.slowNotify()
}
if hasAnyClosedMutateCh(r) {
t.Fatalf("bad")
}
// Verify root and parent triggered, not leaf affected
select {
case <-rootWatch:
default:
t.Fatalf("bad")
}
select {
case <-parentWatch:
default:
t.Fatalf("bad")
}
select {
case <-leafWatch:
t.Fatalf("bad")
default:
}
select {
case <-otherWatch:
t.Fatalf("bad")
default:
}
// Setup new watchers
rootWatch, _, ok = r.Root().GetWatch(nil)
if rootWatch == nil {
t.Fatalf("bad")
}
parentWatch, _, ok = r.Root().GetWatch([]byte("foo"))
if parentWatch == nil {
t.Fatalf("bad")
}
// Write to a exactly leaf should trigger the leaf!
txn = r.Txn()
txn.TrackMutate(true)
txn.Insert([]byte("foobar"), nil)
switch i {
case 0:
r = txn.Commit()
case 1:
r = txn.CommitOnly()
txn.Notify()
default:
r = txn.CommitOnly()
txn.slowNotify()
}
if hasAnyClosedMutateCh(r) {
t.Fatalf("bad")
}
select {
case <-rootWatch:
default:
t.Fatalf("bad")
}
select {
case <-parentWatch:
default:
t.Fatalf("bad")
}
select {
case <-leafWatch:
default:
t.Fatalf("bad")
}
select {
case <-otherWatch:
t.Fatalf("bad")
default:
}
// Setup all the watchers again
rootWatch, _, ok = r.Root().GetWatch(nil)
if rootWatch == nil {
t.Fatalf("bad")
}
parentWatch, _, ok = r.Root().GetWatch([]byte("foo"))
if parentWatch == nil {
t.Fatalf("bad")
}
leafWatch, _, ok = r.Root().GetWatch([]byte("foobar"))
if !ok {
t.Fatalf("should be found")
}
if leafWatch == nil {
t.Fatalf("bad")
}
// Delete to a sub-child should not trigger the leaf!
txn = r.Txn()
txn.TrackMutate(true)
txn.Delete([]byte("foobarbaz"))
switch i {
case 0:
r = txn.Commit()
case 1:
r = txn.CommitOnly()
txn.Notify()
default:
r = txn.CommitOnly()
txn.slowNotify()
}
if hasAnyClosedMutateCh(r) {
t.Fatalf("bad")
}
// Verify root and parent triggered, not leaf affected
select {
case <-rootWatch:
default:
t.Fatalf("bad")
}
select {
case <-parentWatch:
default:
t.Fatalf("bad")
}
select {
case <-leafWatch:
t.Fatalf("bad")
default:
}
select {
case <-otherWatch:
t.Fatalf("bad")
default:
}
// Setup new watchers
rootWatch, _, ok = r.Root().GetWatch(nil)
if rootWatch == nil {
t.Fatalf("bad")
}
parentWatch, _, ok = r.Root().GetWatch([]byte("foo"))
if parentWatch == nil {
t.Fatalf("bad")
}
// Write to a exactly leaf should trigger the leaf!
txn = r.Txn()
txn.TrackMutate(true)
txn.Delete([]byte("foobar"))
switch i {
case 0:
r = txn.Commit()
case 1:
r = txn.CommitOnly()
txn.Notify()
default:
r = txn.CommitOnly()
txn.slowNotify()
}
if hasAnyClosedMutateCh(r) {
t.Fatalf("bad")
}
select {
case <-rootWatch:
default:
t.Fatalf("bad")
}
select {
case <-parentWatch:
default:
t.Fatalf("bad")
}
select {
case <-leafWatch:
default:
t.Fatalf("bad")
}
select {
case <-otherWatch:
t.Fatalf("bad")
default:
}
}
}
func TestTrackMutate_HugeTxn(t *testing.T) {
r := New()
keys := []string{
"foo/bar/baz",
"foo/baz/bar",
"foo/zip/zap",
"foobar",
"nochange",
}
for i := 0; i < defaultModifiedCache; i++ {
key := fmt.Sprintf("aaa%d", i)
r, _, _ = r.Insert([]byte(key), nil)
}
for _, k := range keys {
r, _, _ = r.Insert([]byte(k), nil)
}
for i := 0; i < defaultModifiedCache; i++ {
key := fmt.Sprintf("zzz%d", i)
r, _, _ = r.Insert([]byte(key), nil)
}
if r.Len() != len(keys)+2*defaultModifiedCache {
t.Fatalf("bad len: %v %v", r.Len(), len(keys))
}
rootWatch, _, ok := r.Root().GetWatch(nil)
if rootWatch == nil {
t.Fatalf("bad")
}
parentWatch, _, ok := r.Root().GetWatch([]byte("foo"))
if parentWatch == nil {
t.Fatalf("bad")
}
leafWatch, _, ok := r.Root().GetWatch([]byte("foobar"))
if !ok {
t.Fatalf("should be found")
}
if leafWatch == nil {
t.Fatalf("bad")
}
nopeWatch, _, ok := r.Root().GetWatch([]byte("nochange"))
if !ok {
t.Fatalf("should be found")
}
if nopeWatch == nil {
t.Fatalf("bad")
}
beforeWatch, _, ok := r.Root().GetWatch([]byte("aaa123"))
if beforeWatch == nil {
t.Fatalf("bad")
}
afterWatch, _, ok := r.Root().GetWatch([]byte("zzz123"))
if afterWatch == nil {
t.Fatalf("bad")
}
// Start the transaction.
txn := r.Txn()
txn.TrackMutate(true)
// Add new nodes on both sides of the tree and delete enough nodes to
// overflow the tracking.
txn.Insert([]byte("aaa"), nil)
for i := 0; i < defaultModifiedCache; i++ {
key := fmt.Sprintf("aaa%d", i)
txn.Delete([]byte(key))
}
for i := 0; i < defaultModifiedCache; i++ {
key := fmt.Sprintf("zzz%d", i)
txn.Delete([]byte(key))
}
txn.Insert([]byte("zzz"), nil)
// Hit the leaf, and add a child so we make multiple mutations to the
// same node.
txn.Insert([]byte("foobar"), nil)
txn.Insert([]byte("foobarbaz"), nil)
// Commit and make sure we overflowed but didn't take on extra stuff.
r = txn.CommitOnly()
if !txn.trackOverflow || txn.trackChannels != nil {
t.Fatalf("bad")
}
// Now do the trigger.
txn.Notify()
// Make sure no closed channels escaped the transaction.
if hasAnyClosedMutateCh(r) {
t.Fatalf("bad")
}
// Verify the watches fired as expected.
select {
case <-rootWatch:
default:
t.Fatalf("bad")
}
select {
case <-parentWatch:
default:
t.Fatalf("bad")
}
select {
case <-leafWatch:
default:
t.Fatalf("bad")
}
select {
case <-nopeWatch:
t.Fatalf("bad")
default:
}
select {
case <-beforeWatch:
default:
t.Fatalf("bad")
}
select {
case <-afterWatch:
default:
t.Fatalf("bad")
}
}
func TestTrackMutate_mergeChild(t *testing.T) {
// This case does a delete of the "acb" leaf, which causes the "aca"
// leaf to get merged with the old "ac" node:
//
// [root] [root]
// |a |a
// [node] [node]
// b/ \c b/ \c
// (ab) [node] (ab) (aca)
// a/ \b
// (aca) (acb)
//
for i := 0; i < 3; i++ {
r := New()
r, _, _ = r.Insert([]byte("ab"), nil)
r, _, _ = r.Insert([]byte("aca"), nil)
r, _, _ = r.Insert([]byte("acb"), nil)
snapIter := r.root.rawIterator()
// Run through all notification methods as there were bugs in
// both that affected these operations. The slowNotify path
// would detect copied but otherwise identical leaves as changed
// and wrongly close channels. The normal path would fail to
// notify on a child node that had been merged.
txn := r.Txn()
txn.TrackMutate(true)
txn.Delete([]byte("acb"))
switch i {
case 0:
r = txn.Commit()
case 1:
r = txn.CommitOnly()
txn.Notify()
default:
r = txn.CommitOnly()
txn.slowNotify()
}
if hasAnyClosedMutateCh(r) {
t.Fatalf("bad")
}
// Run through the old tree and make sure the exact channels we
// expected were closed.
for ; snapIter.Front() != nil; snapIter.Next() {
n := snapIter.Front()
path := snapIter.Path()
switch path {
case "", "a", "ac": // parent nodes all change
if !isClosed(n.mutateCh) || n.leaf != nil {
t.Fatalf("bad")
}
case "ab": // unrelated node / leaf sees no change
if isClosed(n.mutateCh) || isClosed(n.leaf.mutateCh) {
t.Fatalf("bad")
}
case "aca": // this node gets merged, but the leaf doesn't change
if !isClosed(n.mutateCh) || isClosed(n.leaf.mutateCh) {
t.Fatalf("bad")
}
case "acb": // this node / leaf gets deleted
if !isClosed(n.mutateCh) || !isClosed(n.leaf.mutateCh) {
t.Fatalf("bad")
}
default:
t.Fatalf("bad: %s", path)
}
}
}
}
func TestTrackMutate_cachedNodeChange(t *testing.T) {
// This case does a delete of the "acb" leaf, which causes the "aca"
// leaf to get merged with the old "ac" node:
//
// [root] [root]
// |a |a
// [node] [node]
// b/ \c b/ \c
// (ab) [node] (ab) (aca*) <- this leaf gets modified
// a/ \b post-merge
// (aca) (acb)
//
// Then it makes a modification to the "aca" leaf on a node that will
// be in the cache, so this makes sure that the leaf watch fires.
for i := 0; i < 3; i++ {
r := New()
r, _, _ = r.Insert([]byte("ab"), nil)
r, _, _ = r.Insert([]byte("aca"), nil)
r, _, _ = r.Insert([]byte("acb"), nil)
snapIter := r.root.rawIterator()
txn := r.Txn()
txn.TrackMutate(true)
txn.Delete([]byte("acb"))
txn.Insert([]byte("aca"), nil)
switch i {
case 0:
r = txn.Commit()
case 1:
r = txn.CommitOnly()
txn.Notify()
default:
r = txn.CommitOnly()
txn.slowNotify()
}
if hasAnyClosedMutateCh(r) {
t.Fatalf("bad")
}
// Run through the old tree and make sure the exact channels we
// expected were closed.
for ; snapIter.Front() != nil; snapIter.Next() {
n := snapIter.Front()
path := snapIter.Path()
switch path {
case "", "a", "ac": // parent nodes all change
if !isClosed(n.mutateCh) || n.leaf != nil {
t.Fatalf("bad")
}
case "ab": // unrelated node / leaf sees no change
if isClosed(n.mutateCh) || isClosed(n.leaf.mutateCh) {
t.Fatalf("bad")
}
case "aca": // merge changes the node, then we update the leaf
if !isClosed(n.mutateCh) || !isClosed(n.leaf.mutateCh) {
t.Fatalf("bad")
}
case "acb": // this node / leaf gets deleted
if !isClosed(n.mutateCh) || !isClosed(n.leaf.mutateCh) {
t.Fatalf("bad")
}
default:
t.Fatalf("bad: %s", path)
}
}
}
}
func TestLenTxn(t *testing.T) {
r := New()
if r.Len() != 0 {
t.Fatalf("not starting with empty tree")
}
txn := r.Txn()
keys := []string{
"foo/bar/baz",
"foo/baz/bar",
"foo/zip/zap",
"foobar",
"nochange",
}
for _, k := range keys {
txn.Insert([]byte(k), nil)
}
r = txn.Commit()
if r.Len() != len(keys) {
t.Fatalf("bad: expected %d, got %d", len(keys), r.Len())
}
txn = r.Txn()
for _, k := range keys {
txn.Delete([]byte(k))
}
r = txn.Commit()
if r.Len() != 0 {
t.Fatalf("tree len should be zero, got %d", r.Len())
}
}
func TestIterateLowerBound(t *testing.T) {
// these should be defined in order
var fixedLenKeys = []string{
"00000",
"00001",
"00004",
"00010",
"00020",
"20020",
}
// these should be defined in order
var mixedLenKeys = []string{
"a1",
"abc",
"barbazboo",
"f",
"foo",
"found",
"zap",
"zip",
}
type exp struct {
keys []string
search string
want []string
}
cases := []exp{
{
fixedLenKeys,
"00000",
fixedLenKeys,
},
{
fixedLenKeys,
"00003",
[]string{
"00004",
"00010",
"00020",
"20020",
},
},
{
fixedLenKeys,
"00010",
[]string{
"00010",
"00020",
"20020",
},
},
{
fixedLenKeys,
"20000",
[]string{
"20020",
},
},
{
fixedLenKeys,
"20020",
[]string{
"20020",
},
},
{
fixedLenKeys,
"20022",
[]string{},
},
{
mixedLenKeys,
"A", // before all lower case letters
mixedLenKeys,
},
{
mixedLenKeys,
"a1",
mixedLenKeys,
},
{
mixedLenKeys,
"b",
[]string{
"barbazboo",
"f",
"foo",
"found",
"zap",
"zip",
},
},
{
mixedLenKeys,
"bar",
[]string{
"barbazboo",
"f",
"foo",
"found",
"zap",
"zip",
},
},
{
mixedLenKeys,
"barbazboo0",
[]string{
"f",
"foo",
"found",
"zap",
"zip",
},
},
{
mixedLenKeys,
"zippy",
[]string{},
},
{
mixedLenKeys,
"zi",
[]string{
"zip",
},
},
// This is a case found by TestIterateLowerBoundFuzz simplified by hand. The
// lowest node should be the first, but it is split on the same char as the
// second char in the search string. My initial implementation didn't take
// that into account (i.e. propagate the fact that we already know we are
// greater than the input key into the recursion). This would skip the first
// result.
{
[]string{
"bb",
"bc",
},
"ac",
[]string{"bb", "bc"},
},
// This is a case found by TestIterateLowerBoundFuzz.
{
[]string{"aaaba", "aabaa", "aabab", "aabcb", "aacca", "abaaa", "abacb", "abbcb", "abcaa", "abcba", "abcbb", "acaaa", "acaab", "acaac", "acaca", "acacb", "acbaa", "acbbb", "acbcc", "accca", "babaa", "babcc", "bbaaa", "bbacc", "bbbab", "bbbac", "bbbcc", "bbcab", "bbcca", "bbccc", "bcaac", "bcbca", "bcbcc", "bccac", "bccbc", "bccca", "caaab", "caacc", "cabac", "cabbb", "cabbc", "cabcb", "cacac", "cacbc", "cacca", "cbaba", "cbabb", "cbabc", "cbbaa", "cbbab", "cbbbc", "cbcbb", "cbcbc", "cbcca", "ccaaa", "ccabc", "ccaca", "ccacc", "ccbac", "cccaa", "cccac", "cccca"},
"cbacb",
[]string{"cbbaa", "cbbab", "cbbbc", "cbcbb", "cbcbc", "cbcca", "ccaaa", "ccabc", "ccaca", "ccacc", "ccbac", "cccaa", "cccac", "cccca"},
},
// Panic case found be TestIterateLowerBoundFuzz.
{
[]string{"gcgc"},
"",
[]string{"gcgc"},
},
// We SHOULD support keys that are prefixes of each other despite some
// confusion in the original implementation.
{
[]string{"f", "fo", "foo", "food", "bug"},
"foo",
[]string{"foo", "food"},
},
// We also support the empty key (which is a prefix of every other key) as a
// valid key to insert and search for.
{
[]string{"f", "fo", "foo", "food", "bug", ""},
"foo",
[]string{"foo", "food"},
},
{
[]string{"f", "bug", ""},
"",
[]string{"", "bug", "f"},
},
{
[]string{"f", "bug", "xylophone"},
"",
[]string{"bug", "f", "xylophone"},
},
// This is a case we realized we were not covering while fixing
// SeekReverseLowerBound and could panic before.
{
[]string{"bar", "foo00", "foo11"},
"foo",
[]string{"foo00", "foo11"},
},
}
for idx, test := range cases {
t.Run(fmt.Sprintf("case%03d", idx), func(t *testing.T) {
r := New()
// Insert keys
for _, k := range test.keys {
var ok bool
r, _, ok = r.Insert([]byte(k), nil)
if ok {
t.Fatalf("duplicate key %s in keys", k)
}
}
if r.Len() != len(test.keys) {
t.Fatal("failed adding keys")
}
// Get and seek iterator
root := r.Root()
iter := root.Iterator()
iter.SeekLowerBound([]byte(test.search))
// Consume all the keys
out := []string{}
for {
key, _, ok := iter.Next()
if !ok {
break
}
out = append(out, string(key))
}
if !reflect.DeepEqual(out, test.want) {
t.Fatalf("mis-match: key=%s\n got=%v\n want=%v", test.search,
out, test.want)
}
})
}
}
type readableString string
func (s readableString) Generate(rand *rand.Rand, size int) reflect.Value {
// Pick a random string from a limited alphabet that makes it easy to read the
// failure cases.
const letters = "abcdefg"
// Ignore size and make them all shortish to provoke bigger chance of hitting
// prefixes and more intersting tree shapes.
size = rand.Intn(8)
b := make([]byte, size)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return reflect.ValueOf(readableString(b))
}
func TestIterateLowerBoundFuzz(t *testing.T) {
r := New()
set := []string{}
// This specifies a property where each call adds a new random key to the radix
// tree.
//
// It also maintains a plain sorted list of the same set of keys and asserts
// that iterating from some random key to the end using LowerBound produces
// the same list as filtering all sorted keys that are lower.
radixAddAndScan := func(newKey, searchKey readableString) []string {
r, _, _ = r.Insert([]byte(newKey), nil)
t.Logf("NewKey: %q, SearchKey: %q", newKey, searchKey)
// Now iterate the tree from searchKey to the end
it := r.Root().Iterator()
result := []string{}
it.SeekLowerBound([]byte(searchKey))
for {
key, _, ok := it.Next()
if !ok {
break
}
result = append(result, string(key))
}
return result
}
sliceAddSortAndFilter := func(newKey, searchKey readableString) []string {
// Append the key to the set and re-sort
set = append(set, string(newKey))
sort.Strings(set)
t.Logf("Current Set: %#v", set)
t.Logf("Search Key: %#v %v", searchKey, "" >= string(searchKey))
result := []string{}
for i, k := range set {
// Check this is not a duplicate of the previous value. Note we don't just
// store the last string to compare because empty string is a valid value
// in the set and makes comparing on the first iteration awkward.
if i > 0 && set[i-1] == k {
continue
}
if k >= string(searchKey) {
result = append(result, k)
}
}
return result
}
if err := quick.CheckEqual(radixAddAndScan, sliceAddSortAndFilter, nil); err != nil {
t.Error(err)
}
}
func TestClone(t *testing.T) {
r := New()
t1 := r.Txn()
t1.Insert([]byte("foo"), 7)
t2 := t1.Clone()
t1.Insert([]byte("bar"), 42)
t2.Insert([]byte("baz"), 43)
if val, ok := t1.Get([]byte("foo")); !ok || val != 7 {
t.Fatalf("bad foo in t1")
}
if val, ok := t2.Get([]byte("foo")); !ok || val != 7 {
t.Fatalf("bad foo in t2")
}
if val, ok := t1.Get([]byte("bar")); !ok || val != 42 {
t.Fatalf("bad bar in t1")
}
if _, ok := t2.Get([]byte("bar")); ok {
t.Fatalf("bar found in t2")
}
if _, ok := t1.Get([]byte("baz")); ok {
t.Fatalf("baz found in t1")
}
if val, ok := t2.Get([]byte("baz")); !ok || val != 43 {
t.Fatalf("bad baz in t2")
}
}