diff --git a/gold_table_test.go b/gold_table_test.go index f511063..5a63354 100644 --- a/gold_table_test.go +++ b/gold_table_test.go @@ -20,6 +20,7 @@ type goldTableItem[V any] struct { val V } +/* func (t *goldTable[V]) insert(pfx netip.Prefix, val V) { pfx = pfx.Masked() for i, ent := range *t { @@ -30,6 +31,7 @@ func (t *goldTable[V]) insert(pfx netip.Prefix, val V) { } *t = append(*t, goldTableItem[V]{pfx, val}) } +*/ func (t *goldTable[V]) get(pfx netip.Prefix) (val V, ok bool) { pfx = pfx.Masked() diff --git a/internal/bitset/bitset_iter_test.go b/internal/bitset/bitset_iter_test.go index 489bf92..3f8a66b 100644 --- a/internal/bitset/bitset_iter_test.go +++ b/internal/bitset/bitset_iter_test.go @@ -7,6 +7,7 @@ package bitset import ( "fmt" + "slices" "testing" ) @@ -41,6 +42,31 @@ func TestAllBitSetIter(t *testing.T) { } } +func TestAllBitSetIterStop(t *testing.T) { + t.Parallel() + + var b BitSet + + for u := range uint(20) { + b = b.Set(u) + } + + got := []uint{} + want := []uint{0, 1, 2, 3, 4} + + // range over func + for u := range b.All() { + if u > 4 { + break + } + got = append(got, u) + } + + if !slices.Equal(got, want) { + t.Fatalf("rangefunc with break condition, expected: %v, got: %v", want, got) + } +} + func TestAllBitSetCallback(t *testing.T) { t.Parallel() tc := []uint{0, 1, 2, 5, 10, 20, 50, 100, 200, 500, 511} diff --git a/internal/bitset/bitset_test.go b/internal/bitset/bitset_test.go index bc616d1..cd64ed2 100644 --- a/internal/bitset/bitset_test.go +++ b/internal/bitset/bitset_test.go @@ -225,6 +225,64 @@ func TestTest(t *testing.T) { } } +func TestFirstSet(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + set []uint + wantIdx uint + wantOk bool + }{ + { + name: "null", + set: []uint{}, + wantIdx: 0, + wantOk: false, + }, + { + name: "zero", + set: []uint{0}, + wantIdx: 0, + wantOk: true, + }, + { + name: "1,5", + set: []uint{1, 5}, + wantIdx: 1, + wantOk: true, + }, + { + name: "5,7", + set: []uint{5, 7}, + wantIdx: 5, + wantOk: true, + }, + { + name: "2. word", + set: []uint{70, 777}, + wantIdx: 70, + wantOk: true, + }, + } + + for _, tc := range testCases { + var b BitSet + for _, u := range tc.set { + b = b.Set(u) + } + + idx, ok := b.FirstSet() + + if ok != tc.wantOk { + t.Errorf("FirstSet, %s: got ok: %v, want: %v", tc.name, ok, tc.wantOk) + } + + if idx != tc.wantIdx { + t.Errorf("FirstSet, %s: got idx: %d, want: %d", tc.name, idx, tc.wantIdx) + } + } +} + func TestNextSet(t *testing.T) { t.Parallel() testCases := []struct { diff --git a/overlaps.go b/overlaps.go index 5e8a92e..0c3dca4 100644 --- a/overlaps.go +++ b/overlaps.go @@ -169,8 +169,6 @@ func (n *node[V]) overlapsChildrenIn(o *node[V]) bool { // make allot table with prefixes as bitsets, bitsets are precalculated // just union the bitsets to one bitset (allot table) for all prefixes // in this node - - // gimmick, don't allocate, can't use bitset.New() prefixRoutes := bitset.BitSet(make([]uint64, 8)) allIndices := n.prefixes.AsSlice(make([]uint, 0, maxNodePrefixes)) diff --git a/overlaps_test.go b/overlaps_test.go new file mode 100644 index 0000000..95d45ac --- /dev/null +++ b/overlaps_test.go @@ -0,0 +1,188 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause +// +// some tests modified from github.com/tailscale/art +// for this implementation by: +// +// Copyright (c) 2024 Karl Gaissmaier +// SPDX-License-Identifier: MIT + +package bart + +import ( + "net/netip" + "testing" +) + +func TestRegressionOverlaps(t *testing.T) { + t.Run("overlaps_divergent_children_with_parent_route_entry", func(t *testing.T) { + t.Parallel() + t1, t2 := Table[int]{}, Table[int]{} + + t1.Insert(mpp("128.0.0.0/2"), 1) + t1.Insert(mpp("99.173.128.0/17"), 1) + t1.Insert(mpp("219.150.142.0/23"), 1) + t1.Insert(mpp("164.148.190.250/31"), 1) + t1.Insert(mpp("48.136.229.233/32"), 1) + + t2.Insert(mpp("217.32.0.0/11"), 1) + t2.Insert(mpp("38.176.0.0/12"), 1) + t2.Insert(mpp("106.16.0.0/13"), 1) + t2.Insert(mpp("164.85.192.0/23"), 1) + t2.Insert(mpp("225.71.164.112/31"), 1) + + if !t1.Overlaps(&t2) { + t.Fatal("tables unexpectedly do not overlap") + } + }) + + t.Run("overlaps_parent_child_comparison_with_route_in_parent", func(t *testing.T) { + t.Parallel() + t1, t2 := Table[int]{}, Table[int]{} + + t1.Insert(mpp("226.0.0.0/8"), 1) + t1.Insert(mpp("81.128.0.0/9"), 1) + t1.Insert(mpp("152.0.0.0/9"), 1) + t1.Insert(mpp("151.220.0.0/16"), 1) + t1.Insert(mpp("89.162.61.0/24"), 1) + + t2.Insert(mpp("54.0.0.0/9"), 1) + t2.Insert(mpp("35.89.128.0/19"), 1) + t2.Insert(mpp("72.33.53.0/24"), 1) + t2.Insert(mpp("2.233.60.32/27"), 1) + t2.Insert(mpp("152.42.142.160/28"), 1) + + if !t1.Overlaps(&t2) { + t.Fatal("tables unexpectedly do not overlap") + } + }) +} + +func TestOverlapsCompare(t *testing.T) { + t.Parallel() + + // Empirically, between 5 and 6 routes per table results in ~50% + // of random pairs overlapping. Cool example of the birthday paradox! + const numEntries = 6 + + seen := map[bool]int{} + for range 10_000 { + pfxs := randomPrefixes(numEntries) + fast := Table[int]{} + gold := goldTable[int](pfxs) + + for _, pfx := range pfxs { + fast.Insert(pfx.pfx, pfx.val) + } + + inter := randomPrefixes(numEntries) + goldInter := goldTable[int](inter) + fastInter := Table[int]{} + for _, pfx := range inter { + fastInter.Insert(pfx.pfx, pfx.val) + } + + gotGold := gold.overlaps(&goldInter) + gotFast := fast.Overlaps(&fastInter) + + if gotGold != gotFast { + t.Fatalf("Overlaps(...) = %v, want %v\nTable1:\n%s\nTable:\n%v", + gotFast, gotGold, fast.String(), fastInter.String()) + } + + seen[gotFast]++ + } +} + +func TestOverlapsPrefixCompare(t *testing.T) { + t.Parallel() + pfxs := randomPrefixes(100_000) + + fast := Table[int]{} + gold := goldTable[int](pfxs) + + for _, pfx := range pfxs { + fast.Insert(pfx.pfx, pfx.val) + } + + tests := randomPrefixes(10_000) + for _, tt := range tests { + gotGold := gold.overlapsPrefix(tt.pfx) + gotFast := fast.OverlapsPrefix(tt.pfx) + if gotGold != gotFast { + t.Fatalf("overlapsPrefix(%q) = %v, want %v", tt.pfx, gotFast, gotGold) + } + } +} + +func TestOverlapsChildren(t *testing.T) { + t.Parallel() + pfxs1 := []netip.Prefix{ + // pfxs + mpp("10.0.0.0/8"), + mpp("11.0.0.0/8"), + mpp("12.0.0.0/8"), + mpp("13.0.0.0/8"), + mpp("14.0.0.0/8"), + // chi5dren + mpp("10.100.0.0/17"), + mpp("11.100.0.0/17"), + mpp("12.100.0.0/17"), + mpp("13.100.0.0/17"), + mpp("14.100.0.0/17"), + mpp("15.100.0.0/17"), + mpp("16.100.0.0/17"), + mpp("17.100.0.0/17"), + mpp("18.100.0.0/17"), + mpp("19.100.0.0/17"), + mpp("20.100.0.0/17"), + mpp("21.100.0.0/17"), + mpp("22.100.0.0/17"), + mpp("23.100.0.0/17"), + mpp("24.100.0.0/17"), + mpp("25.100.0.0/17"), + mpp("26.100.0.0/17"), + mpp("27.100.0.0/17"), + mpp("28.100.0.0/17"), + } + pfxs2 := []netip.Prefix{ + mpp("200.0.0.0/8"), + mpp("201.0.0.0/8"), + mpp("202.0.0.0/8"), + mpp("203.0.0.0/8"), + mpp("204.0.0.0/8"), + // children + mpp("201.200.0.0/18"), + mpp("202.200.0.0/18"), + mpp("203.200.0.0/18"), + mpp("204.200.0.0/18"), + mpp("205.200.0.0/18"), + mpp("206.200.0.0/18"), + mpp("207.200.0.0/18"), + mpp("208.200.0.0/18"), + mpp("209.200.0.0/18"), + mpp("210.200.0.0/18"), + mpp("211.200.0.0/18"), + mpp("212.200.0.0/18"), + mpp("213.200.0.0/18"), + mpp("214.200.0.0/18"), + mpp("215.200.0.0/18"), + mpp("216.200.0.0/18"), + mpp("217.200.0.0/18"), + mpp("218.200.0.0/18"), + mpp("219.200.0.0/18"), + } + + tbl1 := new(Table[string]) + for _, pfx := range pfxs1 { + tbl1.Insert(pfx, pfx.String()) + } + + tbl2 := new(Table[string]) + for _, pfx := range pfxs2 { + tbl2.Insert(pfx, pfx.String()) + } + if tbl1.Overlaps(tbl2) { + t.Fatal("tables unexpectedly do overlap") + } +} diff --git a/table_test.go b/table_test.go index d539abf..3d66ed4 100644 --- a/table_test.go +++ b/table_test.go @@ -1,8 +1,8 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause // -// some tests copied from github.com/tailscale/art -// and modified for this implementation by: +// some regression tests modified from github.com/tailscale/art +// for this implementation by: // // Copyright (c) 2024 Karl Gaissmaier // SPDX-License-Identifier: MIT @@ -120,115 +120,6 @@ func TestInvalidPrefix(t *testing.T) { }) } -func TestRegression(t *testing.T) { - t.Parallel() - // original comment by tailscale for ART, - // - // These tests are specific triggers for subtle correctness issues - // that came up during initial implementation. Even if they seem - // arbitrary, please do not clean them up. They are checking edge - // cases that are very easy to get wrong, and quite difficult for - // the other statistical tests to trigger promptly. - // - // ... but the BART implementation is different and has other edge cases. - - t.Run("prefixes_aligned_on_stride_boundary", func(t *testing.T) { - t.Parallel() - fast := &Table[int]{} - gold := goldTable[int]{} - - fast.Insert(mpp("226.205.197.0/24"), 1) - gold.insert(mpp("226.205.197.0/24"), 1) - - fast.Insert(mpp("226.205.0.0/16"), 2) - gold.insert(mpp("226.205.0.0/16"), 2) - - probe := mpa("226.205.121.152") - got, gotOK := fast.Lookup(probe) - want, wantOK := gold.lookup(probe) - if !getsEqual(got, gotOK, want, wantOK) { - t.Fatalf("got (%v, %v), want (%v, %v)", got, gotOK, want, wantOK) - } - }) - - t.Run("parent_prefix_inserted_in_different_orders", func(t *testing.T) { - t.Parallel() - t1, t2 := &Table[int]{}, &Table[int]{} - - t1.Insert(mpp("136.20.0.0/16"), 1) - t1.Insert(mpp("136.20.201.62/32"), 2) - - t2.Insert(mpp("136.20.201.62/32"), 2) - t2.Insert(mpp("136.20.0.0/16"), 1) - - a := mpa("136.20.54.139") - got1, ok1 := t1.Lookup(a) - got2, ok2 := t2.Lookup(a) - if !getsEqual(got1, ok1, got2, ok2) { - t.Errorf("Lookup(%q) is insertion order dependent: t1=(%v, %v), t2=(%v, %v)", a, got1, ok1, got2, ok2) - } - }) - - t.Run("overlaps_divergent_children_with_parent_route_entry", func(t *testing.T) { - t.Parallel() - t1, t2 := Table[int]{}, Table[int]{} - - t1.Insert(mpp("128.0.0.0/2"), 1) - t1.Insert(mpp("99.173.128.0/17"), 1) - t1.Insert(mpp("219.150.142.0/23"), 1) - t1.Insert(mpp("164.148.190.250/31"), 1) - t1.Insert(mpp("48.136.229.233/32"), 1) - - t2.Insert(mpp("217.32.0.0/11"), 1) - t2.Insert(mpp("38.176.0.0/12"), 1) - t2.Insert(mpp("106.16.0.0/13"), 1) - t2.Insert(mpp("164.85.192.0/23"), 1) - t2.Insert(mpp("225.71.164.112/31"), 1) - - if !t1.Overlaps(&t2) { - t.Fatalf("tables unexpectedly do not overlap") - } - }) - - t.Run("overlaps_parent_child_comparison_with_route_in_parent", func(t *testing.T) { - t.Parallel() - t1, t2 := Table[int]{}, Table[int]{} - - t1.Insert(mpp("226.0.0.0/8"), 1) - t1.Insert(mpp("81.128.0.0/9"), 1) - t1.Insert(mpp("152.0.0.0/9"), 1) - t1.Insert(mpp("151.220.0.0/16"), 1) - t1.Insert(mpp("89.162.61.0/24"), 1) - - t2.Insert(mpp("54.0.0.0/9"), 1) - t2.Insert(mpp("35.89.128.0/19"), 1) - t2.Insert(mpp("72.33.53.0/24"), 1) - t2.Insert(mpp("2.233.60.32/27"), 1) - t2.Insert(mpp("152.42.142.160/28"), 1) - - if !t1.Overlaps(&t2) { - t.Fatalf("tables unexpectedly do not overlap") - } - }) - - t.Run("LookupPrefix, default route", func(t *testing.T) { - t.Parallel() - t1 := Table[int]{} - dg4 := mpp("0.0.0.0/0") - dg6 := mpp("::/0") - - _, ok := t1.LookupPrefix(dg4) - if ok { - t.Fatalf("LookupPrefix(%s) should be false", dg4) - } - - _, ok = t1.LookupPrefix(dg6) - if ok { - t.Fatalf("LookupPrefix(%s) should be false", dg6) - } - }) -} - func TestInsert(t *testing.T) { t.Parallel() @@ -1302,63 +1193,6 @@ func TestUpdate(t *testing.T) { } } -func TestOverlapsCompare(t *testing.T) { - t.Parallel() - - // Empirically, between 5 and 6 routes per table results in ~50% - // of random pairs overlapping. Cool example of the birthday paradox! - const numEntries = 6 - - seen := map[bool]int{} - for range 10_000 { - pfxs := randomPrefixes(numEntries) - fast := Table[int]{} - gold := goldTable[int](pfxs) - - for _, pfx := range pfxs { - fast.Insert(pfx.pfx, pfx.val) - } - - inter := randomPrefixes(numEntries) - goldInter := goldTable[int](inter) - fastInter := Table[int]{} - for _, pfx := range inter { - fastInter.Insert(pfx.pfx, pfx.val) - } - - gotGold := gold.overlaps(&goldInter) - gotFast := fast.Overlaps(&fastInter) - - if gotGold != gotFast { - t.Fatalf("Overlaps(...) = %v, want %v\nTable1:\n%s\nTable:\n%v", - gotFast, gotGold, fast.String(), fastInter.String()) - } - - seen[gotFast]++ - } -} - -func TestOverlapsPrefixCompare(t *testing.T) { - t.Parallel() - pfxs := randomPrefixes(100_000) - - fast := Table[int]{} - gold := goldTable[int](pfxs) - - for _, pfx := range pfxs { - fast.Insert(pfx.pfx, pfx.val) - } - - tests := randomPrefixes(10_000) - for _, tt := range tests { - gotGold := gold.overlapsPrefix(tt.pfx) - gotFast := fast.OverlapsPrefix(tt.pfx) - if gotGold != gotFast { - t.Fatalf("overlapsPrefix(%q) = %v, want %v", tt.pfx, gotFast, gotGold) - } - } -} - func TestUnionEdgeCases(t *testing.T) { t.Parallel()