mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-09 05:53:42 +08:00
ab81464bd1
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>
408 lines
13 KiB
Go
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")
|
|
}
|
|
}
|