100 lines
2.6 KiB
Go
100 lines
2.6 KiB
Go
package poindexter
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
)
|
|
|
|
func TestInsert_DuplicateID(t *testing.T) {
|
|
tr, err := NewKDTreeFromDim[string](1)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
ok := tr.Insert(KDPoint[string]{ID: "X", Coords: []float64{0}})
|
|
if !ok {
|
|
t.Fatalf("first insert should succeed")
|
|
}
|
|
// duplicate ID should fail
|
|
if tr.Insert(KDPoint[string]{ID: "X", Coords: []float64{1}}) {
|
|
t.Fatalf("expected insert duplicate ID to return false")
|
|
}
|
|
}
|
|
|
|
func TestDeleteByID_SwapDelete(t *testing.T) {
|
|
// Arrange 3 points so that deleting the middle triggers swap-delete path
|
|
pts := []KDPoint[int]{
|
|
{ID: "A", Coords: []float64{0}},
|
|
{ID: "B", Coords: []float64{1}},
|
|
{ID: "C", Coords: []float64{2}},
|
|
}
|
|
tr, err := NewKDTree(pts)
|
|
if err != nil {
|
|
t.Fatalf("NewKDTree err: %v", err)
|
|
}
|
|
if !tr.DeleteByID("B") {
|
|
t.Fatalf("delete B failed")
|
|
}
|
|
if tr.Len() != 2 {
|
|
t.Fatalf("expected len 2, got %d", tr.Len())
|
|
}
|
|
// Ensure B is gone and A/C remain reachable
|
|
ids := make(map[string]bool)
|
|
for _, q := range [][]float64{{0}, {2}} {
|
|
p, _, ok := tr.Nearest(q)
|
|
if ok {
|
|
ids[p.ID] = true
|
|
}
|
|
}
|
|
if ids["B"] {
|
|
t.Fatalf("B should not be present after delete")
|
|
}
|
|
if !ids["A"] || !ids["C"] {
|
|
t.Fatalf("expected both A and C to be found after deleting B, got: %v", ids)
|
|
}
|
|
}
|
|
|
|
func TestRadius_NegativeReturnsNil(t *testing.T) {
|
|
pts := []KDPoint[int]{{ID: "z", Coords: []float64{0}}}
|
|
tr, _ := NewKDTree(pts)
|
|
ns, ds := tr.Radius([]float64{0}, -1)
|
|
if ns != nil || ds != nil {
|
|
// Both should be nil on invalid radius
|
|
t.Fatalf("expected nil slices on negative radius, got %v %v", ns, ds)
|
|
}
|
|
}
|
|
|
|
func TestNearest_EmptyTree(t *testing.T) {
|
|
tr, _ := NewKDTreeFromDim[int](2)
|
|
_, _, ok := tr.Nearest([]float64{0, 0})
|
|
if ok {
|
|
t.Fatalf("expected ok=false for empty tree")
|
|
}
|
|
}
|
|
|
|
func TestWeightedCosineMetric_ViaKDTree(t *testing.T) {
|
|
// Two points oriented differently around the query; ensure call path exercised
|
|
type rec struct{ a, b float64 }
|
|
items := []rec{{1, 0}, {0, 1}}
|
|
weights := []float64{1, 2}
|
|
invert := []bool{false, false}
|
|
features := []func(rec) float64{
|
|
func(r rec) float64 { return r.a },
|
|
func(r rec) float64 { return r.b },
|
|
}
|
|
pts, err := BuildND(items, func(r rec) string { return fmt.Sprintf("%v", r) }, features, weights, invert)
|
|
if err != nil {
|
|
t.Fatalf("buildND err: %v", err)
|
|
}
|
|
tr, err := NewKDTree(pts, WithMetric(WeightedCosineDistance{Weights: weights}))
|
|
if err != nil {
|
|
t.Fatalf("kdt err: %v", err)
|
|
}
|
|
q := []float64{0.5 * weights[0], 0.5 * weights[1]} // mid direction
|
|
_, d, ok := tr.Nearest(q)
|
|
if !ok {
|
|
t.Fatalf("no nearest")
|
|
}
|
|
if d < 0 || d > 2 {
|
|
t.Fatalf("cosine distance out of bounds: %v", d)
|
|
}
|
|
}
|