Files
macker/server/addresses_test.go
T

114 lines
3.8 KiB
Go
Raw Normal View History

2026-10-28 14:04:39 -05:00
package main
import (
"encoding/json"
"math"
"math/rand"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"macker/datapoint"
)
const addressFixture = `<osm version="0.6">
<node id="1" lat="32.3" lon="-90.2"><tag k="addr:housenumber" v="100"/><tag k="addr:street" v="Main Street"/><tag k="addr:city" v="Jackson"/></node>
<node id="2" lat="32.31" lon="-90.2"/>
<node id="3" lat="32.31" lon="-90.19"/>
<node id="4" lat="32.32" lon="-90.19"/>
<node id="5" lat="32.32" lon="-90.2"/>
<node id="6" lat="31" lon="-90"><tag k="addr:street" v="Missing number"/></node>
<way id="10"><nd ref="2"/><nd ref="3"/><nd ref="4"/><nd ref="5"/><nd ref="2"/><tag k="addr:housenumber" v="200"/><tag k="addr:street" v="Oak Street"/></way>
<way id="11"><nd ref="999"/><tag k="addr:housenumber" v="300"/><tag k="addr:street" v="Missing geometry"/></way>
</osm>`
func TestImportLocalAddresses(t *testing.T) {
points, err := importAddresses(strings.NewReader(addressFixture))
if err != nil {
t.Fatal(err)
}
if len(points) != 2 || points[0].Label != "100 Main Street, Jackson" || points[1].Label != "200 Oak Street" {
t.Fatalf("bad addresses: %+v", points)
}
if math.Abs(points[1].Lat-32.315) > 1e-8 || math.Abs(points[1].Lon-(-90.195)) > 1e-8 {
t.Fatalf("incorrect closed-way position: %+v", points[1])
}
for _, input := range []string{"<osm><node", "<osm></osm>"} {
if _, err = importAddresses(strings.NewReader(input)); err == nil {
t.Fatal("accepted invalid/empty import")
}
}
}
func TestLocalAddressBuildLoadAndAPI(t *testing.T) {
dir := t.TempDir()
source := filepath.Join(dir, "source.osm")
target := filepath.Join(dir, "data", "addresses.json")
if err := os.WriteFile(source, []byte(addressFixture), 0600); err != nil {
t.Fatal(err)
}
if count, err := buildAddresses(source, target); err != nil || count != 2 {
t.Fatalf("import: %d %v", count, err)
}
index, err := loadAddresses(target)
if err != nil {
t.Fatal(err)
}
tr := &tracker{addresses: index}
tr.accept([]datapoint.DataPoint{{Lat: 32.3, Long: -90.2}}, time.Now())
rr := httptest.NewRecorder()
tr.handler().ServeHTTP(rr, httptest.NewRequest("GET", "/api/location", nil))
var got location
if err = json.Unmarshal(rr.Body.Bytes(), &got); err != nil {
t.Fatal(err)
}
if got.Nearby.Status != "ready" || got.Nearby.Address.Label != "100 Main Street, Jackson" || got.Nearby.Address.DistanceMeters != 0 {
t.Fatalf("bad response: %s", rr.Body.String())
}
if index.view(30.4, -89.1).Status != "unavailable" {
t.Fatal("returned distant address outside search radius")
}
// A failed refresh must preserve the last usable database.
if err = os.WriteFile(source, []byte("<osm>"), 0600); err != nil {
t.Fatal(err)
}
if _, err = buildAddresses(source, target); err == nil {
t.Fatal("bad refresh accepted")
}
if _, err = loadAddresses(target); err != nil {
t.Fatalf("old index lost after failed refresh: %v", err)
}
}
func TestLocalAddressIndexMatchesFullScan(t *testing.T) {
random := rand.New(rand.NewSource(42))
idx := &addressIndex{cells: make(map[addressCell][]addressPoint)}
var all []addressPoint
for i := 0; i < 2000; i++ {
p := addressPoint{Lat: 30.2 + random.Float64()*2.3, Lon: -91.6 + random.Float64()*3.3, Label: "Test address"}
all = append(all, p)
c := cellFor(p.Lat, p.Lon)
idx.cells[c] = append(idx.cells[c], p)
}
for i := 0; i < 300; i++ {
lat, lon := 30.2+random.Float64()*2.3, -91.6+random.Float64()*3.3
best := math.Inf(1)
for _, p := range all {
best = math.Min(best, distanceMeters(lat, lon, p.Lat, p.Lon))
}
got := idx.view(lat, lon)
if best > localAddressRange {
if got.Status != "unavailable" {
t.Fatal("range limit ignored")
}
continue
}
if got.Address == nil || math.Abs(got.Address.DistanceMeters-best) > 0.001 {
t.Fatalf("index missed nearest address: %+v; want %f", got, best)
}
}
}