409 lines
7.2 KiB
Go
409 lines
7.2 KiB
Go
|
package radix
|
||
|
|
||
|
import (
|
||
|
crand "crypto/rand"
|
||
|
"fmt"
|
||
|
"reflect"
|
||
|
"sort"
|
||
|
"strconv"
|
||
|
"testing"
|
||
|
)
|
||
|
|
||
|
func TestRadix(t *testing.T) {
|
||
|
var min, max string
|
||
|
inp := make(map[string]interface{})
|
||
|
for i := 0; i < 1000; i++ {
|
||
|
gen := generateUUID()
|
||
|
inp[gen] = i
|
||
|
if gen < min || i == 0 {
|
||
|
min = gen
|
||
|
}
|
||
|
if gen > max || i == 0 {
|
||
|
max = gen
|
||
|
}
|
||
|
}
|
||
|
|
||
|
r := NewFromMap(inp)
|
||
|
if r.Len() != len(inp) {
|
||
|
t.Fatalf("bad length: %v %v", r.Len(), len(inp))
|
||
|
}
|
||
|
|
||
|
r.Walk(func(k string, v interface{}) bool {
|
||
|
println(k)
|
||
|
return false
|
||
|
})
|
||
|
|
||
|
for k, v := range inp {
|
||
|
out, ok := r.Get(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.Minimum()
|
||
|
if outMin != min {
|
||
|
t.Fatalf("bad minimum: %v %v", outMin, min)
|
||
|
}
|
||
|
outMax, _, _ := r.Maximum()
|
||
|
if outMax != max {
|
||
|
t.Fatalf("bad maximum: %v %v", outMax, max)
|
||
|
}
|
||
|
|
||
|
for k, v := range inp {
|
||
|
out, ok := r.Delete(k)
|
||
|
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())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestRoot(t *testing.T) {
|
||
|
r := New[bool]()
|
||
|
_, ok := r.Delete("")
|
||
|
if ok {
|
||
|
t.Fatalf("bad")
|
||
|
}
|
||
|
_, ok = r.Insert("", true)
|
||
|
if ok {
|
||
|
t.Fatalf("bad")
|
||
|
}
|
||
|
val, ok := r.Get("")
|
||
|
if !ok || val != true {
|
||
|
t.Fatalf("bad: %v", val)
|
||
|
}
|
||
|
val, ok = r.Delete("")
|
||
|
if !ok || val != true {
|
||
|
t.Fatalf("bad: %v", val)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestDelete(t *testing.T) {
|
||
|
|
||
|
r := New[bool]()
|
||
|
|
||
|
s := []string{"", "A", "AB"}
|
||
|
|
||
|
for _, ss := range s {
|
||
|
r.Insert(ss, true)
|
||
|
}
|
||
|
|
||
|
for _, ss := range s {
|
||
|
_, ok := r.Delete(ss)
|
||
|
if !ok {
|
||
|
t.Fatalf("bad %q", ss)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestDeletePrefix(t *testing.T) {
|
||
|
type exp struct {
|
||
|
inp []string
|
||
|
prefix string
|
||
|
out []string
|
||
|
numDeleted int
|
||
|
}
|
||
|
|
||
|
cases := []exp{
|
||
|
{[]string{"", "A", "AB", "ABC", "R", "S"}, "A", []string{"", "R", "S"}, 3},
|
||
|
{[]string{"", "A", "AB", "ABC", "R", "S"}, "ABC", []string{"", "A", "AB", "R", "S"}, 1},
|
||
|
{[]string{"", "A", "AB", "ABC", "R", "S"}, "", []string{}, 6},
|
||
|
{[]string{"", "A", "AB", "ABC", "R", "S"}, "S", []string{"", "A", "AB", "ABC", "R"}, 1},
|
||
|
{[]string{"", "A", "AB", "ABC", "R", "S"}, "SS", []string{"", "A", "AB", "ABC", "R", "S"}, 0},
|
||
|
}
|
||
|
|
||
|
for _, test := range cases {
|
||
|
r := New[bool]()
|
||
|
for _, ss := range test.inp {
|
||
|
r.Insert(ss, true)
|
||
|
}
|
||
|
|
||
|
deleted := r.DeletePrefix(test.prefix)
|
||
|
if deleted != test.numDeleted {
|
||
|
t.Fatalf("Bad delete, expected %v to be deleted but got %v", test.numDeleted, deleted)
|
||
|
}
|
||
|
|
||
|
out := []string{}
|
||
|
fn := func(s string, v bool) bool {
|
||
|
out = append(out, s)
|
||
|
return false
|
||
|
}
|
||
|
r.Walk(fn)
|
||
|
|
||
|
if !reflect.DeepEqual(out, test.out) {
|
||
|
t.Fatalf("mis-match: %v %v", out, test.out)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestLongestPrefix(t *testing.T) {
|
||
|
r := New[interface{}]()
|
||
|
|
||
|
keys := []string{
|
||
|
"",
|
||
|
"foo",
|
||
|
"foobar",
|
||
|
"foobarbaz",
|
||
|
"foobarbazzip",
|
||
|
"foozip",
|
||
|
}
|
||
|
for _, k := range keys {
|
||
|
r.Insert(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"},
|
||
|
}
|
||
|
for _, test := range cases {
|
||
|
m, _, ok := r.LongestPrefix(test.inp)
|
||
|
if !ok {
|
||
|
t.Fatalf("no match: %v", test)
|
||
|
}
|
||
|
if m != test.out {
|
||
|
t.Fatalf("mis-match: %v %v", m, test)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestWalkPrefix(t *testing.T) {
|
||
|
r := New[interface{}]()
|
||
|
|
||
|
keys := []string{
|
||
|
"foobar",
|
||
|
"foo/bar/baz",
|
||
|
"foo/baz/bar",
|
||
|
"foo/zip/zap",
|
||
|
"zipzap",
|
||
|
}
|
||
|
for _, k := range keys {
|
||
|
r.Insert(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"},
|
||
|
},
|
||
|
}
|
||
|
|
||
|
for _, test := range cases {
|
||
|
out := []string{}
|
||
|
fn := func(s string, v interface{}) bool {
|
||
|
out = append(out, s)
|
||
|
return false
|
||
|
}
|
||
|
r.WalkPrefix(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[interface{}]()
|
||
|
|
||
|
keys := []string{
|
||
|
"foo",
|
||
|
"foo/bar",
|
||
|
"foo/bar/baz",
|
||
|
"foo/baz/bar",
|
||
|
"foo/zip/zap",
|
||
|
"zipzap",
|
||
|
}
|
||
|
for _, k := range keys {
|
||
|
r.Insert(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{},
|
||
|
},
|
||
|
}
|
||
|
|
||
|
for _, test := range cases {
|
||
|
out := []string{}
|
||
|
fn := func(s string, v interface{}) bool {
|
||
|
out = append(out, s)
|
||
|
return false
|
||
|
}
|
||
|
r.WalkPath(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 TestWalkDelete(t *testing.T) {
|
||
|
r := New[interface{}]()
|
||
|
r.Insert("init0/0", nil)
|
||
|
r.Insert("init0/1", nil)
|
||
|
r.Insert("init0/2", nil)
|
||
|
r.Insert("init0/3", nil)
|
||
|
r.Insert("init1/0", nil)
|
||
|
r.Insert("init1/1", nil)
|
||
|
r.Insert("init1/2", nil)
|
||
|
r.Insert("init1/3", nil)
|
||
|
r.Insert("init2", nil)
|
||
|
|
||
|
deleteFn := func(s string, v interface{}) bool {
|
||
|
r.Delete(s)
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
r.WalkPrefix("init1", deleteFn)
|
||
|
|
||
|
for _, s := range []string{"init0/0", "init0/1", "init0/2", "init0/3", "init2"} {
|
||
|
if _, ok := r.Get(s); !ok {
|
||
|
t.Fatalf("expecting to still find %q", s)
|
||
|
}
|
||
|
}
|
||
|
if n := r.Len(); n != 5 {
|
||
|
t.Fatalf("expected to find exactly 5 nodes, instead found %d: %v", n, r.ToMap())
|
||
|
}
|
||
|
|
||
|
r.Walk(deleteFn)
|
||
|
if n := r.Len(); n != 0 {
|
||
|
t.Fatalf("expected to find exactly 0 nodes, instead found %d: %v", n, r.ToMap())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// generateUUID is used to generate a random UUID
|
||
|
func generateUUID() string {
|
||
|
buf := make([]byte, 16)
|
||
|
if _, err := crand.Read(buf); err != nil {
|
||
|
panic(fmt.Errorf("failed to read random bytes: %v", err))
|
||
|
}
|
||
|
|
||
|
return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x",
|
||
|
buf[0:4],
|
||
|
buf[4:6],
|
||
|
buf[6:8],
|
||
|
buf[8:10],
|
||
|
buf[10:16])
|
||
|
}
|
||
|
|
||
|
func BenchmarkInsert(b *testing.B) {
|
||
|
r := New[bool]()
|
||
|
for i := 0; i < 10000; i++ {
|
||
|
r.Insert(fmt.Sprintf("init%d", i), true)
|
||
|
}
|
||
|
b.ResetTimer()
|
||
|
for n := 0; n < b.N; n++ {
|
||
|
_, updated := r.Insert(strconv.Itoa(n), true)
|
||
|
if updated {
|
||
|
b.Fatal("bad")
|
||
|
}
|
||
|
}
|
||
|
}
|