2300 lines
58 KiB
Go
2300 lines
58 KiB
Go
//go:build generate
|
|
|
|
// Copyright (c) 2026 Karl Gaissmaier
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
//go:generate ./scripts/generate-table-tests.sh
|
|
|
|
package bart
|
|
|
|
// ### GENERATE DELETE START ###
|
|
|
|
// stub code for generator types and methods
|
|
// useful for gopls during development, deleted during go generate
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"iter"
|
|
"math/rand/v2"
|
|
"net/netip"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gaissmai/bart/internal/nodes"
|
|
"github.com/gaissmai/bart/internal/tests/golden"
|
|
"github.com/gaissmai/bart/internal/tests/random"
|
|
)
|
|
|
|
type (
|
|
_NODE_TYPE[V any] struct{}
|
|
|
|
_TABLE_TYPE[V any] struct {
|
|
root4 _NODE_TYPE[V]
|
|
root6 _NODE_TYPE[V]
|
|
}
|
|
)
|
|
|
|
func (*_NODE_TYPE[V]) StatsRec() (_ nodes.StatsT) { return }
|
|
|
|
func (*_TABLE_TYPE[V]) rootNodeByVersion(bool) (_ *_NODE_TYPE[V]) { return }
|
|
func (*_TABLE_TYPE[V]) sizeUpdate(bool, int) { return }
|
|
func (*_TABLE_TYPE[V]) dump(io.Writer) { return }
|
|
func (*_TABLE_TYPE[V]) dumpString() (_ string) { return }
|
|
func (*_TABLE_TYPE[V]) fprint(io.Writer, bool) (_ error) { return }
|
|
func (*_TABLE_TYPE[V]) Fprint(io.Writer) (_ error) { return }
|
|
func (*_TABLE_TYPE[V]) Size() (_ int) { return }
|
|
func (*_TABLE_TYPE[V]) Size4() (_ int) { return }
|
|
func (*_TABLE_TYPE[V]) Size6() (_ int) { return }
|
|
func (*_TABLE_TYPE[V]) Insert(netip.Prefix, V) { return }
|
|
func (*_TABLE_TYPE[V]) Get(netip.Prefix) (_ V, _ bool) { return }
|
|
func (*_TABLE_TYPE[V]) Delete(netip.Prefix) { return }
|
|
func (*_TABLE_TYPE[V]) Modify(netip.Prefix, func(V, bool) (V, bool)) { return }
|
|
func (*_TABLE_TYPE[V]) Clone() (_ *_TABLE_TYPE[V]) { return }
|
|
func (*_TABLE_TYPE[V]) Union(*_TABLE_TYPE[V]) { return }
|
|
func (*_TABLE_TYPE[V]) Equal(*_TABLE_TYPE[V]) (_ bool) { return }
|
|
func (*_TABLE_TYPE[V]) OverlapsPrefix(netip.Prefix) (_ bool) { return }
|
|
func (*_TABLE_TYPE[V]) Overlaps(*_TABLE_TYPE[V]) (_ bool) { return }
|
|
func (*_TABLE_TYPE[V]) Overlaps4(*_TABLE_TYPE[V]) (_ bool) { return }
|
|
func (*_TABLE_TYPE[V]) Overlaps6(*_TABLE_TYPE[V]) (_ bool) { return }
|
|
func (*_TABLE_TYPE[V]) Contains(netip.Addr) (_ bool) { return }
|
|
func (*_TABLE_TYPE[V]) Lookup(netip.Addr) (_ V, _ bool) { return }
|
|
func (*_TABLE_TYPE[V]) LookupPrefix(netip.Prefix) (_ V, _ bool) { return }
|
|
func (*_TABLE_TYPE[V]) LookupPrefixLPM(netip.Prefix) (_ netip.Prefix, _ V, _ bool) { return }
|
|
|
|
func (*_TABLE_TYPE[V]) InsertPersist(netip.Prefix, V) (_ *_TABLE_TYPE[V]) { return }
|
|
func (*_TABLE_TYPE[V]) DeletePersist(netip.Prefix) (_ *_TABLE_TYPE[V]) { return }
|
|
func (*_TABLE_TYPE[V]) UnionPersist(*_TABLE_TYPE[V]) (_ *_TABLE_TYPE[V]) { return }
|
|
func (*_TABLE_TYPE[V]) ModifyPersist(netip.Prefix, func(V, bool) (V, bool)) (_ *_TABLE_TYPE[V]) {
|
|
return
|
|
}
|
|
func (*_TABLE_TYPE[V]) MarshalText() (_ []byte, _ error) { return }
|
|
func (*_TABLE_TYPE[V]) MarshalJSON() (_ []byte, _ error) { return }
|
|
func (*_TABLE_TYPE[V]) DumpList4() (_ []DumpListNode[V]) { return }
|
|
func (*_TABLE_TYPE[V]) DumpList6() (_ []DumpListNode[V]) { return }
|
|
|
|
func (*_TABLE_TYPE[V]) All() (_ iter.Seq2[netip.Prefix, V]) { return }
|
|
func (*_TABLE_TYPE[V]) All4() (_ iter.Seq2[netip.Prefix, V]) { return }
|
|
func (*_TABLE_TYPE[V]) All6() (_ iter.Seq2[netip.Prefix, V]) { return }
|
|
|
|
func (*_TABLE_TYPE[V]) AllSorted() (_ iter.Seq2[netip.Prefix, V]) { return }
|
|
func (*_TABLE_TYPE[V]) AllSorted4() (_ iter.Seq2[netip.Prefix, V]) { return }
|
|
func (*_TABLE_TYPE[V]) AllSorted6() (_ iter.Seq2[netip.Prefix, V]) { return }
|
|
|
|
func (*_TABLE_TYPE[V]) Subnets(netip.Prefix) (_ iter.Seq2[netip.Prefix, V]) { return }
|
|
func (*_TABLE_TYPE[V]) Supernets(netip.Prefix) (_ iter.Seq2[netip.Prefix, V]) { return }
|
|
|
|
// ### GENERATE DELETE END ###
|
|
|
|
// ############ tests ################################
|
|
|
|
// flatSorted, just a helper to compare with golden table.
|
|
func (t *_TABLE_TYPE[V]) flatSorted() golden.Table[V] {
|
|
var flat golden.Table[V]
|
|
|
|
for p, v := range t.AllSorted() {
|
|
flat = append(flat, golden.TableItem[V]{Pfx: p, Val: v})
|
|
}
|
|
|
|
return flat
|
|
}
|
|
|
|
func TestTableNilReceiver__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var tbl1 *_TABLE_TYPE[any] = nil
|
|
|
|
ip4 := mpa("127.0.0.1")
|
|
ip6 := mpa("::1")
|
|
|
|
pfx4 := mpp("127.0.0.0/8")
|
|
pfx6 := mpp("::1/128")
|
|
|
|
tbl2 := new(_TABLE_TYPE[any])
|
|
tbl2.Insert(pfx4, true)
|
|
tbl2.Insert(pfx6, true)
|
|
|
|
t.Run("mustPanicNilReceiver", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mustPanic(t, "Size", func() { tbl1.Size() })
|
|
mustPanic(t, "Size4", func() { tbl1.Size4() })
|
|
mustPanic(t, "Size6", func() { tbl1.Size6() })
|
|
|
|
mustPanic(t, "Get", func() { tbl1.Get(pfx4) })
|
|
mustPanic(t, "Insert", func() { tbl1.Insert(pfx4, nil) })
|
|
mustPanic(t, "InsertPersist", func() { tbl1.InsertPersist(pfx4, nil) })
|
|
mustPanic(t, "Delete", func() { tbl1.Delete(pfx4) })
|
|
mustPanic(t, "DeletePersist", func() { tbl1.DeletePersist(pfx4) })
|
|
mustPanic(t, "Modify", func() { tbl1.Modify(pfx4, nil) })
|
|
mustPanic(t, "ModifyPersist", func() { tbl1.ModifyPersist(pfx4, nil) })
|
|
mustPanic(t, "Contains", func() { tbl1.Contains(ip4) })
|
|
mustPanic(t, "Lookup", func() { tbl1.Lookup(ip6) })
|
|
mustPanic(t, "LookupPrefix", func() { tbl1.LookupPrefix(pfx4) })
|
|
mustPanic(t, "LookupPrefixLPM", func() { tbl1.LookupPrefixLPM(pfx4) })
|
|
mustPanic(t, "Clone", func() { tbl1.Clone() })
|
|
mustPanic(t, "Union", func() { tbl1.Union(tbl2) })
|
|
mustPanic(t, "UnionPersist", func() { tbl1.UnionPersist(tbl2) })
|
|
mustPanic(t, "Overlaps", func() { tbl1.Overlaps(tbl2) })
|
|
mustPanic(t, "Overlaps4", func() { tbl1.Overlaps4(tbl2) })
|
|
mustPanic(t, "Overlaps6", func() { tbl1.Overlaps6(tbl2) })
|
|
mustPanic(t, "OverlapsPrefix", func() { tbl1.OverlapsPrefix(pfx4) })
|
|
mustPanic(t, "OverlapsPrefix", func() { tbl1.OverlapsPrefix(pfx6) })
|
|
mustPanic(t, "Equal", func() { tbl1.Equal(tbl2) })
|
|
mustPanic(t, "DumpList4", func() { tbl1.DumpList4() })
|
|
mustPanic(t, "DumpList6", func() { tbl1.DumpList6() })
|
|
mustPanic(t, "Fprint", func() { tbl1.Fprint(nil) })
|
|
mustPanic(t, "MarshalJSON", func() { _, _ = tbl1.MarshalJSON() })
|
|
mustPanic(t, "MarshalText", func() { _, _ = tbl1.MarshalText() })
|
|
|
|
mustPanicRangeOverFunc[any](t, "All", tbl1.All)
|
|
mustPanicRangeOverFunc[any](t, "All4", tbl1.All4)
|
|
mustPanicRangeOverFunc[any](t, "All6", tbl1.All6)
|
|
mustPanicRangeOverFunc[any](t, "AllSorted", tbl1.AllSorted)
|
|
mustPanicRangeOverFunc[any](t, "AllSorted4", tbl1.AllSorted4)
|
|
mustPanicRangeOverFunc[any](t, "AllSorted6", tbl1.AllSorted6)
|
|
mustPanicRangeOverFunc[any](t, "Subnets", tbl1.Subnets)
|
|
mustPanicRangeOverFunc[any](t, "Supernets", tbl1.Supernets)
|
|
})
|
|
}
|
|
|
|
func TestTableNilArguments__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("mustPanicNilArgs", func(t *testing.T) {
|
|
t.Parallel()
|
|
tbl := new(_TABLE_TYPE[bool])
|
|
|
|
tbl.Insert(mpp("127.0.0.1/32"), true)
|
|
tbl.Insert(mpp("::1/128"), true)
|
|
|
|
mustPanic(t, "Union(nil)", func() { tbl.Union(nil) })
|
|
mustPanic(t, "UnionPersist(nil)", func() { tbl.UnionPersist(nil) })
|
|
mustPanic(t, "Overlaps(nil)", func() { tbl.Overlaps(nil) })
|
|
mustPanic(t, "Overlaps4(nil)", func() { tbl.Overlaps4(nil) })
|
|
mustPanic(t, "Overlaps6(nil)", func() { tbl.Overlaps6(nil) })
|
|
mustPanic(t, "Equal(nil)", func() { tbl.Equal(nil) })
|
|
})
|
|
}
|
|
|
|
func TestTableInvalid__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tbl1 := new(_TABLE_TYPE[any])
|
|
tbl2 := new(_TABLE_TYPE[any])
|
|
|
|
var zeroIP netip.Addr
|
|
var zeroPfx netip.Prefix
|
|
|
|
noPanic(t, "All", func() { tbl1.All() })
|
|
noPanic(t, "All4", func() { tbl1.All4() })
|
|
noPanic(t, "All6", func() { tbl1.All6() })
|
|
noPanic(t, "AllSorted", func() { tbl1.AllSorted() })
|
|
noPanic(t, "AllSorted4", func() { tbl1.AllSorted4() })
|
|
noPanic(t, "AllSorted6", func() { tbl1.AllSorted6() })
|
|
noPanic(t, "Clone", func() { tbl1.Clone() })
|
|
noPanic(t, "Contains", func() { tbl1.Contains(zeroIP) })
|
|
noPanic(t, "Delete", func() { tbl1.Delete(zeroPfx) })
|
|
noPanic(t, "DeletePersist", func() { tbl1.DeletePersist(zeroPfx) })
|
|
noPanic(t, "DumpList4", func() { tbl1.DumpList4() })
|
|
noPanic(t, "DumpList6", func() { tbl1.DumpList6() })
|
|
noPanic(t, "Equal", func() { tbl1.Equal(tbl2) })
|
|
noPanic(t, "Fprint", func() { tbl1.Fprint(nil) })
|
|
noPanic(t, "Get", func() { tbl1.Get(zeroPfx) })
|
|
noPanic(t, "Insert", func() { tbl1.Insert(zeroPfx, nil) })
|
|
noPanic(t, "InsertPersist", func() { tbl1.InsertPersist(zeroPfx, nil) })
|
|
noPanic(t, "Lookup", func() { tbl1.Lookup(zeroIP) })
|
|
noPanic(t, "LookupPrefix", func() { tbl1.LookupPrefix(zeroPfx) })
|
|
noPanic(t, "LookupPrefixLPM", func() { tbl1.LookupPrefixLPM(zeroPfx) })
|
|
noPanic(t, "MarshalJSON", func() { _, _ = tbl1.MarshalJSON() })
|
|
noPanic(t, "MarshalText", func() { _, _ = tbl1.MarshalText() })
|
|
noPanic(t, "Modify", func() { tbl1.Modify(zeroPfx, nil) })
|
|
noPanic(t, "ModifyPersist", func() { tbl1.ModifyPersist(zeroPfx, nil) })
|
|
noPanic(t, "Overlaps", func() { tbl1.Overlaps(tbl2) })
|
|
noPanic(t, "Overlaps4", func() { tbl1.Overlaps4(tbl2) })
|
|
noPanic(t, "Overlaps6", func() { tbl1.Overlaps6(tbl2) })
|
|
noPanic(t, "OverlapsPrefix", func() { tbl1.OverlapsPrefix(zeroPfx) })
|
|
noPanic(t, "Size", func() { tbl1.Size() })
|
|
noPanic(t, "Size4", func() { tbl1.Size4() })
|
|
noPanic(t, "Size6", func() { tbl1.Size6() })
|
|
noPanic(t, "Subnets", func() { tbl1.Subnets(zeroPfx) })
|
|
noPanic(t, "Supernets", func() { tbl1.Supernets(zeroPfx) })
|
|
noPanic(t, "Union", func() { tbl1.Union(tbl2) })
|
|
noPanic(t, "UnionPersist", func() { tbl1.UnionPersist(tbl2) })
|
|
}
|
|
|
|
func TestTableContainsCompare__TABLE_TYPE(t *testing.T) {
|
|
// Create large route tables repeatedly, and compare Table's
|
|
// behavior to a naive and slow but correct implementation.
|
|
t.Parallel()
|
|
|
|
n := workLoadN()
|
|
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
pfxs := random.RealWorldPrefixes(prng, n)
|
|
|
|
gold := new(golden.Table[int])
|
|
tbl := new(_TABLE_TYPE[int])
|
|
|
|
for i, p := range pfxs {
|
|
gold.Insert(p, i)
|
|
tbl.Insert(p, i)
|
|
}
|
|
|
|
for range n {
|
|
ip := random.IP(prng)
|
|
|
|
_, goldOK := gold.Lookup(ip)
|
|
tblOK := tbl.Contains(ip)
|
|
|
|
if goldOK != tblOK {
|
|
t.Fatalf("Contains(%q) = %v, want %v", ip, tblOK, goldOK)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableLookupCompare__TABLE_TYPE(t *testing.T) {
|
|
// Create large route tables repeatedly, and compare Table's
|
|
// behavior to a naive and slow but correct implementation.
|
|
t.Parallel()
|
|
|
|
n := workLoadN()
|
|
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
pfxs := random.RealWorldPrefixes(prng, n)
|
|
|
|
gold := new(golden.Table[int])
|
|
tbl := new(_TABLE_TYPE[int])
|
|
|
|
for i, pfx := range pfxs {
|
|
gold.Insert(pfx, i)
|
|
tbl.Insert(pfx, i)
|
|
}
|
|
|
|
for range n {
|
|
ip := random.IP(prng)
|
|
|
|
goldVal, goldOK := gold.Lookup(ip)
|
|
tblVal, tblOK := tbl.Lookup(ip)
|
|
|
|
if goldOK != tblOK {
|
|
t.Fatalf("Lookup(%q) = (_, %v), want %v", ip, tblOK, goldOK)
|
|
}
|
|
|
|
// Skip value comparison for liteTable (no real payload)
|
|
if _, isLite := any(tbl).(*liteTable[int]); !isLite {
|
|
if goldVal != tblVal {
|
|
t.Fatalf("Lookup(%q) = (%v, %v), want (%v, %v)", ip, tblVal, tblOK, goldVal, goldOK)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableLookupPrefixUnmasked__TABLE_TYPE(t *testing.T) {
|
|
// test that the pfx must not be masked on input for LookupPrefix
|
|
t.Parallel()
|
|
|
|
tbl := new(_TABLE_TYPE[any])
|
|
tbl.Insert(mpp("10.20.30.0/24"), nil)
|
|
tbl.Insert(mpp("2001:db8::/32"), nil)
|
|
|
|
// not normalized pfxs
|
|
tests := []struct {
|
|
probe netip.Prefix
|
|
wantLPM netip.Prefix
|
|
wantOk bool
|
|
}{
|
|
{
|
|
probe: netip.MustParsePrefix("10.20.30.40/0"),
|
|
wantLPM: netip.Prefix{},
|
|
wantOk: false,
|
|
},
|
|
{
|
|
probe: netip.MustParsePrefix("10.20.30.40/23"),
|
|
wantLPM: netip.Prefix{},
|
|
wantOk: false,
|
|
},
|
|
{
|
|
probe: netip.MustParsePrefix("10.20.30.40/24"),
|
|
wantLPM: mpp("10.20.30.0/24"),
|
|
wantOk: true,
|
|
},
|
|
{
|
|
probe: netip.MustParsePrefix("10.20.30.40/25"),
|
|
wantLPM: mpp("10.20.30.0/24"),
|
|
wantOk: true,
|
|
},
|
|
{
|
|
probe: netip.MustParsePrefix("10.20.30.40/32"),
|
|
wantLPM: mpp("10.20.30.0/24"),
|
|
wantOk: true,
|
|
},
|
|
// IPv6 counterparts
|
|
{
|
|
probe: netip.MustParsePrefix("2001:db8::1/0"),
|
|
wantLPM: netip.Prefix{},
|
|
wantOk: false,
|
|
},
|
|
{
|
|
probe: netip.MustParsePrefix("2001:db8::1/31"),
|
|
wantLPM: netip.Prefix{},
|
|
wantOk: false,
|
|
},
|
|
{
|
|
probe: netip.MustParsePrefix("2001:db8::1/32"),
|
|
wantLPM: mpp("2001:db8::/32"),
|
|
wantOk: true,
|
|
},
|
|
{
|
|
probe: netip.MustParsePrefix("2001:db8::1/64"),
|
|
wantLPM: mpp("2001:db8::/32"),
|
|
wantOk: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
_, got := tbl.LookupPrefix(tc.probe)
|
|
if got != tc.wantOk {
|
|
t.Errorf("LookupPrefix non canonical prefix (%s), got: %v, want: %v", tc.probe, got, tc.wantOk)
|
|
}
|
|
|
|
lpm, _, got := tbl.LookupPrefixLPM(tc.probe)
|
|
if got != tc.wantOk {
|
|
t.Errorf("LookupPrefixLPM non canonical prefix (%s), got: %v, want: %v", tc.probe, got, tc.wantOk)
|
|
}
|
|
if lpm != tc.wantLPM {
|
|
t.Errorf("LookupPrefixLPM non canonical prefix (%s), got: %v, want: %v", tc.probe, lpm, tc.wantLPM)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableLookupPrefixCompare__TABLE_TYPE(t *testing.T) {
|
|
// Create large route tables repeatedly, and compare Table's
|
|
// behavior to a naive and slow but correct implementation.
|
|
t.Parallel()
|
|
|
|
n := workLoadN()
|
|
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
pfxs := random.RealWorldPrefixes(prng, n)
|
|
|
|
gold := new(golden.Table[int])
|
|
tbl := new(_TABLE_TYPE[int])
|
|
for i, pfx := range pfxs {
|
|
gold.Insert(pfx, i)
|
|
tbl.Insert(pfx, i)
|
|
}
|
|
|
|
for range n {
|
|
pfx := random.Prefix(prng)
|
|
|
|
goldVal, goldOK := gold.LookupPrefix(pfx)
|
|
tblVal, tblOK := tbl.LookupPrefix(pfx)
|
|
|
|
if goldOK != tblOK {
|
|
t.Fatalf("LookupPrefix(%q) = (_, %v), want (_, %v)", pfx, tblOK, goldOK)
|
|
}
|
|
|
|
// Skip value comparison for liteTable (no real payload)
|
|
if _, isLite := any(tbl).(*liteTable[int]); !isLite {
|
|
if goldVal != tblVal {
|
|
t.Fatalf("LookupPrefix(%q) = (%v, %v), want (%v, %v)", pfx, tblVal, tblOK, goldVal, goldOK)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableLookupPrefixLPMCompare__TABLE_TYPE(t *testing.T) {
|
|
// Create large route tables repeatedly, and compare Table's
|
|
// behavior to a naive and slow but correct implementation.
|
|
t.Parallel()
|
|
|
|
n := workLoadN()
|
|
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
pfxs := random.RealWorldPrefixes(prng, n)
|
|
|
|
gold := new(golden.Table[int])
|
|
tbl := new(_TABLE_TYPE[int])
|
|
for i, pfx := range pfxs {
|
|
gold.Insert(pfx, i)
|
|
tbl.Insert(pfx, i)
|
|
}
|
|
|
|
for range n {
|
|
pfx := random.Prefix(prng)
|
|
|
|
goldLPM, goldVal, goldOK := gold.LookupPrefixLPM(pfx)
|
|
tblLPM, tblVal, tblOK := tbl.LookupPrefixLPM(pfx)
|
|
|
|
if goldOK != tblOK {
|
|
t.Fatalf("LookupPrefixLPM(%q) = (_, _, %v), want (_, _, %v)", pfx, tblOK, goldOK)
|
|
}
|
|
|
|
if goldLPM != tblLPM {
|
|
t.Fatalf("LookupPrefixLPM(%q) = ( %v, _, _), want ( %v, _, _)", pfx, tblLPM, goldLPM)
|
|
}
|
|
|
|
// Skip value comparison for liteTable (no real payload)
|
|
if _, isLite := any(tbl).(*liteTable[int]); !isLite {
|
|
if goldVal != tblVal {
|
|
t.Fatalf("LookupPrefixLPM(%q) = (_, %v, _), want (_, %v, _)", pfx, tblVal, goldVal)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableInsertShuffled__TABLE_TYPE(t *testing.T) {
|
|
// The order in which you insert prefixes into a route table
|
|
// should not matter, as long as you're inserting the same set of
|
|
// routes.
|
|
t.Parallel()
|
|
|
|
n := workLoadN()
|
|
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
pfxs := random.RealWorldPrefixes(prng, n)
|
|
|
|
for range 10 {
|
|
pfxs2 := slices.Clone(pfxs)
|
|
prng.Shuffle(len(pfxs2), func(i, j int) { pfxs2[i], pfxs2[j] = pfxs2[j], pfxs2[i] })
|
|
|
|
tbl1 := new(_TABLE_TYPE[string])
|
|
tbl2 := new(_TABLE_TYPE[string])
|
|
|
|
for _, pfx := range pfxs {
|
|
tbl1.Insert(pfx, pfx.String())
|
|
tbl1.Insert(pfx, pfx.String()) // idempotent
|
|
}
|
|
for _, pfx := range pfxs2 {
|
|
tbl2.Insert(pfx, pfx.String()) // idempotent
|
|
}
|
|
|
|
if tbl1.dumpString() != tbl2.dumpString() {
|
|
t.Fatal("tbl1 and tbl2 have different dumpString representation")
|
|
}
|
|
if !tbl1.Equal(tbl2) {
|
|
t.Fatal("expected Equal")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableInsertPersistShuffled__TABLE_TYPE(t *testing.T) {
|
|
// The order in which you insert prefixes into a route table
|
|
// should not matter, as long as you're inserting the same set of
|
|
// routes.
|
|
t.Parallel()
|
|
|
|
n := workLoadN()
|
|
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
pfxs := random.RealWorldPrefixes(prng, n)
|
|
|
|
for range 10 {
|
|
pfxs2 := slices.Clone(pfxs)
|
|
prng.Shuffle(len(pfxs2), func(i, j int) { pfxs2[i], pfxs2[j] = pfxs2[j], pfxs2[i] })
|
|
|
|
tbl1 := new(_TABLE_TYPE[string])
|
|
tbl2 := new(_TABLE_TYPE[string])
|
|
|
|
// bart1 is mutable
|
|
for _, pfx := range pfxs {
|
|
tbl1.Insert(pfx, pfx.String())
|
|
}
|
|
|
|
// bart2 is persistent
|
|
for _, pfx := range pfxs2 {
|
|
tbl2 = tbl2.InsertPersist(pfx, pfx.String())
|
|
}
|
|
|
|
if tbl1.dumpString() != tbl2.dumpString() {
|
|
t.Fatal("mutable and immutable table have different dumpString representation")
|
|
}
|
|
|
|
if !tbl1.Equal(tbl2) {
|
|
t.Fatal("expected Equal")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableDeleteCompare__TABLE_TYPE(t *testing.T) {
|
|
// Create large route tables repeatedly, delete half of their
|
|
// prefixes, and compare Table's behavior to a naive and slow but
|
|
// correct implementation.
|
|
t.Parallel()
|
|
|
|
var (
|
|
n = workLoadN()
|
|
prng = rand.New(rand.NewPCG(42, 42))
|
|
numPrefixes = n // total prefixes to insert (test deletes 50% of them)
|
|
numPerFamily = numPrefixes / 2
|
|
deleteCut = numPerFamily / 2
|
|
probes = 3
|
|
)
|
|
|
|
for range probes {
|
|
all4 := random.RealWorldPrefixes4(prng, numPerFamily)
|
|
all6 := random.RealWorldPrefixes6(prng, numPerFamily)
|
|
|
|
pfxs := slices.Concat(all4, all6)
|
|
toDelete := slices.Concat(all4[deleteCut:], all6[deleteCut:])
|
|
|
|
gold := new(golden.Table[string])
|
|
tbl := new(_TABLE_TYPE[string])
|
|
|
|
for _, pfx := range pfxs {
|
|
gold.Insert(pfx, pfx.String())
|
|
tbl.Insert(pfx, pfx.String())
|
|
}
|
|
|
|
for _, pfx := range toDelete {
|
|
gold.Delete(pfx)
|
|
tbl.Delete(pfx)
|
|
}
|
|
|
|
gold.Sort()
|
|
|
|
tblFlat := tbl.flatSorted()
|
|
|
|
// Skip value comparison for liteTable (no real payload)
|
|
if _, isLite := any(tbl).(*liteTable[string]); isLite {
|
|
if !slices.Equal(gold.AllSorted(), tblFlat.AllSorted()) {
|
|
t.Fatal("expected Equal")
|
|
}
|
|
} else {
|
|
if !slices.Equal(*gold, tblFlat) {
|
|
t.Fatal("expected Equal")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableDeleteShuffled__TABLE_TYPE(t *testing.T) {
|
|
// The order in which you delete prefixes from a route table
|
|
// should not matter, as long as you're deleting the same set of
|
|
// routes.
|
|
t.Parallel()
|
|
|
|
n := workLoadN()
|
|
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
|
|
var (
|
|
numPrefixes = n // prefixes to insert (test deletes 50% of them)
|
|
numPerFamily = numPrefixes / 2
|
|
deleteCut = numPerFamily / 2
|
|
)
|
|
|
|
for range 10 {
|
|
all4 := random.RealWorldPrefixes4(prng, numPerFamily)
|
|
all6 := random.RealWorldPrefixes6(prng, numPerFamily)
|
|
|
|
pfxs := slices.Concat(all4, all6)
|
|
toDelete := slices.Concat(all4[deleteCut:], all6[deleteCut:])
|
|
|
|
tbl := new(_TABLE_TYPE[string])
|
|
|
|
// insert
|
|
for _, pfx := range pfxs {
|
|
tbl.Insert(pfx, pfx.String())
|
|
}
|
|
|
|
// delete
|
|
for _, pfx := range toDelete {
|
|
tbl.Delete(pfx)
|
|
}
|
|
|
|
pfxs2 := slices.Clone(pfxs)
|
|
toDelete2 := slices.Clone(toDelete)
|
|
prng.Shuffle(len(toDelete2), func(i, j int) { toDelete2[i], toDelete2[j] = toDelete2[j], toDelete2[i] })
|
|
|
|
tbl2 := new(_TABLE_TYPE[string])
|
|
|
|
// insert
|
|
for _, pfx := range pfxs2 {
|
|
tbl2.Insert(pfx, pfx.String())
|
|
}
|
|
|
|
// delete
|
|
for _, pfx := range toDelete2 {
|
|
tbl2.Delete(pfx)
|
|
}
|
|
|
|
if !tbl.Equal(tbl2) {
|
|
t.Fatal("expect equal")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableDeleteIsReverseOfInsert__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
n := workLoadN()
|
|
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
// Insert n prefixes, then delete those same prefixes in reverse
|
|
// order. Each deletion should exactly undo the internal structure
|
|
// changes that each insert did.
|
|
|
|
tbl := new(_TABLE_TYPE[string])
|
|
want := tbl.dumpString()
|
|
|
|
prefixes := random.RealWorldPrefixes(prng, n)
|
|
|
|
defer func() {
|
|
if t.Failed() {
|
|
t.Logf("the prefixes that fail the test: %v\n", prefixes)
|
|
}
|
|
}()
|
|
|
|
for _, p := range prefixes {
|
|
tbl.Insert(p, p.String())
|
|
}
|
|
|
|
for i := len(prefixes) - 1; i >= 0; i-- {
|
|
tbl.Delete(prefixes[i])
|
|
}
|
|
if got := tbl.dumpString(); got != want {
|
|
t.Fatalf("after delete, mismatch:\n\n got: %s\n\nwant: %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestTableDeleteButOne__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
// Insert n prefixes, then delete all but one
|
|
n := workLoadN()
|
|
|
|
for range 10 {
|
|
|
|
tbl := new(_TABLE_TYPE[any])
|
|
prefixes := random.RealWorldPrefixes(prng, n)
|
|
|
|
for _, p := range prefixes {
|
|
tbl.Insert(p, nil)
|
|
}
|
|
|
|
// shuffle the prefixes
|
|
prng.Shuffle(n, func(i, j int) {
|
|
prefixes[i], prefixes[j] = prefixes[j], prefixes[i]
|
|
})
|
|
|
|
// skip the first
|
|
for i := 1; i < len(prefixes); i++ {
|
|
tbl.Delete(prefixes[i])
|
|
}
|
|
|
|
if size := tbl.Size(); size != 1 {
|
|
t.Fatalf("Size(), got %d, want 1", size)
|
|
}
|
|
|
|
stats4 := tbl.root4.StatsRec()
|
|
stats6 := tbl.root6.StatsRec()
|
|
|
|
if nodes := stats4.SubNodes + stats6.SubNodes; nodes != 1 {
|
|
t.Fatalf("delete but one, want nodes: 1, got: %d\n%s", nodes, tbl.dumpString())
|
|
}
|
|
|
|
sum := stats4.Prefixes + stats4.Leaves + stats4.Fringes +
|
|
stats6.Prefixes + stats6.Leaves + stats6.Fringes
|
|
|
|
if sum != 1 {
|
|
t.Fatalf("delete but one, only one item must be left, but: %d\n%s", sum, tbl.dumpString())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableGet__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
pfx := random.Prefix(prng)
|
|
|
|
tests := []struct {
|
|
name string
|
|
pfx netip.Prefix
|
|
val int
|
|
}{
|
|
{
|
|
name: "default route v4",
|
|
pfx: mpp("0.0.0.0/0"),
|
|
val: 0,
|
|
},
|
|
{
|
|
name: "default route v6",
|
|
pfx: mpp("::/0"),
|
|
val: 0,
|
|
},
|
|
{
|
|
name: "set v4",
|
|
pfx: mpp("1.2.3.4/32"),
|
|
val: 1234,
|
|
},
|
|
{
|
|
name: "set v6",
|
|
pfx: mpp("2001:db8::/32"),
|
|
val: 2001,
|
|
},
|
|
}
|
|
|
|
tbl := new(_TABLE_TYPE[int])
|
|
if _, ok := tbl.Get(pfx); ok {
|
|
t.Errorf("empty table: Get(%v), ok=%v, expected: %v", pfx, ok, false)
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tbl.Insert(tt.pfx, tt.val)
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got, ok := tbl.Get(tt.pfx)
|
|
|
|
if !ok {
|
|
t.Errorf("%s: ok=%v, expected: %v", tt.name, ok, true)
|
|
}
|
|
|
|
if got != tt.val {
|
|
t.Errorf("%s: val=%v, expected: %v", tt.name, got, tt.val)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTableGetCompare__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
n := workLoadN()
|
|
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
pfxs := random.RealWorldPrefixes(prng, n)
|
|
|
|
gold := new(golden.Table[string])
|
|
tbl := new(_TABLE_TYPE[string])
|
|
for _, pfx := range pfxs {
|
|
gold.Insert(pfx, pfx.String())
|
|
tbl.Insert(pfx, pfx.String())
|
|
}
|
|
|
|
for _, pfx := range pfxs {
|
|
goldVal, goldOK := gold.Get(pfx)
|
|
tblVal, tblOK := tbl.Get(pfx)
|
|
|
|
if goldOK != tblOK {
|
|
t.Fatalf("Get(%q) = (_, %v), want (_, %v)", pfx, tblOK, goldOK)
|
|
}
|
|
|
|
// Skip value comparison for liteTable (no real payload)
|
|
if _, isLite := any(tbl).(*liteTable[string]); !isLite {
|
|
if goldVal != tblVal {
|
|
t.Fatalf("Get(%q) = (%v, %v), want (%v, %v)", pfx, tblVal, tblOK, goldVal, goldOK)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableModifySemantics__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type args struct {
|
|
pfx netip.Prefix
|
|
cb func(val int, found bool) (_ int, del bool)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
prepare map[netip.Prefix]int // entries to pre-populate the table
|
|
args args
|
|
finalData map[netip.Prefix]int // expected table contents after the operation
|
|
}{
|
|
{
|
|
name: "Delete existing entry",
|
|
prepare: map[netip.Prefix]int{mpp("10.0.0.0/8"): 42, mpp("2001:db8::/32"): 4242},
|
|
args: args{
|
|
pfx: mpp("10.0.0.0/8"),
|
|
cb: func(val int, found bool) (_ int, del bool) { return 0, true },
|
|
},
|
|
finalData: map[netip.Prefix]int{mpp("2001:db8::/32"): 4242},
|
|
},
|
|
|
|
{
|
|
name: "Insert new entry",
|
|
prepare: map[netip.Prefix]int{mpp("10.0.0.0/8"): 42},
|
|
args: args{
|
|
pfx: mpp("2001:db8::/32"),
|
|
cb: func(val int, found bool) (_ int, del bool) { return 4242, false },
|
|
},
|
|
finalData: map[netip.Prefix]int{mpp("10.0.0.0/8"): 42, mpp("2001:db8::/32"): 4242},
|
|
},
|
|
|
|
{
|
|
name: "Update existing entry",
|
|
prepare: map[netip.Prefix]int{mpp("10.0.0.0/8"): 42, mpp("2001:db8::/32"): 4242},
|
|
args: args{
|
|
pfx: mpp("10.0.0.0/8"),
|
|
cb: func(val int, found bool) (_ int, del bool) { return -1, false },
|
|
},
|
|
finalData: map[netip.Prefix]int{mpp("10.0.0.0/8"): -1, mpp("2001:db8::/32"): 4242},
|
|
},
|
|
|
|
{
|
|
name: "No-op on missing entry",
|
|
prepare: map[netip.Prefix]int{mpp("10.0.0.0/8"): 42},
|
|
args: args{
|
|
pfx: mpp("2001:db8::/32"),
|
|
cb: func(val int, found bool) (_ int, del bool) { return 0, true },
|
|
},
|
|
finalData: map[netip.Prefix]int{mpp("10.0.0.0/8"): 42},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tbl := new(_TABLE_TYPE[int])
|
|
|
|
// Insert initial entries using Modify
|
|
for pfx, v := range tt.prepare {
|
|
tbl.Modify(pfx, func(_ int, _ bool) (_ int, del bool) { return v, false })
|
|
}
|
|
|
|
tbl.Modify(tt.args.pfx, tt.args.cb)
|
|
|
|
// Check the final state of the table using Get, compares expected and actual table
|
|
got := make(map[netip.Prefix]int, len(tt.finalData))
|
|
for pfx, val := range tbl.All() {
|
|
got[pfx] = val
|
|
}
|
|
if len(got) != len(tt.finalData) {
|
|
t.Fatalf("[%s] final table size mismatch: got %d, want %d", tt.name, len(got), len(tt.finalData))
|
|
}
|
|
for pfx, wantVal := range tt.finalData {
|
|
gotVal, ok := got[pfx]
|
|
if !ok || gotVal != wantVal {
|
|
t.Fatalf("[%s] final table: key %v = %v (present=%v), want %v", tt.name, pfx, gotVal, ok, wantVal)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableModifyPersistSemantics__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if _, isLite := any(new(_TABLE_TYPE[any])).(*liteTable[any]); isLite {
|
|
t.Skip("liteNode has no real payload")
|
|
}
|
|
|
|
type args struct {
|
|
pfx netip.Prefix
|
|
cb func(val int, found bool) (_ int, del bool)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
prepare map[netip.Prefix]int // entries to pre-populate the table
|
|
args args
|
|
finalData map[netip.Prefix]int // expected table contents after the operation
|
|
}{
|
|
{
|
|
name: "Delete existing entry",
|
|
prepare: map[netip.Prefix]int{mpp("10.0.0.0/8"): 42, mpp("2001:db8::/32"): 4242},
|
|
args: args{
|
|
pfx: mpp("10.0.0.0/8"),
|
|
cb: func(val int, found bool) (_ int, del bool) { return 0, true },
|
|
},
|
|
finalData: map[netip.Prefix]int{mpp("2001:db8::/32"): 4242},
|
|
},
|
|
|
|
{
|
|
name: "Insert new entry",
|
|
prepare: map[netip.Prefix]int{mpp("10.0.0.0/8"): 42},
|
|
args: args{
|
|
pfx: mpp("2001:db8::/32"),
|
|
cb: func(val int, found bool) (_ int, del bool) { return 4242, false },
|
|
},
|
|
finalData: map[netip.Prefix]int{mpp("10.0.0.0/8"): 42, mpp("2001:db8::/32"): 4242},
|
|
},
|
|
|
|
{
|
|
name: "Update existing entry",
|
|
prepare: map[netip.Prefix]int{mpp("10.0.0.0/8"): 42, mpp("2001:db8::/32"): 4242},
|
|
args: args{
|
|
pfx: mpp("10.0.0.0/8"),
|
|
cb: func(val int, found bool) (_ int, del bool) { return -1, false },
|
|
},
|
|
finalData: map[netip.Prefix]int{mpp("10.0.0.0/8"): -1, mpp("2001:db8::/32"): 4242},
|
|
},
|
|
|
|
{
|
|
name: "No-op on missing entry",
|
|
prepare: map[netip.Prefix]int{mpp("10.0.0.0/8"): 42},
|
|
args: args{
|
|
pfx: mpp("2001:db8::/32"),
|
|
cb: func(val int, found bool) (_ int, del bool) { return 0, true },
|
|
},
|
|
finalData: map[netip.Prefix]int{mpp("10.0.0.0/8"): 42},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tbl := new(_TABLE_TYPE[int])
|
|
|
|
// Insert initial entries using Modify
|
|
for pfx, v := range tt.prepare {
|
|
tbl.Modify(pfx, func(_ int, _ bool) (_ int, del bool) { return v, false })
|
|
}
|
|
|
|
prt := tbl.ModifyPersist(tt.args.pfx, tt.args.cb)
|
|
|
|
// Check the final state of the table using Get, compares expected and actual table
|
|
for pfx, wantVal := range tt.finalData {
|
|
gotVal, ok := prt.Get(pfx)
|
|
if !ok || gotVal != wantVal {
|
|
t.Errorf("[%s] final table: key %v = %v (ok=%v), want %v (ok=true)", tt.name, pfx, gotVal, ok, wantVal)
|
|
}
|
|
}
|
|
|
|
// Ensure there are no unexpected entries
|
|
for pfx := range tt.prepare {
|
|
if _, expect := tt.finalData[pfx]; !expect {
|
|
if _, ok := prt.Get(pfx); ok {
|
|
t.Errorf("[%s] final table: key %v should not be present", tt.name, pfx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ensure the original table isn't modified
|
|
for pfx, wantVal := range tt.prepare {
|
|
val, ok := tbl.Get(pfx)
|
|
if !ok {
|
|
t.Errorf("[%s] original table: key %v should be present", tt.name, pfx)
|
|
}
|
|
|
|
if val != wantVal {
|
|
t.Errorf("[%s] original table: val %v is not as expected %v", tt.name, val, wantVal)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableModifyCompare__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
n := workLoadN()
|
|
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
pfxs := random.RealWorldPrefixes(prng, n)
|
|
|
|
gold := new(golden.Table[int])
|
|
tbl := new(_TABLE_TYPE[int])
|
|
|
|
// Update as insert
|
|
for i, pfx := range pfxs {
|
|
gold.Insert(pfx, i)
|
|
tbl.Modify(pfx, func(int, bool) (int, bool) { return i, false })
|
|
}
|
|
|
|
gold.Sort()
|
|
tblFlat := tbl.flatSorted()
|
|
|
|
// Skip value comparison for liteTable (no real payload)
|
|
if _, isLite := any(tbl).(*liteTable[int]); isLite {
|
|
if !slices.Equal(gold.AllSorted(), tblFlat.AllSorted()) {
|
|
t.Fatal("expected Equal")
|
|
}
|
|
} else {
|
|
if !slices.Equal(*gold, tblFlat) {
|
|
t.Fatal("expected Equal")
|
|
}
|
|
}
|
|
|
|
cb1 := func(val int, _ bool) int { return val + 1 }
|
|
cb2 := func(val int, _ bool) (int, bool) { return val + 1, false }
|
|
|
|
// Update as update
|
|
for _, pfx := range pfxs[:len(pfxs)/2] {
|
|
gold.Update(pfx, cb1)
|
|
tbl.Modify(pfx, cb2)
|
|
}
|
|
|
|
gold.Sort()
|
|
tblFlat = tbl.flatSorted()
|
|
|
|
// Skip value comparison for liteTable (no real payload)
|
|
if _, isLite := any(tbl).(*liteTable[int]); isLite {
|
|
if !slices.Equal(gold.AllSorted(), tblFlat.AllSorted()) {
|
|
t.Fatal("expected Equal")
|
|
}
|
|
} else {
|
|
if !slices.Equal(*gold, tblFlat) {
|
|
t.Fatal("expected Equal")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableModifyPersistCompare__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
n := workLoadN()
|
|
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
pfxs := random.RealWorldPrefixes(prng, n)
|
|
|
|
mut := new(_TABLE_TYPE[int])
|
|
imu := new(_TABLE_TYPE[int])
|
|
|
|
// Update as insert
|
|
for i, pfx := range pfxs {
|
|
mut.Modify(pfx, func(int, bool) (int, bool) { return i, false })
|
|
imu = imu.ModifyPersist(pfx, func(int, bool) (int, bool) { return i, false })
|
|
}
|
|
|
|
if !mut.Equal(imu) {
|
|
t.Fatal("expected Equal")
|
|
}
|
|
|
|
cb := func(val int, _ bool) (int, bool) { return val + 1, false }
|
|
|
|
// Update as update
|
|
for _, pfx := range pfxs[:len(pfxs)/2] {
|
|
mut.Modify(pfx, cb)
|
|
imu = imu.ModifyPersist(pfx, cb)
|
|
}
|
|
|
|
if !mut.Equal(imu) {
|
|
t.Fatal("expected Equal")
|
|
}
|
|
}
|
|
|
|
func TestTableModifyShuffled__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
n := workLoadN()
|
|
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
|
|
var (
|
|
numPrefixes = n // prefixes to insert (test deletes 50% of them)
|
|
numPerFamily = numPrefixes / 2
|
|
deleteCut = numPerFamily / 2
|
|
)
|
|
|
|
for range 10 {
|
|
all4 := random.RealWorldPrefixes4(prng, numPerFamily)
|
|
all6 := random.RealWorldPrefixes6(prng, numPerFamily)
|
|
|
|
// pfxs toDelete should be non-overlapping sets
|
|
pfxs := slices.Concat(all4, all6)
|
|
toDelete := slices.Concat(all4[deleteCut:], all6[deleteCut:])
|
|
|
|
tbl1 := new(_TABLE_TYPE[string])
|
|
|
|
// insert
|
|
for _, pfx := range pfxs {
|
|
tbl1.Insert(pfx, pfx.String())
|
|
}
|
|
|
|
// this callback deletes unconditionally
|
|
cb := func(string, bool) (string, bool) { return "", true }
|
|
|
|
// delete
|
|
for _, pfx := range toDelete {
|
|
tbl1.Modify(pfx, cb)
|
|
}
|
|
|
|
pfxs2 := slices.Clone(pfxs)
|
|
toDelete2 := slices.Clone(toDelete)
|
|
prng.Shuffle(len(toDelete2), func(i, j int) { toDelete2[i], toDelete2[j] = toDelete2[j], toDelete2[i] })
|
|
|
|
tbl2 := new(_TABLE_TYPE[string])
|
|
|
|
// insert
|
|
for _, pfx := range pfxs2 {
|
|
tbl2.Insert(pfx, pfx.String())
|
|
}
|
|
|
|
// delete
|
|
for _, pfx := range toDelete2 {
|
|
tbl2.Modify(pfx, cb)
|
|
}
|
|
|
|
if !tbl1.Equal(tbl2) {
|
|
t.Fatal("expected equal")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableModifyPersistShuffled__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
n := workLoadN()
|
|
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
|
|
var (
|
|
numPrefixes = n // prefixes to insert (test deletes 50% of them)
|
|
numPerFamily = numPrefixes / 2
|
|
deleteCut = numPerFamily / 2
|
|
)
|
|
|
|
for range 10 {
|
|
all4 := random.RealWorldPrefixes4(prng, numPerFamily)
|
|
all6 := random.RealWorldPrefixes6(prng, numPerFamily)
|
|
|
|
// pfxs toDelete should be non-overlapping sets
|
|
pfxs := slices.Concat(all4, all6)
|
|
toDelete := slices.Concat(all4[deleteCut:], all6[deleteCut:])
|
|
|
|
tbl1 := new(_TABLE_TYPE[string])
|
|
|
|
// insert
|
|
for _, pfx := range pfxs {
|
|
tbl1.Insert(pfx, pfx.String())
|
|
}
|
|
|
|
// this callback deletes unconditionally
|
|
cb := func(string, bool) (string, bool) { return "", true }
|
|
|
|
// delete
|
|
for _, pfx := range toDelete {
|
|
tbl1 = tbl1.ModifyPersist(pfx, cb)
|
|
}
|
|
|
|
pfxs2 := slices.Clone(pfxs)
|
|
toDelete2 := slices.Clone(toDelete)
|
|
prng.Shuffle(len(toDelete2), func(i, j int) { toDelete2[i], toDelete2[j] = toDelete2[j], toDelete2[i] })
|
|
|
|
tbl2 := new(_TABLE_TYPE[string])
|
|
|
|
// insert
|
|
for _, pfx := range pfxs2 {
|
|
tbl2.Insert(pfx, pfx.String())
|
|
}
|
|
|
|
// delete
|
|
for _, pfx := range toDelete2 {
|
|
tbl2 = tbl2.ModifyPersist(pfx, cb)
|
|
}
|
|
|
|
if !tbl1.Equal(tbl2) {
|
|
t.Fatal("expected equal")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestUnionMemoryAliasing tests that the Union method does not alias memory
|
|
// between the two tables.
|
|
func TestTableUnionMemoryAliasing__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
newTable := func(pfx ...string) *_TABLE_TYPE[any] {
|
|
tbl := new(_TABLE_TYPE[any])
|
|
for _, s := range pfx {
|
|
tbl.Insert(mpp(s), nil)
|
|
}
|
|
return tbl
|
|
}
|
|
|
|
// First create two tables with disjoint prefixes.
|
|
stable := newTable("0.0.0.0/24")
|
|
temp := newTable("100.69.1.0/24")
|
|
|
|
// Verify that the tables are disjoint.
|
|
if stable.Overlaps(temp) {
|
|
t.Error("stable should not overlap temp")
|
|
}
|
|
|
|
// Now union them.
|
|
temp.Union(stable)
|
|
|
|
// Add a new prefix to temp.
|
|
temp.Insert(mpp("0.0.1.0/24"), struct{}{})
|
|
|
|
// Ensure that stable is unchanged.
|
|
_, ok := stable.Lookup(mpa("0.0.1.1"))
|
|
if ok {
|
|
t.Error("stable should not contain 0.0.1.1")
|
|
}
|
|
if stable.OverlapsPrefix(mpp("0.0.1.1/32")) {
|
|
t.Error("stable should not overlap 0.0.1.1/32")
|
|
}
|
|
}
|
|
|
|
// TestUnionPersistMemoryAliasing tests that the Union method does not alias memory
|
|
// between the tables.
|
|
func TestTableUnionPersistMemoryAliasing__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
newTable := func(pfx ...string) *_TABLE_TYPE[any] {
|
|
tbl := new(_TABLE_TYPE[any])
|
|
for _, s := range pfx {
|
|
tbl.Insert(mpp(s), nil)
|
|
}
|
|
return tbl
|
|
}
|
|
// First create two tables with disjoint prefixes.
|
|
a := newTable("100.69.1.0/24")
|
|
b := newTable("0.0.0.0/24")
|
|
|
|
// Verify that the tables are disjoint.
|
|
if a.Overlaps(b) {
|
|
t.Error("this should not overlap other")
|
|
}
|
|
|
|
// Now union them with copy-on-write.
|
|
pTbl := a.UnionPersist(b)
|
|
|
|
// Add a new prefix to new union
|
|
pTbl.Insert(mpp("0.0.1.0/24"), struct{}{})
|
|
|
|
// Ensure that a is unchanged.
|
|
_, ok := a.Lookup(mpa("0.0.1.1"))
|
|
if ok {
|
|
t.Error("a should not contain 0.0.1.1")
|
|
}
|
|
if a.OverlapsPrefix(mpp("0.0.1.1/32")) {
|
|
t.Error("a should not overlap 0.0.1.1/32")
|
|
}
|
|
}
|
|
|
|
func TestTableUnionCompare__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
n := workLoadN()
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
|
|
for range 3 {
|
|
pfxs := random.RealWorldPrefixes(prng, n)
|
|
|
|
gold := new(golden.Table[string])
|
|
tbl := new(_TABLE_TYPE[string])
|
|
|
|
for _, pfx := range pfxs {
|
|
gold.Insert(pfx, pfx.String())
|
|
tbl.Insert(pfx, pfx.String())
|
|
}
|
|
|
|
pfxs2 := random.RealWorldPrefixes(prng, n)
|
|
|
|
gold2 := new(golden.Table[string])
|
|
tbl2 := new(_TABLE_TYPE[string])
|
|
|
|
for _, pfx := range pfxs2 {
|
|
gold2.Insert(pfx, pfx.String())
|
|
tbl2.Insert(pfx, pfx.String())
|
|
}
|
|
|
|
gold.Union(gold2)
|
|
tbl.Union(tbl2)
|
|
|
|
// dump as slow table for comparison
|
|
tblFlat := tbl.flatSorted()
|
|
|
|
// sort for comparison
|
|
gold.Sort()
|
|
|
|
// Skip value comparison for liteTable (no real payload)
|
|
if _, isLite := any(tbl).(*liteTable[string]); isLite {
|
|
if !slices.Equal(gold.AllSorted(), tblFlat.AllSorted()) {
|
|
t.Fatal("expected Equal")
|
|
}
|
|
} else {
|
|
if !slices.Equal(*gold, tblFlat) {
|
|
t.Fatal("expected Equal")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableUnionPersistCompare__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
|
|
n := workLoadN()
|
|
|
|
for range 3 {
|
|
pfxs := random.RealWorldPrefixes(prng, n)
|
|
|
|
gold := new(golden.Table[int])
|
|
tbl := new(_TABLE_TYPE[int])
|
|
|
|
for i, pfx := range pfxs {
|
|
gold.Insert(pfx, i)
|
|
tbl.Insert(pfx, i)
|
|
}
|
|
|
|
pfxs2 := random.RealWorldPrefixes(prng, n)
|
|
|
|
gold2 := new(golden.Table[int])
|
|
tbl2 := new(_TABLE_TYPE[int])
|
|
|
|
for i, pfx := range pfxs2 {
|
|
gold2.Insert(pfx, i)
|
|
tbl2.Insert(pfx, i)
|
|
}
|
|
|
|
gold.Union(gold2)
|
|
tblP := tbl.UnionPersist(tbl2)
|
|
|
|
// dump as slow table for comparison
|
|
flatP := tblP.flatSorted()
|
|
|
|
// sort for comparison
|
|
gold.Sort()
|
|
|
|
// Skip value comparison for liteTable (no real payload)
|
|
if _, isLite := any(tbl).(*liteTable[int]); isLite {
|
|
if !slices.Equal(gold.AllSorted(), flatP.AllSorted()) {
|
|
t.Fatal("expected Equal")
|
|
}
|
|
} else {
|
|
if !slices.Equal(*gold, flatP) {
|
|
t.Fatal("expected Equal")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableClone__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
n := workLoadN()
|
|
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
pfxs := random.RealWorldPrefixes(prng, n)
|
|
|
|
tbl := new(_TABLE_TYPE[int])
|
|
for i, pfx := range pfxs {
|
|
tbl.Insert(pfx, i)
|
|
}
|
|
clone := tbl.Clone()
|
|
|
|
if !tbl.Equal(clone) {
|
|
t.Fatal("expected equal")
|
|
}
|
|
}
|
|
|
|
func TestTableCloneShallow__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tbl := new(_TABLE_TYPE[*int])
|
|
|
|
if _, isLite := any(tbl).(*liteTable[*int]); isLite {
|
|
t.Skip("liteNode has no real payload")
|
|
}
|
|
|
|
clone := tbl.Clone()
|
|
if tbl.dumpString() != clone.dumpString() {
|
|
t.Errorf("empty Clone: got:\n%swant:\n%s", clone.dumpString(), tbl.dumpString())
|
|
}
|
|
|
|
val := 1
|
|
pfx := mpp("10.0.0.1/32")
|
|
tbl.Insert(pfx, &val)
|
|
|
|
clone = tbl.Clone()
|
|
want, _ := tbl.Get(pfx)
|
|
got, _ := clone.Get(pfx)
|
|
|
|
if *got != *want || got != want {
|
|
t.Errorf("shallow copy, values and pointers must be equal:\nvalues(%d, %d)\n(ptr(%v, %v)", *got, *want, got, want)
|
|
}
|
|
|
|
// update value, shallow copy of values, clone must be equal
|
|
val = 2
|
|
want, _ = tbl.Get(pfx)
|
|
got, _ = clone.Get(pfx)
|
|
|
|
if *got != *want {
|
|
t.Errorf("memory aliasing after shallow copy, values must be equal:\nvalues(%d, %d)", *got, *want)
|
|
}
|
|
}
|
|
|
|
func TestTableCloneDeep__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tbl := new(_TABLE_TYPE[*MyInt])
|
|
|
|
if _, isLite := any(tbl).(*liteTable[*MyInt]); isLite {
|
|
t.Skip("liteNode has no real payload")
|
|
}
|
|
|
|
clone := tbl.Clone()
|
|
if tbl.dumpString() != clone.dumpString() {
|
|
t.Errorf("empty Clone: got:\n%swant:\n%s", clone.dumpString(), tbl.dumpString())
|
|
}
|
|
|
|
val := MyInt(1)
|
|
pfx := mpp("10.0.0.1/32")
|
|
tbl.Insert(pfx, &val)
|
|
|
|
clone = tbl.Clone()
|
|
want, _ := tbl.Get(pfx)
|
|
got, _ := clone.Get(pfx)
|
|
|
|
if *got != *want || got == want {
|
|
t.Errorf("value with Cloner interface, pointers must be different:\nvalues(%d, %d)\n(ptr(%v, %v)", *got, *want, got, want)
|
|
}
|
|
|
|
// update value, deep copy of values, cloned value must now be different
|
|
val = 2
|
|
want, _ = tbl.Get(pfx)
|
|
got, _ = clone.Get(pfx)
|
|
|
|
if *got == *want {
|
|
t.Errorf("memory aliasing after deep copy, values must be different:\nvalues(%d, %d)", *got, *want)
|
|
}
|
|
}
|
|
|
|
func TestTableUnionShallow__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tbl1 := new(_TABLE_TYPE[*int])
|
|
tbl2 := new(_TABLE_TYPE[*int])
|
|
|
|
if _, isLite := any(tbl1).(*liteTable[*int]); isLite {
|
|
t.Skip("liteNode has no real payload")
|
|
}
|
|
|
|
val := 1
|
|
pfx := mpp("10.0.0.1/32")
|
|
tbl2.Insert(pfx, &val)
|
|
|
|
tbl1.Union(tbl2)
|
|
got, _ := tbl1.Get(pfx)
|
|
want, _ := tbl2.Get(pfx)
|
|
|
|
if *got != *want || got != want {
|
|
t.Errorf("shallow copy, values and pointers must be equal:\nvalues(%d, %d)\n(ptr(%v, %v)", *got, *want, got, want)
|
|
}
|
|
|
|
// update value, shallow copy of values, union must be equal
|
|
val = 2
|
|
got, _ = tbl1.Get(pfx)
|
|
want, _ = tbl2.Get(pfx)
|
|
|
|
if *got != *want {
|
|
t.Errorf("memory aliasing after shallow copy, values must be equal:\nvalues(%d, %d)", *got, *want)
|
|
}
|
|
}
|
|
|
|
func TestTableUnionDeep__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tbl1 := new(_TABLE_TYPE[*MyInt])
|
|
tbl2 := new(_TABLE_TYPE[*MyInt])
|
|
|
|
if _, isLite := any(tbl1).(*liteTable[*MyInt]); isLite {
|
|
t.Skip("liteNode has no real payload")
|
|
}
|
|
|
|
val := MyInt(1)
|
|
pfx := mpp("10.0.0.1/32")
|
|
tbl2.Insert(pfx, &val)
|
|
|
|
tbl1.Union(tbl2)
|
|
got, _ := tbl1.Get(pfx)
|
|
want, _ := tbl2.Get(pfx)
|
|
|
|
if *got != *want || got == want {
|
|
t.Errorf("value with Cloner interface, pointers must be different:\nvalues(%d, %d)\n(ptr(%v, %v)", *got, *want, got, want)
|
|
}
|
|
|
|
// update value, shallow copy of values, union must be equal
|
|
val = 2
|
|
got, _ = tbl1.Get(pfx)
|
|
want, _ = tbl2.Get(pfx)
|
|
|
|
if *got == *want {
|
|
t.Errorf("memory aliasing after deep copy, values must be different:\nvalues(%d, %d)", *got, *want)
|
|
}
|
|
}
|
|
|
|
// test some edge cases
|
|
func TestTableOverlapsPrefixEdgeCases__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type probe struct {
|
|
pfx netip.Prefix
|
|
want bool
|
|
}
|
|
|
|
type probes []probe
|
|
type pfxs []netip.Prefix
|
|
|
|
type test struct {
|
|
name string
|
|
insert pfxs
|
|
probes probes
|
|
}
|
|
|
|
tests := []test{
|
|
{
|
|
name: "empty table",
|
|
insert: nil,
|
|
probes: probes{{mpp("0.0.0.0/0"), false}, {mpp("::/0"), false}},
|
|
},
|
|
{
|
|
name: "default route I",
|
|
insert: pfxs{mpp("10.0.0.0/9"), mpp("2001:db8::/32")},
|
|
probes: probes{{mpp("0.0.0.0/0"), true}, {mpp("::/0"), true}},
|
|
},
|
|
{
|
|
name: "default route II",
|
|
insert: pfxs{mpp("0.0.0.0/0"), mpp("::/0")},
|
|
probes: probes{{mpp("10.0.0.0/9"), true}, {mpp("2001:db8::/32"), true}},
|
|
},
|
|
{
|
|
name: "single IP I",
|
|
insert: pfxs{mpp("10.0.0.0/7"), mpp("2001::/16")},
|
|
probes: probes{{mpp("10.1.2.3/32"), true}, {mpp("2001:db8:affe::cafe/128"), true}},
|
|
},
|
|
{
|
|
name: "single IP II",
|
|
insert: pfxs{mpp("10.1.2.3/32"), mpp("2001:db8:affe::cafe/128")},
|
|
probes: probes{{mpp("10.0.0.0/7"), true}, {mpp("2001::/16"), true}},
|
|
},
|
|
{
|
|
name: "same IP",
|
|
insert: pfxs{mpp("10.1.2.3/32"), mpp("2001:db8:affe::cafe/128")},
|
|
probes: probes{{mpp("10.1.2.3/32"), true}, {mpp("2001:db8:affe::cafe/128"), true}},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tbl := new(_TABLE_TYPE[int])
|
|
for _, pfx := range tt.insert {
|
|
tbl.Insert(pfx, 0)
|
|
}
|
|
|
|
for _, probe := range tt.probes {
|
|
got := tbl.OverlapsPrefix(probe.pfx)
|
|
if got != probe.want {
|
|
t.Errorf("[%s] OverlapsPrefix(%v) = %v, want %v", tt.name, probe.pfx, got, probe.want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableSize__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
n := workLoadN()
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
|
|
tbl := new(_TABLE_TYPE[any])
|
|
if tbl.Size() != 0 {
|
|
t.Errorf("empty Table: want: 0, got: %d", tbl.Size())
|
|
}
|
|
|
|
if tbl.Size4() != 0 {
|
|
t.Errorf("empty Table: want: 0, got: %d", tbl.Size4())
|
|
}
|
|
|
|
if tbl.Size6() != 0 {
|
|
t.Errorf("empty Table: want: 0, got: %d", tbl.Size6())
|
|
}
|
|
|
|
pfxs1 := random.RealWorldPrefixes(prng, n)
|
|
pfxs2 := random.RealWorldPrefixes(prng, n)
|
|
|
|
for _, pfx := range pfxs1 {
|
|
tbl.Insert(pfx, nil)
|
|
}
|
|
|
|
for _, pfx := range pfxs2 {
|
|
tbl.Modify(pfx, func(any, bool) (any, bool) { return nil, false })
|
|
}
|
|
|
|
pfxs1 = append(pfxs1, pfxs2...)
|
|
|
|
for _, pfx := range pfxs1[:n] {
|
|
tbl.Modify(pfx, func(any, bool) (any, bool) { return nil, false })
|
|
}
|
|
|
|
for _, pfx := range random.RealWorldPrefixes(prng, n) {
|
|
tbl.Delete(pfx)
|
|
}
|
|
|
|
var allCount4 int
|
|
var allCount6 int
|
|
|
|
for range tbl.AllSorted4() {
|
|
allCount4++
|
|
}
|
|
|
|
for range tbl.AllSorted6() {
|
|
allCount6++
|
|
}
|
|
|
|
if allCount4 != tbl.Size4() {
|
|
t.Errorf("Size4: want: %d, got: %d", allCount4, tbl.Size4())
|
|
}
|
|
|
|
if allCount6 != tbl.Size6() {
|
|
t.Errorf("Size6: want: %d, got: %d", allCount6, tbl.Size6())
|
|
}
|
|
}
|
|
|
|
// TestAll tests All with random samples
|
|
func TestTableAll__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
n := workLoadN()
|
|
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
pfxs := random.RealWorldPrefixes(prng, n)
|
|
|
|
tbl := new(_TABLE_TYPE[string])
|
|
|
|
// Insert all prefixes with their values
|
|
for _, pfx := range pfxs {
|
|
tbl.Insert(pfx, pfx.String())
|
|
}
|
|
|
|
// Collect all prefixes from All
|
|
gotPrefixes := make([]netip.Prefix, 0, n)
|
|
gotValues := make([]string, 0, n)
|
|
|
|
for pfx, val := range tbl.All() {
|
|
gotPrefixes = append(gotPrefixes, pfx)
|
|
gotValues = append(gotValues, val)
|
|
}
|
|
|
|
// Collect all prefixes from All4 and All6
|
|
got4Prefixes := make([]netip.Prefix, 0, n)
|
|
got4Values := make([]string, 0, n)
|
|
|
|
for pfx, val := range tbl.All4() {
|
|
got4Prefixes = append(got4Prefixes, pfx)
|
|
got4Values = append(got4Values, val)
|
|
}
|
|
|
|
got6Prefixes := make([]netip.Prefix, 0, n)
|
|
got6Values := make([]string, 0, n)
|
|
for pfx, val := range tbl.All6() {
|
|
got6Prefixes = append(got6Prefixes, pfx)
|
|
got6Values = append(got6Values, val)
|
|
}
|
|
|
|
// Verify we got exactly the same number of prefixes
|
|
if len(gotPrefixes) != len(pfxs) {
|
|
t.Fatalf("Expected %d prefixes, got %d", len(pfxs), len(gotPrefixes))
|
|
}
|
|
|
|
if len(got4Prefixes)+len(got6Prefixes) != len(pfxs) {
|
|
t.Fatalf("Expected %d prefixes, got %d", len(pfxs), len(got4Prefixes)+len(got6Prefixes))
|
|
}
|
|
|
|
if !slices.Equal(slices.Concat(got4Prefixes, got6Prefixes), gotPrefixes) {
|
|
t.Fatal("Prefixes: All4 + All6 != All")
|
|
}
|
|
if !slices.Equal(slices.Concat(got4Values, got6Values), gotValues) {
|
|
t.Fatal("Values: All4 + All6 != All")
|
|
}
|
|
|
|
// Verify we can find each original prefix in the results
|
|
// Create a map for O(1) lookup verification
|
|
resultMap := make(map[netip.Prefix]string, n)
|
|
for i, pfx := range gotPrefixes {
|
|
resultMap[pfx] = gotValues[i]
|
|
}
|
|
|
|
for _, pfx := range pfxs {
|
|
val, found := resultMap[pfx]
|
|
if !found {
|
|
t.Fatalf("Original prefix %v not found in All results", pfx)
|
|
}
|
|
|
|
// Skip value comparison for liteTable (no real payload)
|
|
if _, isLite := any(tbl).(*liteTable[string]); !isLite {
|
|
if val != pfx.String() {
|
|
t.Fatalf("Original prefix %v has wrong value: expected %s, got %s",
|
|
pfx, pfx.String(), val)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify no duplicates in results
|
|
seen := make(map[netip.Prefix]bool, n)
|
|
for _, pfx := range gotPrefixes {
|
|
if seen[pfx] {
|
|
t.Fatalf("Duplicate prefix %v found in All results", pfx)
|
|
}
|
|
seen[pfx] = true
|
|
}
|
|
}
|
|
|
|
func TestTableAllSorted__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Test cases with known CIDR sort order
|
|
testCases := []struct {
|
|
name string
|
|
prefixes []string
|
|
expected []string // Expected order after sorting
|
|
}{
|
|
{
|
|
name: "Mixed IPv4 addresses and prefix lengths",
|
|
prefixes: []string{
|
|
"10.0.0.0/16",
|
|
"10.0.0.0/8",
|
|
"192.168.1.0/24",
|
|
"10.0.0.0/24",
|
|
"172.16.0.0/12",
|
|
},
|
|
expected: []string{
|
|
"10.0.0.0/8", // Same address, shorter prefix first
|
|
"10.0.0.0/16", // Same address, longer prefix
|
|
"10.0.0.0/24", // Same address, longest prefix
|
|
"172.16.0.0/12", // Next address
|
|
"192.168.1.0/24", // Highest address
|
|
},
|
|
},
|
|
{
|
|
name: "Mixed IPv6 addresses and prefix lengths",
|
|
prefixes: []string{
|
|
"2001:db8::/32",
|
|
"2001:db8::/64",
|
|
"2000::/16",
|
|
"2001:db8:1::/48",
|
|
},
|
|
expected: []string{
|
|
"2000::/16", // Lowest address
|
|
"2001:db8::/32", // Same address, shorter prefix first
|
|
"2001:db8::/64", // Same address, longer prefix
|
|
"2001:db8:1::/48", // Higher address
|
|
},
|
|
},
|
|
{
|
|
name: "Mixed IPv4 and IPv6",
|
|
prefixes: []string{
|
|
"192.168.1.0/24",
|
|
"2001:db8::/32",
|
|
"10.0.0.0/8",
|
|
"::1/128",
|
|
},
|
|
expected: []string{
|
|
"10.0.0.0/8", // IPv4 addresses come first (lower in comparison)
|
|
"192.168.1.0/24", // Next IPv4 address
|
|
"::1/128", // IPv6 addresses after IPv4
|
|
"2001:db8::/32", // Higher IPv6 address
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tbl := new(_TABLE_TYPE[int])
|
|
|
|
// Insert prefixes with index as value
|
|
for i, prefixStr := range tc.prefixes {
|
|
pfx := netip.MustParsePrefix(prefixStr)
|
|
tbl.Insert(pfx, i)
|
|
}
|
|
|
|
// Collect sorted results
|
|
var actualOrder []string
|
|
for pfx := range tbl.AllSorted() {
|
|
actualOrder = append(actualOrder, pfx.String())
|
|
}
|
|
|
|
// Verify the order matches expected
|
|
if len(actualOrder) != len(tc.expected) {
|
|
t.Fatalf("%s: Expected %d results, got %d", tc.name, len(tc.expected), len(actualOrder))
|
|
}
|
|
|
|
// Collect sorted 4 results
|
|
var actual4Order []string
|
|
for pfx := range tbl.AllSorted4() {
|
|
actual4Order = append(actual4Order, pfx.String())
|
|
}
|
|
|
|
// Collect sorted 6 results
|
|
var actual6Order []string
|
|
for pfx := range tbl.AllSorted6() {
|
|
actual6Order = append(actual6Order, pfx.String())
|
|
}
|
|
|
|
if !slices.Equal(slices.Concat(actual4Order, actual6Order), actualOrder) {
|
|
t.Fatalf("%s: Prefixes: AllSorted4 + AllSorted6 != AllSorted", tc.name)
|
|
}
|
|
|
|
for i, expected := range tc.expected {
|
|
if actualOrder[i] != expected {
|
|
t.Errorf("%s:At position %d: expected %s, got %s", tc.name, i, expected, actualOrder[i])
|
|
t.Errorf("%s:Full expected order: %v", tc.name, tc.expected)
|
|
t.Errorf("%s:Full actual order: %v", tc.name, actualOrder)
|
|
break
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTableSubnets__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var zeroPfx netip.Prefix
|
|
|
|
t.Run("empty table", func(t *testing.T) {
|
|
t.Parallel()
|
|
tbl := new(_TABLE_TYPE[string])
|
|
pfx := mpp("::1/128")
|
|
|
|
for range tbl.Subnets(pfx) {
|
|
t.Errorf("empty table, must not range over")
|
|
}
|
|
})
|
|
|
|
t.Run("invalid prefix", func(t *testing.T) {
|
|
tbl := new(_TABLE_TYPE[string])
|
|
pfx := mpp("::1/128")
|
|
val := "foo"
|
|
tbl.Insert(pfx, val)
|
|
for range tbl.Subnets(zeroPfx) {
|
|
t.Errorf("invalid prefix, must not range over")
|
|
}
|
|
})
|
|
|
|
t.Run("identity", func(t *testing.T) {
|
|
tbl := new(_TABLE_TYPE[string])
|
|
pfx := mpp("::1/128")
|
|
val := "foo"
|
|
tbl.Insert(pfx, val)
|
|
|
|
for p, v := range tbl.Subnets(pfx) {
|
|
if p != pfx {
|
|
t.Errorf("Subnet(%v), got: %v, want: %v", pfx, p, pfx)
|
|
}
|
|
|
|
// Skip value comparison for liteTable (no real payload)
|
|
if _, isLite := any(tbl).(*liteTable[string]); !isLite {
|
|
if v != val {
|
|
t.Errorf("Subnet(%v), got: %v, want: %v", pfx, v, val)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("default gateway", func(t *testing.T) {
|
|
n := workLoadN()
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
|
|
want4 := n - n/2
|
|
want6 := n + n/2
|
|
|
|
tbl := new(_TABLE_TYPE[int])
|
|
for i, pfx := range random.RealWorldPrefixes4(prng, want4) {
|
|
tbl.Insert(pfx, i)
|
|
}
|
|
for i, pfx := range random.RealWorldPrefixes6(prng, want6) {
|
|
tbl.Insert(pfx, i)
|
|
}
|
|
|
|
// default gateway v4 covers all v4 prefixes in table
|
|
dg4 := mpp("0.0.0.0/0")
|
|
got4 := 0
|
|
for range tbl.Subnets(dg4) {
|
|
got4++
|
|
}
|
|
|
|
// default gateway v6 covers all v6 prefixes in table
|
|
dg6 := mpp("::/0")
|
|
got6 := 0
|
|
for range tbl.Subnets(dg6) {
|
|
got6++
|
|
}
|
|
|
|
if got4 != want4 {
|
|
t.Errorf("Subnets v4, want: %d, got: %d", want4, got4)
|
|
}
|
|
if got6 != want6 {
|
|
t.Errorf("Subnets v6, want: %d, got: %d", want6, got6)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestTableSubnetsCompare__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
n := workLoadN()
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
|
|
pfxs := random.RealWorldPrefixes(prng, n)
|
|
|
|
gold := new(golden.Table[int])
|
|
tbl := new(_TABLE_TYPE[int])
|
|
|
|
for i, pfx := range pfxs {
|
|
gold.Insert(pfx, i)
|
|
tbl.Insert(pfx, i)
|
|
}
|
|
|
|
for _, pfx := range random.RealWorldPrefixes(prng, n) {
|
|
t.Run("subtest", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
gotGold := gold.Subnets(pfx)
|
|
gotTbl := []netip.Prefix{}
|
|
for pfx := range tbl.Subnets(pfx) {
|
|
gotTbl = append(gotTbl, pfx)
|
|
}
|
|
if !slices.Equal(gotGold, gotTbl) {
|
|
t.Fatalf("Subnets(%q) = %v, want %v", pfx, gotTbl, gotGold)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTableSupernetsEdgeCase__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var zeroPfx netip.Prefix
|
|
|
|
t.Run("empty table", func(t *testing.T) {
|
|
t.Parallel()
|
|
tbl := new(_TABLE_TYPE[any])
|
|
pfx := mpp("::1/128")
|
|
|
|
tbl.Supernets(pfx)(func(_ netip.Prefix, _ any) bool {
|
|
t.Errorf("empty table, must not range over")
|
|
return false
|
|
})
|
|
})
|
|
|
|
t.Run("invalid prefix", func(t *testing.T) {
|
|
tbl := new(_TABLE_TYPE[any])
|
|
pfx := mpp("::1/128")
|
|
val := "foo"
|
|
tbl.Insert(pfx, val)
|
|
|
|
tbl.Supernets(zeroPfx)(func(_ netip.Prefix, _ any) bool {
|
|
t.Errorf("invalid prefix, must not range over")
|
|
return false
|
|
})
|
|
})
|
|
|
|
t.Run("identity", func(t *testing.T) {
|
|
tbl := new(_TABLE_TYPE[string])
|
|
pfx := mpp("::1/128")
|
|
val := "foo"
|
|
tbl.Insert(pfx, val)
|
|
|
|
for p, v := range tbl.Supernets(pfx) {
|
|
if p != pfx {
|
|
t.Errorf("Supernets(%v), got: %v, want: %v", pfx, p, pfx)
|
|
}
|
|
|
|
// Skip value comparison for liteTable (no real payload)
|
|
if _, isLite := any(tbl).(*liteTable[string]); !isLite {
|
|
if v != val {
|
|
t.Errorf("Supernets(%v), got: %v, want: %v", pfx, v, val)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestTableSupernetsCompare__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
n := workLoadN()
|
|
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
|
|
pfxs := random.RealWorldPrefixes(prng, n)
|
|
|
|
gold := new(golden.Table[int])
|
|
tbl := new(_TABLE_TYPE[int])
|
|
|
|
for i, pfx := range pfxs {
|
|
gold.Insert(pfx, i)
|
|
tbl.Insert(pfx, i)
|
|
}
|
|
|
|
for _, pfx := range random.RealWorldPrefixes(prng, n) {
|
|
t.Run("subtest", func(t *testing.T) {
|
|
t.Parallel()
|
|
gotGold := gold.Supernets(pfx)
|
|
gotTbl := []netip.Prefix{}
|
|
|
|
for p := range tbl.Supernets(pfx) {
|
|
gotTbl = append(gotTbl, p)
|
|
}
|
|
|
|
if !slices.Equal(gotGold, gotTbl) {
|
|
t.Fatalf("Supernets(%q) = %v, want %v", pfx, gotTbl, gotGold)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTableMarshalText__TABLE_TYPE(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
expectedData map[netip.Prefix]string
|
|
}{
|
|
{
|
|
name: "empty",
|
|
expectedData: map[netip.Prefix]string{},
|
|
},
|
|
{
|
|
name: "with_data",
|
|
expectedData: map[netip.Prefix]string{
|
|
mpp("192.168.1.0/24"): "test1",
|
|
mpp("10.0.0.0/8"): "test2",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tbl := new(_TABLE_TYPE[string])
|
|
|
|
// Insert test data
|
|
for prefix, value := range tt.expectedData {
|
|
tbl.Insert(prefix, value)
|
|
}
|
|
|
|
data, err := tbl.MarshalText()
|
|
if err != nil {
|
|
t.Errorf("MarshalText failed: %v", err)
|
|
}
|
|
|
|
if len(tt.expectedData) > 0 && len(data) == 0 {
|
|
t.Error("Expected non-empty marshaled text")
|
|
}
|
|
|
|
// Check that all expected values appear in marshaled text
|
|
text := string(data)
|
|
for _, value := range tt.expectedData {
|
|
if !strings.Contains(text, value) {
|
|
t.Errorf("Marshaled text doesn't contain expected value: %s", value)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTableMarshalJSON__TABLE_TYPE(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
expectedData map[netip.Prefix]any
|
|
}{
|
|
{
|
|
name: "empty",
|
|
expectedData: map[netip.Prefix]any{},
|
|
},
|
|
{
|
|
name: "string_values",
|
|
expectedData: map[netip.Prefix]any{
|
|
mpp("192.168.1.0/24"): "net1",
|
|
mpp("10.0.0.0/8"): "net2",
|
|
},
|
|
},
|
|
{
|
|
name: "mixed_values",
|
|
expectedData: map[netip.Prefix]any{
|
|
mpp("192.168.1.0/24"): "string",
|
|
mpp("10.0.0.0/8"): 42,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tbl := new(_TABLE_TYPE[any])
|
|
|
|
// Insert test data
|
|
for prefix, value := range tt.expectedData {
|
|
tbl.Insert(prefix, value)
|
|
}
|
|
|
|
jsonData, err := tbl.MarshalJSON()
|
|
if err != nil {
|
|
t.Errorf("JSON marshaling failed: %v", err)
|
|
}
|
|
|
|
if len(jsonData) == 0 {
|
|
t.Error("Expected valid JSON")
|
|
}
|
|
|
|
// Should be valid JSON
|
|
var result interface{}
|
|
if err := json.Unmarshal(jsonData, &result); err != nil {
|
|
t.Errorf("Invalid JSON produced: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTableDumpList4__TABLE_TYPE(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
expectedData map[netip.Prefix]string
|
|
expectItems int
|
|
}{
|
|
{
|
|
name: "empty",
|
|
expectedData: map[netip.Prefix]string{},
|
|
expectItems: 0,
|
|
},
|
|
{
|
|
name: "single_ipv4",
|
|
expectedData: map[netip.Prefix]string{
|
|
mpp("192.168.1.0/24"): "lan",
|
|
},
|
|
expectItems: 1,
|
|
},
|
|
{
|
|
name: "multiple_ipv4",
|
|
expectedData: map[netip.Prefix]string{
|
|
mpp("192.168.1.0/24"): "lan",
|
|
mpp("10.0.0.0/8"): "private",
|
|
},
|
|
expectItems: 2,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tbl := new(_TABLE_TYPE[string])
|
|
|
|
// Insert test data
|
|
for prefix, value := range tt.expectedData {
|
|
tbl.Insert(prefix, value)
|
|
}
|
|
|
|
dumpList := tbl.DumpList4()
|
|
|
|
// Count total nodes in the tree (including nested)
|
|
totalNodes := countDumpListNodes(dumpList)
|
|
if totalNodes != tt.expectItems {
|
|
t.Errorf("DumpList4() total nodes (%d) does not match expected (%d)", totalNodes, tt.expectItems)
|
|
}
|
|
|
|
// Verify all nodes are IPv4
|
|
verifyAllIPv4Nodes(t, dumpList)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTableDumpList6__TABLE_TYPE(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
expectedData map[netip.Prefix]string
|
|
expectItems int
|
|
}{
|
|
{
|
|
name: "empty",
|
|
expectedData: map[netip.Prefix]string{},
|
|
expectItems: 0,
|
|
},
|
|
{
|
|
name: "single_ipv6",
|
|
expectedData: map[netip.Prefix]string{
|
|
mpp("2001:db8::/32"): "doc",
|
|
},
|
|
expectItems: 1,
|
|
},
|
|
{
|
|
name: "multiple_ipv6",
|
|
expectedData: map[netip.Prefix]string{
|
|
mpp("2001:db8::/32"): "doc",
|
|
mpp("fe80::/10"): "link-local",
|
|
},
|
|
expectItems: 2,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tbl := new(_TABLE_TYPE[string])
|
|
|
|
// Insert test data
|
|
for prefix, value := range tt.expectedData {
|
|
tbl.Insert(prefix, value)
|
|
}
|
|
|
|
dumpList := tbl.DumpList6()
|
|
|
|
// Count total nodes in the tree (including nested)
|
|
totalNodes := countDumpListNodes(dumpList)
|
|
if totalNodes != tt.expectItems {
|
|
t.Errorf("DumpList6() total nodes (%d) does not match expected (%d)", totalNodes, tt.expectItems)
|
|
}
|
|
|
|
// Verify all nodes are IPv6
|
|
verifyAllIPv6Nodes(t, dumpList)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTableEqual__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
n := workLoadN()
|
|
prng := rand.New(rand.NewPCG(42, 42))
|
|
|
|
at := new(_TABLE_TYPE[int])
|
|
for i, pfx := range random.RealWorldPrefixes(prng, n) {
|
|
at.Insert(pfx, i)
|
|
}
|
|
|
|
t.Run("clone", func(t *testing.T) {
|
|
t.Parallel()
|
|
bt := at.Clone()
|
|
if !at.Equal(bt) {
|
|
t.Error("expected true, got false")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestTableExtra__TABLE_TYPE(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// 1. Equal: checking self equality
|
|
t1 := new(_TABLE_TYPE[int])
|
|
t1.Insert(mpp("10.0.0.0/8"), 42)
|
|
t1.Insert(mpp("2001:db8::/32"), 42)
|
|
|
|
if !t1.Equal(t1) {
|
|
t.Error("expected table to be equal to itself")
|
|
}
|
|
|
|
// 2. Equal: checking different size4 / size6
|
|
t2 := new(_TABLE_TYPE[int])
|
|
t2.Insert(mpp("10.0.0.0/8"), 42)
|
|
if t1.Equal(t2) {
|
|
t.Error("expected false for different size6")
|
|
}
|
|
|
|
t3 := new(_TABLE_TYPE[int])
|
|
t3.Insert(mpp("2001:db8::/32"), 42)
|
|
if t1.Equal(t3) {
|
|
t.Error("expected false for different size4")
|
|
}
|
|
|
|
// 3. Overlaps6: checking when size6 is 0
|
|
emptyT := new(_TABLE_TYPE[int])
|
|
if t1.Overlaps6(emptyT) {
|
|
t.Error("expected false when other table has no IPv6 prefixes")
|
|
}
|
|
if emptyT.Overlaps6(t1) {
|
|
t.Error("expected false when this table has no IPv6 prefixes")
|
|
}
|
|
|
|
// 4. Fprint: nil writer check
|
|
var nilWriter io.Writer = nil
|
|
if err := t1.Fprint(nilWriter); err == nil {
|
|
t.Error("expected error when writing to nil writer")
|
|
}
|
|
|
|
// 5. ModifyPersist: no-op deletion of non-existing key
|
|
t4 := new(_TABLE_TYPE[int])
|
|
t5 := t4.ModifyPersist(mpp("10.0.0.0/8"), func(val int, ok bool) (int, bool) {
|
|
return 0, true
|
|
})
|
|
if t5 != t4 {
|
|
t.Error("expected same table pointer for no-op ModifyPersist")
|
|
}
|
|
}
|