Verified end-to-end on the local OSRM stack: - internal/traffic: 511NY OData client (lenient decode, raw archive), Overpass way matcher (class-window selection: highest-class road within 150m beats a closer service road), incident policy map, CSV builder - cmd/traffic: fetch | match | apply; apply copies the dataset (CoW) and re-customizes the traffic layer in ~15s (v26 customize has no --output-prefix; it writes contract files in place) - Proof: synthetic I-90 closure → 38s (static :5000) vs 142s (traffic :5002) on the same 0.87 km stretch - Policy speeds are 'assumed' provenance; R4: traffic routes must carry computed(traffic, as_of, policy=assumed)
113 lines
3.2 KiB
Go
113 lines
3.2 KiB
Go
package traffic
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestPolicyFor(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
ok bool
|
|
kph int
|
|
}{
|
|
{"Road Closed", true, 5},
|
|
{"ACCIDENT", true, 25},
|
|
{"Accident - Multi Vehicle", true, 25},
|
|
{"road work", true, 50},
|
|
{"Bridge Outage (Unknown)", false, 0},
|
|
{"", false, 0},
|
|
}
|
|
for _, c := range cases {
|
|
p, ok := PolicyFor(c.in)
|
|
if ok != c.ok || (ok && p.SpeedKmh != c.kph) {
|
|
t.Errorf("PolicyFor(%q) = %+v, %v; want ok=%v kph=%d", c.in, p, ok, c.ok, c.kph)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildCSV(t *testing.T) {
|
|
// same pair, two incidents → most severe (highest weight) wins
|
|
csv := BuildCSV([]Assignment{
|
|
{Pair: [2]int64{100, 200}, SpeedKmh: 50, Weight: 40},
|
|
{Pair: [2]int64{200, 100}, SpeedKmh: 5, Weight: 100}, // reversed order, must dedupe
|
|
{Pair: [2]int64{300, 400}, SpeedKmh: 25, Weight: 90},
|
|
})
|
|
lines := strings.Split(strings.TrimSpace(csv), "\n")
|
|
if len(lines) != 2 {
|
|
t.Fatalf("want 2 rows, got %d: %v", len(lines), lines)
|
|
}
|
|
if lines[0] != "100,200,5" {
|
|
t.Errorf("row0 = %q; want 100,200,5", lines[0])
|
|
}
|
|
if lines[1] != "300,400,25" {
|
|
t.Errorf("row1 = %q; want 300,400,25", lines[1])
|
|
}
|
|
}
|
|
|
|
func TestClassRank(t *testing.T) {
|
|
if !(classRank("motorway") > classRank("trunk") && classRank("trunk") > classRank("primary") &&
|
|
classRank("primary") > classRank("residential")) {
|
|
t.Errorf("classRank ordering wrong")
|
|
}
|
|
}
|
|
|
|
func TestClosestNodePairPrefersClass(t *testing.T) {
|
|
// motorway at 120m, service at 20m → motorway wins (within class window)
|
|
ways := []Way{
|
|
{
|
|
ID: 1, Highway: "service",
|
|
Nodes: []WayNode{{ID: 11, Lat: 45.0000, Lon: -73.0000}, {ID: 12, Lat: 45.0000, Lon: -72.9990}},
|
|
},
|
|
{
|
|
ID: 2, Highway: "motorway",
|
|
Nodes: []WayNode{{ID: 21, Lat: 45.0011, Lon: -73.0000}, {ID: 22, Lat: 45.0011, Lon: -72.9990}},
|
|
},
|
|
}
|
|
// point 20m from service line (lat 45.0000) and ~120m from motorway line (lat 45.0011)
|
|
way, a, b, _, ok := ClosestNodePair(ways, 45.0002, -73.0)
|
|
if !ok {
|
|
t.Fatal("no match")
|
|
}
|
|
if way.Highway != "motorway" || a != 21 || b != 22 {
|
|
t.Errorf("got %s (%d→%d); want motorway (21→22)", way.Highway, a, b)
|
|
}
|
|
}
|
|
|
|
func TestClosestNodePairNearestFallback(t *testing.T) {
|
|
// only a service road exists, 500m away → nearest still wins
|
|
ways := []Way{
|
|
{
|
|
ID: 1, Highway: "service",
|
|
Nodes: []WayNode{{ID: 11, Lat: 45.0050, Lon: -73.0000}, {ID: 12, Lat: 45.0050, Lon: -72.9900}},
|
|
},
|
|
}
|
|
way, _, _, d, ok := ClosestNodePair(ways, 45.0, -73.0)
|
|
if !ok || way.ID != 1 {
|
|
t.Fatalf("got ok=%v way=%+v", ok, way)
|
|
}
|
|
if d < 500 || d > 560 {
|
|
t.Errorf("dist = %.0f; want ~500", d)
|
|
}
|
|
}
|
|
|
|
func TestDecodeEvents(t *testing.T) {
|
|
// OData v3 envelope
|
|
body := []byte(`{"d":[{"EventType":"Road Closed","Description":"I-90 closed","City":"Albany","Location":{"Latitude":42.75,"Longitude":-73.93}},{"EventType":"Accident","lat":42.1,"lon":-73.5}]}`)
|
|
events, err := decodeEvents(body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(events) != 2 {
|
|
t.Fatalf("want 2 events, got %d", len(events))
|
|
}
|
|
e0 := events[0]
|
|
if e0.Type != "Road Closed" || !e0.Valid() || e0.Lat != 42.75 || e0.Lon != -73.93 {
|
|
t.Errorf("event0 = %+v", e0)
|
|
}
|
|
e1 := events[1]
|
|
if e1.Type != "Accident" || !e1.Valid() {
|
|
t.Errorf("event1 = %+v", e1)
|
|
}
|
|
}
|