Files
StarPilot/mapd_repo/maps/offline_validation_test.go
T
firestar5683 ab81464bd1 Maps
Add navigation and map matching providers with the vendored map service sources.

Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
2026-10-14 19:56:17 -05:00

408 lines
13 KiB
Go

package maps
import (
"encoding/binary"
"math"
"os"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
m "pfeifer.dev/mapd/math"
"capnproto.org/go/capnp/v3"
"capnproto.org/go/capnp/v3/packed"
"pfeifer.dev/mapd/cereal/offline"
)
func testOfflineTile(t *testing.T, edit func(offline.Offline, *capnp.Segment)) []byte {
t.Helper()
msg, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
if err != nil {
t.Fatal(err)
}
tile, err := offline.NewRootOffline(seg)
if err != nil {
t.Fatal(err)
}
tile.SetMinLat(35)
tile.SetMaxLat(35.25)
tile.SetMinLon(-98)
tile.SetMaxLon(-97.75)
if _, err := tile.NewWays(0); err != nil {
t.Fatal(err)
}
if edit != nil {
edit(tile, seg)
}
data, err := msg.MarshalPacked()
if err != nil {
t.Fatal(err)
}
return data
}
func validTestWay(t *testing.T, tile offline.Offline) offline.Way {
t.Helper()
ways, err := tile.NewWays(1)
if err != nil {
t.Fatal(err)
}
w := ways.At(0)
w.SetId(42)
w.SetMinLat(35.1)
w.SetMaxLat(35.2)
w.SetMinLon(-97.9)
w.SetMaxLon(-97.9)
nodes, err := w.NewNodes(2)
if err != nil {
t.Fatal(err)
}
nodes.At(0).SetLatitude(35.1)
nodes.At(0).SetLongitude(-97.9)
nodes.At(1).SetLatitude(35.2)
nodes.At(1).SetLongitude(-97.9)
return w
}
func TestOfflineRejectsMalformedWays(t *testing.T) {
for _, kind := range []string{"struct", "absent"} {
t.Run(kind, func(t *testing.T) {
data := testOfflineTile(t, func(tile offline.Offline, seg *capnp.Segment) {
var pointer capnp.Ptr
if kind == "struct" {
wrong, err := capnp.NewStruct(seg, capnp.ObjectSize{DataSize: 8})
if err != nil {
t.Fatal(err)
}
pointer = wrong.ToPtr()
}
if err := capnp.Struct(tile).SetPtr(0, pointer); err != nil {
t.Fatal(err)
}
})
if ReadOffline(data).Loaded {
t.Fatal("malformed or missing ways accepted as loaded")
}
})
}
}
func TestOfflineRejectsInvalidBounds(t *testing.T) {
cases := map[string]func(offline.Offline){
"nan": func(tile offline.Offline) { tile.SetMinLat(math.NaN()) },
"infinity": func(tile offline.Offline) { tile.SetMaxLon(math.Inf(1)) },
"latitude range": func(tile offline.Offline) { tile.SetMinLat(-91) },
"longitude range": func(tile offline.Offline) { tile.SetMaxLon(181) },
"reversed latitude": func(tile offline.Offline) { tile.SetMinLat(36) },
"zero width": func(tile offline.Offline) { tile.SetMinLon(tile.MaxLon()) },
"negative overlap": func(tile offline.Offline) { tile.SetOverlap(-1) },
"nonfinite overlap": func(tile offline.Offline) { tile.SetOverlap(math.NaN()) },
}
for name, edit := range cases {
t.Run(name, func(t *testing.T) {
data := testOfflineTile(t, func(tile offline.Offline, _ *capnp.Segment) { edit(tile) })
if ReadOffline(data).Loaded {
t.Fatal("invalid bounds accepted as loaded")
}
})
}
}
func TestOfflineAcceptsEmptyCoverageAndReadsWay(t *testing.T) {
empty := ReadOffline(testOfflineTile(t, nil))
if !empty.Loaded || empty.Ways.Len() != 0 {
t.Fatal("valid empty coverage rejected")
}
data := testOfflineTile(t, func(tile offline.Offline, seg *capnp.Segment) {
ways, err := tile.NewWays(1)
if err != nil {
t.Fatal(err)
}
way := ways.At(0)
way.SetId(42)
way.SetMaxSpeed(13.4112)
way.SetMinLat(35.1)
way.SetMaxLat(35.2)
way.SetMinLon(-97.9)
way.SetMaxLon(-97.9)
if err := way.SetName("Synthetic road"); err != nil {
t.Fatal(err)
}
nodes, err := way.NewNodes(2)
if err != nil {
t.Fatal(err)
}
nodes.At(0).SetLatitude(35.1)
nodes.At(0).SetLongitude(-97.9)
nodes.At(1).SetLatitude(35.2)
nodes.At(1).SetLongitude(-97.9)
})
parsed := ReadOffline(data)
if !parsed.Loaded || parsed.Ways.Len() != 1 {
t.Fatal("valid way rejected")
}
way := parsed.Ways.At(0)
if way.Id() != 42 || way.MaxSpeed() != 13.4112 || way.WayName() != "Synthetic road" || way.Nodes.Len() != 2 {
t.Fatal("way changed during parsing")
}
}
func TestOfflineRejectsTruncatedPayload(t *testing.T) {
payload := testOfflineTile(t, nil)
for _, data := range [][]byte{nil, payload[:len(payload)/2], {0xff, 0xff}} {
if ReadOffline(data).Loaded {
t.Fatal("unreadable packed data accepted as loaded")
}
}
}
func TestOfflineRejectsNestedInvalidValues(t *testing.T) {
cases := map[string]func(offline.Way){
"node nan": func(w offline.Way) { nodes, _ := w.Nodes(); nodes.At(0).SetLatitude(math.NaN()) },
"node out of range": func(w offline.Way) { nodes, _ := w.Nodes(); nodes.At(0).SetLongitude(181) },
"node outside way": func(w offline.Way) { w.SetMaxLat(35.15) },
"limit infinity": func(w offline.Way) { w.SetMaxSpeed(math.Inf(1)) },
"limit negative": func(w offline.Way) { w.SetMaxSpeedBackward(-1) },
"limit unrepresentable": func(w offline.Way) { w.SetAdvisorySpeed(math.MaxFloat64) },
"unknown class": func(w offline.Way) { w.SetHighwayClass(255) },
"zero identifier": func(w offline.Way) { w.SetId(0) },
"one node": func(w offline.Way) { _, _ = w.NewNodes(1) },
"missing nodes": func(w offline.Way) { _ = capnp.Struct(w).SetPtr(2, capnp.Ptr{}) },
"wrong node pointer": func(w offline.Way) { _ = capnp.Struct(w).SetPtr(2, capnp.Struct(w).ToPtr()) },
"wrong text pointer": func(w offline.Way) { _ = capnp.Struct(w).SetPtr(0, capnp.Struct(w).ToPtr()) },
}
for name, mutate := range cases {
t.Run(name, func(t *testing.T) {
data := testOfflineTile(t, func(tile offline.Offline, _ *capnp.Segment) { mutate(validTestWay(t, tile)) })
if ReadOffline(data).Loaded {
t.Fatal("invalid nested way accepted")
}
})
}
}
func TestOfflineRejectsWrongNestedListShapes(t *testing.T) {
cases := []struct {
name string
index uint16
makeList func(*capnp.Segment) (capnp.Ptr, error)
}{
{"byte nodes", 2, func(seg *capnp.Segment) (capnp.Ptr, error) {
l, err := capnp.NewUInt8List(seg, 2)
return l.ToPtr(), err
}},
{"uint64 nodes", 2, func(seg *capnp.Segment) (capnp.Ptr, error) {
l, err := capnp.NewUInt64List(seg, 2)
return l.ToPtr(), err
}},
{"void nodes", 2, func(seg *capnp.Segment) (capnp.Ptr, error) { return capnp.NewVoidList(seg, 2).ToPtr(), nil }},
{"short composite nodes", 2, func(seg *capnp.Segment) (capnp.Ptr, error) {
l, err := capnp.NewCompositeList(seg, capnp.ObjectSize{DataSize: 8}, 2)
return l.ToPtr(), err
}},
{"uint64 text", 0, func(seg *capnp.Segment) (capnp.Ptr, error) {
l, err := capnp.NewUInt64List(seg, 2)
return l.ToPtr(), err
}},
{"composite text", 0, func(seg *capnp.Segment) (capnp.Ptr, error) {
l, err := capnp.NewCompositeList(seg, capnp.ObjectSize{DataSize: 16}, 2)
return l.ToPtr(), err
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
data := testOfflineTile(t, func(tile offline.Offline, seg *capnp.Segment) {
way := validTestWay(t, tile)
ptr, err := tc.makeList(seg)
if err != nil {
t.Fatal(err)
}
if err := capnp.Struct(way).SetPtr(tc.index, ptr); err != nil {
t.Fatal(err)
}
})
if ReadOffline(data).Loaded {
t.Fatal("wrong nested wire type accepted")
}
})
}
}
func TestOfflineAllowsAppendCompatibleCoordinateLayout(t *testing.T) {
data := testOfflineTile(t, func(tile offline.Offline, seg *capnp.Segment) {
way := validTestWay(t, tile)
larger, err := capnp.NewCompositeList(seg, capnp.ObjectSize{DataSize: 24, PointerCount: 1}, 2)
if err != nil {
t.Fatal(err)
}
if err := capnp.Struct(way).SetPtr(2, larger.ToPtr()); err != nil {
t.Fatal(err)
}
nodes, err := way.Nodes()
if err != nil {
t.Fatal(err)
}
nodes.At(0).SetLatitude(35.1)
nodes.At(0).SetLongitude(-97.9)
nodes.At(1).SetLatitude(35.2)
nodes.At(1).SetLongitude(-97.9)
})
if !ReadOffline(data).Loaded {
t.Fatal("append-compatible coordinate struct rejected")
}
}
func TestOfflineAcceptsHistoricalFourPointerWay(t *testing.T) {
data := testOfflineTile(t, func(tile offline.Offline, seg *capnp.Segment) {
// The pre-conditional layout has the original 80-byte scalar area
// through ID but only name/ref/nodes/hazard pointer slots.
list, err := capnp.NewCompositeList(seg, capnp.ObjectSize{DataSize: 80, PointerCount: 4}, 1)
if err != nil {
t.Fatal(err)
}
if err := capnp.Struct(tile).SetPtr(0, list.ToPtr()); err != nil {
t.Fatal(err)
}
w := offline.Way(list.Struct(0))
w.SetId(42)
w.SetMaxSpeed(13.4)
w.SetMinLat(35.1)
w.SetMaxLat(35.2)
w.SetMinLon(-97.9)
w.SetMaxLon(-97.9)
if err := w.SetName("Historical road"); err != nil {
t.Fatal(err)
}
if err := w.SetRef("H1"); err != nil {
t.Fatal(err)
}
nodes, err := w.NewNodes(2)
if err != nil {
t.Fatal(err)
}
nodes.At(0).SetLatitude(35.1)
nodes.At(0).SetLongitude(-97.9)
nodes.At(1).SetLatitude(35.2)
nodes.At(1).SetLongitude(-97.9)
})
parsed := ReadOffline(data)
if !parsed.Loaded || parsed.Ways.Len() != 1 {
t.Fatal("historical tile rejected")
}
w := parsed.Ways.At(0)
if w.Id() != 42 || w.MaxSpeed() != 13.4 || w.WayName() != "Historical road" || w.WayRef() != "H1" || w.Nodes.Len() != 2 ||
w.HighwayClass() != offline.HighwayClass_unknown || w.MaxSpeedConditional() != "" ||
w.MaxSpeedForwardConditional() != "" || w.MaxSpeedBackwardConditional() != "" {
t.Fatal("historical geometry or absent optional fields changed")
}
}
func TestOfflineRejectsTrailingOrOversizeAndReusesValidatedGraph(t *testing.T) {
valid := testOfflineTile(t, func(tile offline.Offline, _ *capnp.Segment) {
w := validTestWay(t, tile)
if err := w.SetName(strings.Repeat("x", 2048)); err != nil {
t.Fatal(err)
}
})
if ReadOffline(append(append([]byte{}, valid...), valid...)).Loaded {
t.Fatal("second message accepted")
}
if ReadOffline(append(append([]byte{}, valid...), 0xff)).Loaded {
t.Fatal("trailing garbage accepted")
}
if ReadOffline(make([]byte, maxOfflinePackedBytes+1)).Loaded {
t.Fatal("oversize packed message accepted")
}
var largeHeader [8]byte
binary.LittleEndian.PutUint32(largeHeader[4:], 1<<24) // declares 128 MiB
if ReadOffline(packed.Pack(nil, largeHeader[:])).Loaded {
t.Fatal("oversize decoded message header accepted")
}
tile := ReadOffline(valid)
if !tile.Loaded {
t.Fatal("valid tile rejected")
}
// Repeated pointer reads exceed the one-time 256 MiB validation budget.
for i := 0; i < 150_000; i++ {
name, err := tile.waysRaw.At(0).Name()
if err != nil || len(name) != 2048 {
t.Fatalf("cached read %d: %v", i, err)
}
}
}
func TestOfflinePathRequiresExactAreaAndCurrentRegularFile(t *testing.T) {
old := DEFAULT_SETTINGS.OutputDirectory
defer func() { DEFAULT_SETTINGS.OutputDirectory = old }()
DEFAULT_SETTINGS.OutputDirectory = t.TempDir()
pos := m.NewPosition(35.15, -97.9)
area := Area{Box: m.Box{MinPos: m.NewPosition(35, -98), MaxPos: m.NewPosition(35.25, -97.75)}}
path := GenerateBoundsFileName(area, DEFAULT_SETTINGS)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatal(err)
}
wrong := testOfflineTile(t, func(tile offline.Offline, _ *capnp.Segment) { tile.SetMaxLat(35.5) })
if err := os.WriteFile(path, wrong, 0644); err != nil {
t.Fatal(err)
}
loaded, err := FindWaysAroundPosition(pos)
if err != nil || loaded.Loaded {
t.Fatalf("wrong area accepted: loaded=%v err=%v", loaded.Loaded, err)
}
valid := testOfflineTile(t, nil)
if err := os.WriteFile(path, valid, 0644); err != nil {
t.Fatal(err)
}
loaded, err = FindWaysAroundPosition(pos)
if err != nil || !loaded.Loaded || !loaded.SourceCurrent() {
t.Fatalf("valid path: loaded=%v err=%v", loaded.Loaded, err)
}
stage := path + ".stage"
if err := os.WriteFile(stage, valid, 0644); err != nil {
t.Fatal(err)
}
if err := os.Rename(stage, path); err != nil {
t.Fatal(err)
}
if loaded.SourceCurrent() {
t.Fatal("atomic replacement retained cache identity")
}
if err := os.Remove(path); err != nil {
t.Fatal(err)
}
if err := os.Symlink(stage, path); err != nil {
t.Fatal(err)
}
if _, err := FindWaysAroundPosition(pos); err == nil {
t.Fatal("symlink tile accepted")
}
}
func TestOfflinePathRejectsFifoWithoutBlocking(t *testing.T) {
old := DEFAULT_SETTINGS.OutputDirectory
defer func() { DEFAULT_SETTINGS.OutputDirectory = old }()
DEFAULT_SETTINGS.OutputDirectory = t.TempDir()
pos := m.NewPosition(35.15, -97.9)
area := Area{Box: m.Box{MinPos: m.NewPosition(35, -98), MaxPos: m.NewPosition(35.25, -97.75)}}
path := GenerateBoundsFileName(area, DEFAULT_SETTINGS)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatal(err)
}
if err := syscall.Mkfifo(path, 0600); err != nil {
t.Fatal(err)
}
done := make(chan error, 1)
go func() { _, err := FindWaysAroundPosition(pos); done <- err }()
select {
case err := <-done:
if err == nil {
t.Fatal("FIFO accepted as a tile")
}
case <-time.After(2 * time.Second):
t.Fatal("FIFO open blocked before special-file rejection")
}
}