// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
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" )
}
}
}