Some checks failed
Coverage / coverage (push) Failing after 6s
Tests / test (1.24.x, ubuntu-latest) (push) Failing after 42s
Tests / test (1.26.x, ubuntu-latest) (push) Failing after 38s
Tests / test (1.25.x, ubuntu-latest) (push) Failing after 35s
Tests / test (1.25.x, windows-latest) (push) Has been cancelled
Tests / test (1.26.x, macos-latest) (push) Has been cancelled
Tests / test (1.26.x, windows-latest) (push) Has been cancelled
Tests / test (1.24.x, macos-latest) (push) Has been cancelled
Tests / test (1.24.x, windows-latest) (push) Has been cancelled
Tests / test (1.25.x, macos-latest) (push) Has been cancelled
129 lines
4.4 KiB
Go
129 lines
4.4 KiB
Go
// Copyright (c) 2026 Couchbase, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package geov2
|
|
|
|
import (
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/blevesearch/geo/s2"
|
|
)
|
|
|
|
func TestBinarySearchLeftmostGreaterOrEqual(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
arr []uint64
|
|
target uint64
|
|
want int
|
|
}{
|
|
{"empty", nil, 5, 0},
|
|
{"all less than target", []uint64{1, 2, 3}, 5, 3},
|
|
{"all greater than target", []uint64{5, 6, 7}, 1, 0},
|
|
{"exact match", []uint64{1, 3, 5, 7}, 5, 2},
|
|
{"target between elements", []uint64{1, 3, 5, 7}, 4, 2},
|
|
{"leftmost of duplicates", []uint64{1, 3, 3, 3, 5}, 3, 1},
|
|
{"target before all", []uint64{2, 4, 6}, 0, 0},
|
|
{"target equals last", []uint64{2, 4, 6}, 6, 2},
|
|
{"single element hit", []uint64{9}, 9, 0},
|
|
{"single element miss high", []uint64{9}, 10, 1},
|
|
}
|
|
for _, test := range tests {
|
|
if got := binarySearchLeftmostGreaterOrEqual(test.arr, test.target); got != test.want {
|
|
t.Errorf("%s: binarySearchLeftmostGreaterOrEqual(%v, %d) = %d, want %d",
|
|
test.name, test.arr, test.target, got, test.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRangeScanOne verifies the two scoring paths of rangeScanOne: index
|
|
// cells that fall within the query cell's range (the query cell itself and
|
|
// its descendants), and ancestor cells found by walking the query cell's
|
|
// parents (which lie outside the range).
|
|
func TestRangeScanOne(t *testing.T) {
|
|
const queryLevel = 10
|
|
|
|
queryCell := uint64(s2.CellIDFromFace(2).ChildBeginAtLevel(queryLevel))
|
|
childCell := uint64(s2.CellID(queryCell).ChildBeginAtLevel(12)) // descendant, in range
|
|
parentCell := uint64(s2.CellID(queryCell).Parent(5)) // ancestor, out of range
|
|
unrelated := uint64(s2.CellIDFromFace(4).ChildBeginAtLevel(queryLevel))
|
|
|
|
// assign each cell a distinct doc ID
|
|
type entry struct {
|
|
cell uint64
|
|
docID uint32
|
|
}
|
|
entries := []entry{
|
|
{queryCell, 0},
|
|
{childCell, 1},
|
|
{parentCell, 2},
|
|
{unrelated, 3},
|
|
}
|
|
|
|
// index cells must be sorted ascending, with docIds kept parallel
|
|
sort.Slice(entries, func(i, j int) bool {
|
|
return entries[i].cell < entries[j].cell
|
|
})
|
|
indexCells := make([]uint64, len(entries))
|
|
docIds := make([]uint32, len(entries))
|
|
for i, e := range entries {
|
|
indexCells[i] = e.cell
|
|
docIds[i] = e.docID
|
|
}
|
|
|
|
scores := make(map[uint32]uint64)
|
|
minVal, maxVal := getCellSearchBounds(queryCell)
|
|
rangeScanOne(queryCell, minVal, maxVal, queryLevel, indexCells, docIds, scores)
|
|
|
|
// docID 0 is the query cell itself: in range, equal levels
|
|
if want := calcScore(queryLevel, queryLevel); scores[0] != want {
|
|
t.Errorf("query cell score = %d, want %d", scores[0], want)
|
|
}
|
|
// docID 1 is a descendant at level 12: in range, index cell deeper
|
|
if want := calcScore(queryLevel, 12); scores[1] != want {
|
|
t.Errorf("descendant cell score = %d, want %d", scores[1], want)
|
|
}
|
|
// docID 2 is an ancestor at level 5: found via the parent walk
|
|
if want := calcScore(queryLevel, 5); scores[2] != want {
|
|
t.Errorf("ancestor cell score = %d, want %d", scores[2], want)
|
|
}
|
|
// docID 3 is unrelated (different face): must not be scored
|
|
if scores[3] != 0 {
|
|
t.Errorf("unrelated cell score = %d, want 0", scores[3])
|
|
}
|
|
}
|
|
|
|
// TestRangeScanOneNoMatches confirms that a query cell with no overlapping
|
|
// index cells produces no scores, exercising the empty-range case where
|
|
// end < start.
|
|
func TestRangeScanOneNoMatches(t *testing.T) {
|
|
const queryLevel = 10
|
|
queryCell := uint64(s2.CellIDFromFace(0).ChildBeginAtLevel(queryLevel))
|
|
|
|
// index cells entirely on a different, non-ancestor part of the tree
|
|
indexCells := []uint64{
|
|
uint64(s2.CellIDFromFace(5).ChildBeginAtLevel(queryLevel)),
|
|
uint64(s2.CellIDFromFace(5).ChildBeginAtLevel(queryLevel).Next()),
|
|
}
|
|
docIds := []uint32{0, 1}
|
|
scores := make(map[uint32]uint64)
|
|
|
|
minVal, maxVal := getCellSearchBounds(queryCell)
|
|
rangeScanOne(queryCell, minVal, maxVal, queryLevel, indexCells, docIds, scores)
|
|
|
|
if len(scores) != 0 {
|
|
t.Errorf("expected no scores, got %d entries: %v", len(scores), scores)
|
|
}
|
|
}
|