milergeo added

This commit is contained in:
2026-10-06 12:26:57 +05:30
parent 220e934045
commit 9b94be1f07
10 changed files with 329 additions and 44 deletions

View File

@@ -0,0 +1,120 @@
package milergeo
import (
"context"
"errors"
"strings"
"testing"
"github.com/redis/go-redis/v9"
)
// fakeRedis answers the three geo commands the way a given Redis would.
type fakeRedis struct {
searchErr error // GEOSEARCH result; nil = supported
radiusErr error
addErr error
locs []redis.GeoLocation
searchHits int
radiusHits int
}
func (f *fakeRedis) GeoSearchLocation(ctx context.Context, _ string, q *redis.GeoSearchLocationQuery) *redis.GeoSearchLocationCmd {
f.searchHits++
cmd := redis.NewGeoSearchLocationCmd(ctx, q)
if f.searchErr != nil {
cmd.SetErr(f.searchErr)
} else {
cmd.SetVal(f.locs)
}
return cmd
}
func (f *fakeRedis) GeoRadius(_ context.Context, _ string, _, _ float64, _ *redis.GeoRadiusQuery) *redis.GeoLocationCmd {
f.radiusHits++
return redis.NewGeoLocationCmdResult(f.locs, f.radiusErr)
}
func (f *fakeRedis) GeoAdd(ctx context.Context, _ string, _ ...*redis.GeoLocation) *redis.IntCmd {
cmd := redis.NewIntCmd(ctx)
if f.addErr != nil {
cmd.SetErr(f.addErr)
} else {
cmd.SetVal(1)
}
return cmd
}
var nearby = []redis.GeoLocation{{Name: "38", Dist: 0.03}, {Name: "23", Dist: 0.4}}
func TestSearchUsesGeosearchOnRedis62(t *testing.T) {
legacyOnly.Store(false)
f := &fakeRedis{locs: nearby}
got, err := Search(context.Background(), f, 11.0053, 76.9511, 10, 10)
if err != nil || len(got) != 2 || got[0].Name != "38" {
t.Fatalf("got %v, %v", got, err)
}
if f.searchHits != 1 || f.radiusHits != 0 {
t.Fatalf("GEOSEARCH must be used when supported: search=%d radius=%d", f.searchHits, f.radiusHits)
}
}
// The production failure: Redis < 6.2 has no GEOSEARCH, so every order found
// "no riders". The fallback must find them, and stop retrying GEOSEARCH.
func TestSearchFallsBackOnOldRedis(t *testing.T) {
legacyOnly.Store(false)
t.Cleanup(func() { legacyOnly.Store(false) })
f := &fakeRedis{searchErr: errors.New("ERR unknown command 'GEOSEARCH', with args beginning with: 'milers:locations'"), locs: nearby}
for i := 0; i < 3; i++ {
got, err := Search(context.Background(), f, 11.0053, 76.9511, 10, 10)
if err != nil || len(got) != 2 {
t.Fatalf("call %d: got %v, %v", i, got, err)
}
}
if f.searchHits != 1 || f.radiusHits != 3 {
t.Fatalf("after the first rejection only GEORADIUS should run: search=%d radius=%d", f.searchHits, f.radiusHits)
}
if p := Probe(context.Background(), f); !strings.Contains(p, "GEORADIUS fallback") {
t.Fatalf("probe = %q", p)
}
}
// Any other failure must surface, not read as an empty street.
func TestSearchReturnsOtherErrors(t *testing.T) {
legacyOnly.Store(false)
f := &fakeRedis{searchErr: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")}
if _, err := Search(context.Background(), f, 11, 76, 10, 10); err == nil || !strings.Contains(err.Error(), "WRONGTYPE") {
t.Fatalf("want the WRONGTYPE error, got %v", err)
}
if f.radiusHits != 0 || legacyOnly.Load() {
t.Fatal("a non-'unknown command' error must not switch to the fallback")
}
if p := Probe(context.Background(), f); !strings.HasPrefix(p, "error: ") {
t.Fatalf("probe = %q", p)
}
}
func TestIndexReportsWriteFailure(t *testing.T) {
if err := Index(context.Background(), &fakeRedis{}, 38, 11.0, 76.9); err != nil {
t.Fatalf("ok write: %v", err)
}
err := Index(context.Background(), &fakeRedis{addErr: errors.New("WRONGTYPE Operation")}, 38, 11.0, 76.9)
if err == nil || !strings.Contains(err.Error(), "WRONGTYPE") {
t.Fatalf("want the write error, got %v", err)
}
}
// db.Rdb is a *redis.Client; before InitRedis it is a nil pointer, which in
// an interface is not == nil. It must be refused, not dereferenced.
func TestNilClientIsRefused(t *testing.T) {
var rdb *redis.Client
if _, err := Search(context.Background(), rdb, 11, 76, 10, 10); err == nil {
t.Fatal("nil client must be an error")
}
if err := Index(context.Background(), rdb, 1, 11, 76); err == nil {
t.Fatal("nil client must be an error")
}
if p := Probe(context.Background(), rdb); !strings.HasPrefix(p, "error") {
t.Fatalf("probe = %q", p)
}
}