forked from Snider/Poindexter
Enhance CI workflow with coverage options and add tests for KDTree functionality
This commit is contained in:
parent
3a67ba031b
commit
054c9af39e
14 changed files with 499 additions and 2 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -38,7 +38,7 @@ jobs:
|
||||||
run: go vet ./...
|
run: go vet ./...
|
||||||
|
|
||||||
- name: Test (race + coverage)
|
- name: Test (race + coverage)
|
||||||
run: go test -race -coverprofile=coverage.out -covermode=atomic ./...
|
run: go test -race -coverprofile=coverage.out -covermode=atomic -coverpkg=./... ./...
|
||||||
|
|
||||||
- name: Fuzz (10s)
|
- name: Fuzz (10s)
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
10
Makefile
10
Makefile
|
|
@ -54,9 +54,17 @@ race: ## Run tests with race detector
|
||||||
|
|
||||||
.PHONY: cover
|
.PHONY: cover
|
||||||
cover: ## Run tests with race + coverage and summarize
|
cover: ## Run tests with race + coverage and summarize
|
||||||
$(GO) test -race -coverprofile=$(COVEROUT) -covermode=atomic ./...
|
$(GO) test -race -coverprofile=$(COVEROUT) -covermode=atomic ./...
|
||||||
@$(GO) tool cover -func=$(COVEROUT) | tail -n 1
|
@$(GO) tool cover -func=$(COVEROUT) | tail -n 1
|
||||||
|
|
||||||
|
.PHONY: coverfunc
|
||||||
|
coverfunc: ## Print per-function coverage from $(COVEROUT)
|
||||||
|
@$(GO) tool cover -func=$(COVEROUT)
|
||||||
|
|
||||||
|
.PHONY: cover-kdtree
|
||||||
|
cover-kdtree: ## Print coverage details for kdtree.go only
|
||||||
|
@$(GO) tool cover -func=$(COVEROUT) | grep 'kdtree.go' || true
|
||||||
|
|
||||||
.PHONY: coverhtml
|
.PHONY: coverhtml
|
||||||
coverhtml: cover ## Generate HTML coverage report at $(COVERHTML)
|
coverhtml: cover ## Generate HTML coverage report at $(COVERHTML)
|
||||||
@$(GO) tool cover -html=$(COVEROUT) -o $(COVERHTML)
|
@$(GO) tool cover -html=$(COVEROUT) -o $(COVERHTML)
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,8 @@ The repository includes a maintainer-friendly `Makefile` that mirrors CI tasks a
|
||||||
- race — run tests with the race detector
|
- race — run tests with the race detector
|
||||||
- cover — run tests with race + coverage (writes `coverage.out` and prints summary)
|
- cover — run tests with race + coverage (writes `coverage.out` and prints summary)
|
||||||
- coverhtml — render HTML coverage report to `coverage.html`
|
- coverhtml — render HTML coverage report to `coverage.html`
|
||||||
|
- coverfunc — print per-function coverage (from `coverage.out`)
|
||||||
|
- cover-kdtree — print coverage details filtered to `kdtree.go`
|
||||||
- fuzz — run Go fuzzing for a configurable time (default 10s) matching CI
|
- fuzz — run Go fuzzing for a configurable time (default 10s) matching CI
|
||||||
- bench — run benchmarks with `-benchmem` (writes `bench.txt`)
|
- bench — run benchmarks with `-benchmem` (writes `bench.txt`)
|
||||||
- lint — run `golangci-lint` (if installed)
|
- lint — run `golangci-lint` (if installed)
|
||||||
|
|
|
||||||
48
examples/dht_ping_1d/example_test.go
Normal file
48
examples/dht_ping_1d/example_test.go
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
poindexter "github.com/Snider/Poindexter"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type peer struct {
|
||||||
|
Addr string
|
||||||
|
Ping int
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExample1D ensures the 1D example logic runs and exercises KDTree paths.
|
||||||
|
func TestExample1D(t *testing.T) {
|
||||||
|
// Same toy table as the example
|
||||||
|
table := []peer{
|
||||||
|
{Addr: "peer1.example:4001", Ping: 74},
|
||||||
|
{Addr: "peer2.example:4001", Ping: 52},
|
||||||
|
{Addr: "peer3.example:4001", Ping: 110},
|
||||||
|
{Addr: "peer4.example:4001", Ping: 35},
|
||||||
|
{Addr: "peer5.example:4001", Ping: 60},
|
||||||
|
{Addr: "peer6.example:4001", Ping: 44},
|
||||||
|
}
|
||||||
|
pts := make([]poindexter.KDPoint[peer], 0, len(table))
|
||||||
|
for i, p := range table {
|
||||||
|
pts = append(pts, poindexter.KDPoint[peer]{
|
||||||
|
ID: fmt.Sprintf("peer-%d", i+1),
|
||||||
|
Coords: []float64{float64(p.Ping)},
|
||||||
|
Value: p,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
kdt, err := poindexter.NewKDTree(pts, poindexter.WithMetric(poindexter.EuclideanDistance{}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewKDTree err: %v", err)
|
||||||
|
}
|
||||||
|
best, d, ok := kdt.Nearest([]float64{0})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("no nearest")
|
||||||
|
}
|
||||||
|
// Expect the minimum ping (35ms)
|
||||||
|
if best.Value.Ping != 35 {
|
||||||
|
t.Fatalf("expected best ping 35ms, got %d", best.Value.Ping)
|
||||||
|
}
|
||||||
|
if d <= 0 {
|
||||||
|
t.Fatalf("expected positive distance, got %v", d)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
examples/dht_ping_1d/main_test.go
Normal file
9
examples/dht_ping_1d/main_test.go
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// TestExampleMain runs the example's main function to ensure it executes without panic.
|
||||||
|
// This also allows the example code paths to be included in coverage reports.
|
||||||
|
func TestExampleMain(t *testing.T) {
|
||||||
|
main()
|
||||||
|
}
|
||||||
49
examples/kdtree_2d_ping_hop/example_test.go
Normal file
49
examples/kdtree_2d_ping_hop/example_test.go
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
poindexter "github.com/Snider/Poindexter"
|
||||||
|
)
|
||||||
|
|
||||||
|
type peer2 struct {
|
||||||
|
ID string
|
||||||
|
PingMS float64
|
||||||
|
Hops float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExample2D(t *testing.T) {
|
||||||
|
peers := []peer2{
|
||||||
|
{ID: "A", PingMS: 22, Hops: 3},
|
||||||
|
{ID: "B", PingMS: 34, Hops: 2},
|
||||||
|
{ID: "C", PingMS: 15, Hops: 4},
|
||||||
|
{ID: "D", PingMS: 55, Hops: 1},
|
||||||
|
{ID: "E", PingMS: 18, Hops: 2},
|
||||||
|
}
|
||||||
|
weights := [2]float64{1.0, 1.0}
|
||||||
|
invert := [2]bool{false, false}
|
||||||
|
pts, err := poindexter.Build2D(
|
||||||
|
peers,
|
||||||
|
func(p peer2) string { return p.ID },
|
||||||
|
func(p peer2) float64 { return p.PingMS },
|
||||||
|
func(p peer2) float64 { return p.Hops },
|
||||||
|
weights, invert,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Build2D err: %v", err)
|
||||||
|
}
|
||||||
|
tr, err := poindexter.NewKDTree(pts, poindexter.WithMetric(poindexter.ManhattanDistance{}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewKDTree err: %v", err)
|
||||||
|
}
|
||||||
|
best, d, ok := tr.Nearest([]float64{0, 0.3})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("no nearest")
|
||||||
|
}
|
||||||
|
if best.ID == "" {
|
||||||
|
t.Fatalf("unexpected empty ID")
|
||||||
|
}
|
||||||
|
if d < 0 {
|
||||||
|
t.Fatalf("negative distance: %v", d)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
examples/kdtree_2d_ping_hop/main_test.go
Normal file
7
examples/kdtree_2d_ping_hop/main_test.go
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestExample2D_Main(t *testing.T) {
|
||||||
|
main()
|
||||||
|
}
|
||||||
50
examples/kdtree_3d_ping_hop_geo/example_test.go
Normal file
50
examples/kdtree_3d_ping_hop_geo/example_test.go
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
poindexter "github.com/Snider/Poindexter"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type peer3test struct {
|
||||||
|
ID string
|
||||||
|
PingMS float64
|
||||||
|
Hops float64
|
||||||
|
GeoKM float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExample3D(t *testing.T) {
|
||||||
|
peers := []peer3test{
|
||||||
|
{ID: "A", PingMS: 22, Hops: 3, GeoKM: 1200},
|
||||||
|
{ID: "B", PingMS: 34, Hops: 2, GeoKM: 800},
|
||||||
|
{ID: "C", PingMS: 15, Hops: 4, GeoKM: 4500},
|
||||||
|
{ID: "D", PingMS: 55, Hops: 1, GeoKM: 300},
|
||||||
|
{ID: "E", PingMS: 18, Hops: 2, GeoKM: 2200},
|
||||||
|
}
|
||||||
|
weights := [3]float64{1.0, 0.7, 0.3}
|
||||||
|
invert := [3]bool{false, false, false}
|
||||||
|
pts, err := poindexter.Build3D(
|
||||||
|
peers,
|
||||||
|
func(p peer3test) string { return p.ID },
|
||||||
|
func(p peer3test) float64 { return p.PingMS },
|
||||||
|
func(p peer3test) float64 { return p.Hops },
|
||||||
|
func(p peer3test) float64 { return p.GeoKM },
|
||||||
|
weights, invert,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Build3D err: %v", err)
|
||||||
|
}
|
||||||
|
tr, err := poindexter.NewKDTree(pts, poindexter.WithMetric(poindexter.EuclideanDistance{}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewKDTree err: %v", err)
|
||||||
|
}
|
||||||
|
best, d, ok := tr.Nearest([]float64{0, weights[1] * 0.2, weights[2] * 0.4})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("no nearest")
|
||||||
|
}
|
||||||
|
if best.ID == "" {
|
||||||
|
t.Fatalf("unexpected empty ID")
|
||||||
|
}
|
||||||
|
if d < 0 {
|
||||||
|
t.Fatalf("negative distance: %v", d)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
examples/kdtree_3d_ping_hop_geo/main_test.go
Normal file
7
examples/kdtree_3d_ping_hop_geo/main_test.go
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestExample3D_Main(t *testing.T) {
|
||||||
|
main()
|
||||||
|
}
|
||||||
52
examples/kdtree_4d_ping_hop_geo_score/example_test.go
Normal file
52
examples/kdtree_4d_ping_hop_geo_score/example_test.go
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
poindexter "github.com/Snider/Poindexter"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type peer4test struct {
|
||||||
|
ID string
|
||||||
|
PingMS float64
|
||||||
|
Hops float64
|
||||||
|
GeoKM float64
|
||||||
|
Score float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExample4D(t *testing.T) {
|
||||||
|
peers := []peer4test{
|
||||||
|
{ID: "A", PingMS: 22, Hops: 3, GeoKM: 1200, Score: 0.86},
|
||||||
|
{ID: "B", PingMS: 34, Hops: 2, GeoKM: 800, Score: 0.91},
|
||||||
|
{ID: "C", PingMS: 15, Hops: 4, GeoKM: 4500, Score: 0.70},
|
||||||
|
{ID: "D", PingMS: 55, Hops: 1, GeoKM: 300, Score: 0.95},
|
||||||
|
{ID: "E", PingMS: 18, Hops: 2, GeoKM: 2200, Score: 0.80},
|
||||||
|
}
|
||||||
|
weights := [4]float64{1.0, 0.7, 0.2, 1.2}
|
||||||
|
invert := [4]bool{false, false, false, true}
|
||||||
|
pts, err := poindexter.Build4D(
|
||||||
|
peers,
|
||||||
|
func(p peer4test) string { return p.ID },
|
||||||
|
func(p peer4test) float64 { return p.PingMS },
|
||||||
|
func(p peer4test) float64 { return p.Hops },
|
||||||
|
func(p peer4test) float64 { return p.GeoKM },
|
||||||
|
func(p peer4test) float64 { return p.Score },
|
||||||
|
weights, invert,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Build4D err: %v", err)
|
||||||
|
}
|
||||||
|
tr, err := poindexter.NewKDTree(pts, poindexter.WithMetric(poindexter.EuclideanDistance{}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewKDTree err: %v", err)
|
||||||
|
}
|
||||||
|
best, d, ok := tr.Nearest([]float64{0, weights[1] * 0.2, weights[2] * 0.3, 0})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("no nearest")
|
||||||
|
}
|
||||||
|
if best.ID == "" {
|
||||||
|
t.Fatalf("unexpected empty ID")
|
||||||
|
}
|
||||||
|
if d < 0 {
|
||||||
|
t.Fatalf("negative distance: %v", d)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
examples/kdtree_4d_ping_hop_geo_score/main_test.go
Normal file
7
examples/kdtree_4d_ping_hop_geo_score/main_test.go
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestExample4D_Main(t *testing.T) {
|
||||||
|
main()
|
||||||
|
}
|
||||||
42
kdtree_branches_test.go
Normal file
42
kdtree_branches_test.go
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
package poindexter
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestKNearest_EdgeCases(t *testing.T) {
|
||||||
|
pts := []KDPoint[int]{
|
||||||
|
{ID: "a", Coords: []float64{0}},
|
||||||
|
}
|
||||||
|
tr, _ := NewKDTree(pts)
|
||||||
|
// k <= 0 → nil
|
||||||
|
ns, ds := tr.KNearest([]float64{0}, 0)
|
||||||
|
if ns != nil || ds != nil {
|
||||||
|
t.Fatalf("expected nil for k<=0, got %v %v", ns, ds)
|
||||||
|
}
|
||||||
|
// query-dim mismatch → nil
|
||||||
|
ns, ds = tr.KNearest([]float64{0, 1}, 1)
|
||||||
|
if ns != nil || ds != nil {
|
||||||
|
t.Fatalf("expected nil for dim mismatch, got %v %v", ns, ds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRadius_QueryDimMismatch(t *testing.T) {
|
||||||
|
pts := []KDPoint[int]{{ID: "p", Coords: []float64{0}}}
|
||||||
|
tr, _ := NewKDTree(pts)
|
||||||
|
ns, ds := tr.Radius([]float64{0, 0}, 1)
|
||||||
|
if ns != nil || ds != nil {
|
||||||
|
t.Fatalf("expected nil for dim mismatch, got %v %v", ns, ds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInsert_DimMismatch(t *testing.T) {
|
||||||
|
tr, _ := NewKDTreeFromDim[int](2)
|
||||||
|
ok := tr.Insert(KDPoint[int]{ID: "bad", Coords: []float64{0}}) // wrong dim
|
||||||
|
if ok {
|
||||||
|
t.Fatalf("expected false on insert with dim mismatch")
|
||||||
|
}
|
||||||
|
// inserting with empty ID should succeed and not touch idIndex
|
||||||
|
ok = tr.Insert(KDPoint[int]{ID: "", Coords: []float64{0, 0}})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected true on insert with empty ID and matching dim")
|
||||||
|
}
|
||||||
|
}
|
||||||
116
kdtree_extra_test.go
Normal file
116
kdtree_extra_test.go
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
package poindexter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewKDTree_Errors(t *testing.T) {
|
||||||
|
// empty points
|
||||||
|
if _, err := NewKDTree[string](nil); !errors.Is(err, ErrEmptyPoints) {
|
||||||
|
t.Fatalf("want ErrEmptyPoints, got %v", err)
|
||||||
|
}
|
||||||
|
// zero-dim
|
||||||
|
pts0 := []KDPoint[string]{{ID: "A", Coords: nil}}
|
||||||
|
if _, err := NewKDTree(pts0); !errors.Is(err, ErrZeroDim) {
|
||||||
|
t.Fatalf("want ErrZeroDim, got %v", err)
|
||||||
|
}
|
||||||
|
// dim mismatch
|
||||||
|
ptsDim := []KDPoint[string]{
|
||||||
|
{ID: "A", Coords: []float64{0}},
|
||||||
|
{ID: "B", Coords: []float64{0, 1}},
|
||||||
|
}
|
||||||
|
if _, err := NewKDTree(ptsDim); !errors.Is(err, ErrDimMismatch) {
|
||||||
|
t.Fatalf("want ErrDimMismatch, got %v", err)
|
||||||
|
}
|
||||||
|
// duplicate IDs
|
||||||
|
ptsDup := []KDPoint[string]{
|
||||||
|
{ID: "X", Coords: []float64{0}},
|
||||||
|
{ID: "X", Coords: []float64{1}},
|
||||||
|
}
|
||||||
|
if _, err := NewKDTree(ptsDup); !errors.Is(err, ErrDuplicateID) {
|
||||||
|
t.Fatalf("want ErrDuplicateID, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteByID_NotFound(t *testing.T) {
|
||||||
|
pts := []KDPoint[int]{
|
||||||
|
{ID: "A", Coords: []float64{0}, Value: 1},
|
||||||
|
}
|
||||||
|
tr, err := NewKDTree(pts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewKDTree err: %v", err)
|
||||||
|
}
|
||||||
|
if tr.DeleteByID("NOPE") {
|
||||||
|
t.Fatalf("expected false for missing ID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKNearest_KGreaterThanN(t *testing.T) {
|
||||||
|
pts := []KDPoint[int]{
|
||||||
|
{ID: "a", Coords: []float64{0}},
|
||||||
|
{ID: "b", Coords: []float64{2}},
|
||||||
|
}
|
||||||
|
tr, _ := NewKDTree(pts)
|
||||||
|
ns, ds := tr.KNearest([]float64{1}, 5)
|
||||||
|
if len(ns) != 2 || len(ds) != 2 {
|
||||||
|
t.Fatalf("want 2 neighbors, got %d", len(ns))
|
||||||
|
}
|
||||||
|
if !(ds[0] <= ds[1]) {
|
||||||
|
t.Fatalf("distances not sorted: %v", ds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRadius_BoundaryAndZero(t *testing.T) {
|
||||||
|
pts := []KDPoint[int]{
|
||||||
|
{ID: "o", Coords: []float64{0}},
|
||||||
|
{ID: "one", Coords: []float64{1}},
|
||||||
|
}
|
||||||
|
tr, _ := NewKDTree(pts, WithMetric(EuclideanDistance{}))
|
||||||
|
// radius exactly includes point at distance 1
|
||||||
|
within, _ := tr.Radius([]float64{0}, 1)
|
||||||
|
foundOne := false
|
||||||
|
for _, p := range within {
|
||||||
|
if p.ID == "one" {
|
||||||
|
foundOne = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundOne {
|
||||||
|
t.Fatalf("expected to include point at exact radius")
|
||||||
|
}
|
||||||
|
// radius zero should include exact match only
|
||||||
|
within0, _ := tr.Radius([]float64{0}, 0)
|
||||||
|
if len(within0) == 0 || within0[0].ID != "o" {
|
||||||
|
t.Fatalf("expected only origin at r=0, got %v", within0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewKDTreeFromDim_WithMetric_InsertQuery(t *testing.T) {
|
||||||
|
tr, err := NewKDTreeFromDim[string](2, WithMetric(ManhattanDistance{}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
ok := tr.Insert(KDPoint[string]{ID: "A", Coords: []float64{0, 0}, Value: "a"})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("insert failed")
|
||||||
|
}
|
||||||
|
tr.Insert(KDPoint[string]{ID: "B", Coords: []float64{2, 2}, Value: "b"})
|
||||||
|
p, d, ok := tr.Nearest([]float64{1, 0})
|
||||||
|
if !ok || p.ID != "A" {
|
||||||
|
t.Fatalf("expected A nearest, got %v", p)
|
||||||
|
}
|
||||||
|
if d != 1 { // ManhattanDistance from (1,0) to (0,0) is 1
|
||||||
|
t.Fatalf("expected manhattan distance 1, got %v", d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNearest_QueryDimMismatch(t *testing.T) {
|
||||||
|
pts := []KDPoint[int]{
|
||||||
|
{ID: "a", Coords: []float64{0, 0}},
|
||||||
|
}
|
||||||
|
tr, _ := NewKDTree(pts)
|
||||||
|
_, _, ok := tr.Nearest([]float64{0})
|
||||||
|
if ok {
|
||||||
|
t.Fatalf("expected ok=false for query dim mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
100
kdtree_morecov_test.go
Normal file
100
kdtree_morecov_test.go
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
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 either A or C to be nearest for respective queries: %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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue