142 lines
5.1 KiB
Go
142 lines
5.1 KiB
Go
package routing
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"doormile/internal/legs"
|
|
)
|
|
|
|
// Where a rider's leg actually ends.
|
|
//
|
|
// The bug these tests exist to prevent, found while testing the base-handover
|
|
// flow with a bulk sheet of intercity orders: the sequencer read
|
|
// pickupbookings.deliverylatitude for every active assignment, with no idea
|
|
// whether the parcel was hub-routed. For a Coimbatore → Chennai booking that
|
|
// told the route optimizer the rider was riding 430 km to the receiver, when
|
|
// the rider's real next stop is a base a few kilometres away.
|
|
//
|
|
// Wrong on its own — a 430 km "stop" is not a stop anyone makes — and worse in
|
|
// company: one such destination in a rider's set drags the ordering of every
|
|
// genuine local stop beside it, because the solver is optimising a journey
|
|
// nobody is going to make.
|
|
|
|
// Coimbatore pickup, and the local base a rider hands parcels to.
|
|
const (
|
|
pickPin = "641025"
|
|
pickLat, pickLng = 11.0168, 76.9558
|
|
baseLat, baseLng = 11.0272, 76.9905
|
|
localPin = "641004"
|
|
localLat, localLng = 11.029, 76.993
|
|
chennaiPin = "600001"
|
|
chennaiLat, chennaiLng = 13.091, 80.285
|
|
)
|
|
|
|
func TestDropForLeg(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
deliveryPin string
|
|
dLat, dLng float64
|
|
baseLat, baseLng float64
|
|
wantLat, wantLng float64
|
|
wantOK bool
|
|
why string
|
|
}{
|
|
{
|
|
name: "a hyperlocal parcel ends at the receiver",
|
|
deliveryPin: localPin, dLat: localLat, dLng: localLng,
|
|
baseLat: baseLat, baseLng: baseLng,
|
|
wantLat: localLat, wantLng: localLng, wantOK: true,
|
|
why: "same postal area — the collecting rider carries it all the way",
|
|
},
|
|
{
|
|
name: "an intercity parcel ends at the base, not in Chennai",
|
|
deliveryPin: chennaiPin, dLat: chennaiLat, dLng: chennaiLng,
|
|
baseLat: baseLat, baseLng: baseLng,
|
|
wantLat: baseLat, wantLng: baseLng, wantOK: true,
|
|
why: "this is the whole fix — the rider rides to the base",
|
|
},
|
|
{
|
|
name: "a nearby but different postal area still ends at the base",
|
|
deliveryPin: "642001", dLat: 10.658, dLng: 77.008,
|
|
baseLat: baseLat, baseLng: baseLng,
|
|
wantLat: baseLat, wantLng: baseLng, wantOK: true,
|
|
why: "Pollachi is 40km away; the prefix rule decides, not the distance",
|
|
},
|
|
{
|
|
name: "no pincode and a short hop still ends at the receiver",
|
|
deliveryPin: "", dLat: 11.05, dLng: 77.01,
|
|
baseLat: baseLat, baseLng: baseLng,
|
|
wantLat: 11.05, wantLng: 77.01, wantOK: true,
|
|
why: "the distance fallback puts it inside 30km",
|
|
},
|
|
{
|
|
name: "no pincode and a long haul ends at the base",
|
|
deliveryPin: "", dLat: chennaiLat, dLng: chennaiLng,
|
|
baseLat: baseLat, baseLng: baseLng,
|
|
wantLat: baseLat, wantLng: baseLng, wantOK: true,
|
|
why: "the distance fallback puts it far outside 30km",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
lat, lng, ok := dropForLeg(pickPin, tc.deliveryPin,
|
|
pickLat, pickLng, tc.dLat, tc.dLng, tc.baseLat, tc.baseLng)
|
|
if ok != tc.wantOK {
|
|
t.Fatalf("ok = %v, want %v — %s", ok, tc.wantOK, tc.why)
|
|
}
|
|
if lat != tc.wantLat || lng != tc.wantLng {
|
|
t.Errorf("drop = (%v, %v), want (%v, %v) — %s", lat, lng, tc.wantLat, tc.wantLng, tc.why)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDropForLegNeverSendsARiderInterstate(t *testing.T) {
|
|
// The regression guard, stated as the thing that actually matters rather
|
|
// than as a coordinate comparison: whatever the destination, the drop the
|
|
// sequencer is given must be somewhere a rider can plausibly ride to.
|
|
for _, d := range []struct {
|
|
name string
|
|
pin string
|
|
lat, lng float64
|
|
}{
|
|
{"Chennai", "600001", 13.091, 80.285},
|
|
{"Hyderabad", "500081", 17.44, 78.3489},
|
|
{"Bengaluru", "560066", 12.9698, 77.75},
|
|
{"Madurai", "625001", 9.9195, 78.119},
|
|
} {
|
|
t.Run(d.name, func(t *testing.T) {
|
|
lat, lng, ok := dropForLeg(pickPin, d.pin, pickLat, pickLng, d.lat, d.lng, baseLat, baseLng)
|
|
if !ok {
|
|
t.Fatal("a stop with a usable base must still be sequenced")
|
|
}
|
|
if lat == d.lat && lng == d.lng {
|
|
t.Fatalf("%s was passed to the optimizer as the rider's own drop", d.name)
|
|
}
|
|
if km := legs.HaversineKM(pickLat, pickLng, lat, lng); km > 50 {
|
|
t.Errorf("drop is %.0f km from the pickup — no rider is making that leg", km)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDropForLegSkipsAHubRoutedStopWithNoBase(t *testing.T) {
|
|
// Falling back to the receiver here would be the original bug wearing a
|
|
// different hat: better to leave one stop unsequenced than to skew the
|
|
// ordering of every other stop the rider is carrying.
|
|
_, _, ok := dropForLeg(pickPin, chennaiPin, pickLat, pickLng, chennaiLat, chennaiLng, 0, 0)
|
|
if ok {
|
|
t.Error("a hub-routed stop with no base coordinates must be left out, not pointed at the receiver")
|
|
}
|
|
}
|
|
|
|
func TestDropForLegKeepsAHyperlocalStopWithNoBase(t *testing.T) {
|
|
// A hyperlocal parcel never needed a base, so a missing one is irrelevant
|
|
// to it and must not cost the rider a stop.
|
|
lat, lng, ok := dropForLeg(pickPin, localPin, pickLat, pickLng, localLat, localLng, 0, 0)
|
|
if !ok || lat != localLat || lng != localLng {
|
|
t.Errorf("hyperlocal stop = (%v, %v, %v), want the receiver and ok", lat, lng, ok)
|
|
}
|
|
}
|