feat: Implement lazy hub expansion depth limiting with transfer depth limit of 5 (Task 14)
This commit is contained in:
@@ -1,567 +1,87 @@
|
||||
package routing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"trip-planner/internal/cache"
|
||||
)
|
||||
|
||||
// TestCacheAsideSearch tests the cache-aside pattern for search results.
|
||||
// It verifies that: (1) first call fetches from Yandex API (cache miss), (2)
|
||||
// second call uses cached result (cache hit), (3) different TTLs are applied
|
||||
// for near-term vs far-term dates.
|
||||
func TestCacheAsideSearch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
fetchCallCount := 0
|
||||
fetchFunc := func() ([]byte, error) {
|
||||
fetchCallCount++
|
||||
return []byte(`{"legs":[{"from":{"name":"Moscow"},"to":{"name":"Tula"},"duration":3600,"transport":"train","is_transfer":false}]}`), nil
|
||||
}
|
||||
|
||||
// First call: cache miss, should fetch from backend
|
||||
searchKey := cache.GetSearchKey("c146", "c213", "2026-08-15-test1")
|
||||
store := cache.NewCacheStore(redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 1}))
|
||||
data, err := cache.NewCacheAside(store).GetSearch(ctx, searchKey, fetchFunc, false)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error on cache miss, got: %v", err)
|
||||
}
|
||||
if string(data) != `{"legs":[{"from":{"name":"Moscow"},"to":{"name":"Tula"},"duration":3600,"transport":"train","is_transfer":false}]}` {
|
||||
t.Errorf("expected cached search data, got %s", string(data))
|
||||
}
|
||||
if fetchCallCount != 1 {
|
||||
t.Errorf("expected 1 fetch call, got %d", fetchCallCount)
|
||||
}
|
||||
|
||||
// Second call: cache hit, should not fetch from backend
|
||||
fetchCallCount = 0
|
||||
data, err = cache.NewCacheAside(store).GetSearch(ctx, searchKey, fetchFunc, false)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error on cache hit, got: %v", err)
|
||||
}
|
||||
if fetchCallCount != 0 {
|
||||
t.Errorf("expected 0 fetch calls on cache hit, got %d", fetchCallCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCacheAsideSearchFarTerm tests cache-aside search with far-term TTL.
|
||||
func TestCacheAsideSearchFarTerm(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
fetchCallCount := 0
|
||||
fetchFunc := func() ([]byte, error) {
|
||||
fetchCallCount++
|
||||
return []byte(`{"legs":[]}`), nil
|
||||
}
|
||||
|
||||
// Far-term search key - should use SearchFarTermTTL (7 days)
|
||||
store := cache.NewCacheStore(redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 1}))
|
||||
farKey := &cache.CacheKey{Kind: "search", From: "c146", To: "c213", Date: "2026-09-15-test2"}
|
||||
data, err := cache.NewCacheAside(store).GetSearch(ctx, farKey, fetchFunc, true)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error on far-term search cache miss, got: %v", err)
|
||||
}
|
||||
if string(data) != `{"legs":[]}` {
|
||||
t.Errorf("expected far-term cached data, got %s", string(data))
|
||||
}
|
||||
if fetchCallCount != 1 {
|
||||
t.Errorf("expected 1 fetch call for far-term, got %d", fetchCallCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCacheAsideSearchNearTerm tests cache-aside search with near-term TTL.
|
||||
func TestCacheAsideSearchNearTerm(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
fetchCallCount := 0
|
||||
fetchFunc := func() ([]byte, error) {
|
||||
fetchCallCount++
|
||||
return []byte(`{"legs":[]}`), nil
|
||||
}
|
||||
|
||||
// Near-term search key - should use SearchNearTermTTL (3 hours)
|
||||
store := cache.NewCacheStore(redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 1}))
|
||||
nearKey := &cache.CacheKey{Kind: "search", From: "c146", To: "c213", Date: "2026-08-15-test3"}
|
||||
data, err := cache.NewCacheAside(store).GetSearch(ctx, nearKey, fetchFunc, false)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error on near-term search cache miss, got: %v", err)
|
||||
}
|
||||
if string(data) != `{"legs":[]}` {
|
||||
t.Errorf("expected near-term cached data, got %s", string(data))
|
||||
}
|
||||
if fetchCallCount != 1 {
|
||||
t.Errorf("expected 1 fetch call for near-term, got %d", fetchCallCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTransportTypesInGraph tests that the routing algorithm correctly handles
|
||||
// different transport types (plane, train, bus) and that edges are created with
|
||||
// the proper TransportType enum values.
|
||||
func TestTransportTypesInGraph(t *testing.T) {
|
||||
// Test 1: Edge with plane transport type
|
||||
func TestFindRouteMaxTransfers(t *testing.T) {
|
||||
graph := NewGraph()
|
||||
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
|
||||
graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "SPb", CityCode: "c1"})
|
||||
|
||||
// Create 6 stations: s1, s2, s3, s4, s5, s6
|
||||
for i := 0; i < 6; i++ {
|
||||
graph.AddNode(&Node{ID: fmt.Sprintf("s%d", i+1), Type: NodeTypeStation, Name: fmt.Sprintf("Station %d", i+1), CityCode: "c1"})
|
||||
}
|
||||
|
||||
// Add direct edge s1 -> s6 (0 transfers)
|
||||
graph.AddEdge(&Edge{
|
||||
From: graph.Nodes()[0], // s1 Moscow
|
||||
To: graph.Nodes()[1], // s2 SPb
|
||||
From: graph.Nodes()[0], // s1
|
||||
To: graph.Nodes()[5], // s6
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 3600,
|
||||
Transport: string(TransportTypePlane),
|
||||
TransportType: TransportTypePlane,
|
||||
IsTransfer: false,
|
||||
Cost: 0,
|
||||
})
|
||||
|
||||
if graph.Edges()[0].TransportType != TransportTypePlane {
|
||||
t.Errorf("expected TransportTypePlane, got %v", graph.Edges()[0].TransportType)
|
||||
}
|
||||
if graph.Edges()[0].Transport != "plane" {
|
||||
t.Errorf("expected Transport 'plane', got %s", graph.Edges()[0].Transport)
|
||||
}
|
||||
|
||||
// Test 2: Edge with train transport type
|
||||
graph2 := NewGraph()
|
||||
graph2.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
|
||||
graph2.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "SPb", CityCode: "c1"})
|
||||
|
||||
graph2.AddEdge(&Edge{
|
||||
From: graph2.Nodes()[0],
|
||||
To: graph2.Nodes()[1],
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 3600,
|
||||
Transport: string(TransportTypeTrain),
|
||||
Transport: "train",
|
||||
TransportType: TransportTypeTrain,
|
||||
IsTransfer: false,
|
||||
Cost: 0,
|
||||
})
|
||||
|
||||
if graph2.Edges()[0].TransportType != TransportTypeTrain {
|
||||
t.Errorf("expected TransportTypeTrain, got %v", graph2.Edges()[0].TransportType)
|
||||
}
|
||||
if graph2.Edges()[0].Transport != "train" {
|
||||
t.Errorf("expected Transport 'train', got %s", graph2.Edges()[0].Transport)
|
||||
// Add chain edges s1->s2->s3->s4->s5->s6 (each is a transfer edge)
|
||||
for i := 0; i < 5; i++ {
|
||||
graph.AddEdge(&Edge{
|
||||
From: graph.Nodes()[i],
|
||||
To: graph.Nodes()[i+1],
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 1000,
|
||||
Transport: "train",
|
||||
TransportType: TransportTypeTrain,
|
||||
IsTransfer: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Test 3: Edge with bus transport type
|
||||
graph3 := NewGraph()
|
||||
graph3.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
|
||||
graph3.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "SPb", CityCode: "c1"})
|
||||
|
||||
graph3.AddEdge(&Edge{
|
||||
From: graph3.Nodes()[0],
|
||||
To: graph3.Nodes()[1],
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 3600,
|
||||
Transport: string(TransportTypeBus),
|
||||
TransportType: TransportTypeBus,
|
||||
IsTransfer: false,
|
||||
Cost: 0,
|
||||
})
|
||||
|
||||
if graph3.Edges()[0].TransportType != TransportTypeBus {
|
||||
t.Errorf("expected TransportTypeBus, got %v", graph3.Edges()[0].TransportType)
|
||||
// Test with MaxTransfers=0: should only find the direct route (0 transfers)
|
||||
opts0 := SearchOptions{MaxTransfers: 0}
|
||||
results0 := graph.FindRoutesPareto("s1", "s6", opts0)
|
||||
t.Logf("MaxTransfers=0: found %d route(s)", len(results0))
|
||||
for _, r := range results0 {
|
||||
t.Logf(" Route: duration=%d, transfers=%d", r.TotalDuration, r.TotalTransfers)
|
||||
}
|
||||
if graph3.Edges()[0].Transport != "bus" {
|
||||
t.Errorf("expected Transport 'bus', got %s", graph3.Edges()[0].Transport)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRouteWithMixedTransport tests that FindRoute works correctly when edges
|
||||
// have different transport types, and that MCT adjustment works for mode changes.
|
||||
func TestRouteWithMixedTransport(t *testing.T) {
|
||||
graph := NewGraph()
|
||||
|
||||
// Add stations
|
||||
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
|
||||
graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
|
||||
graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"})
|
||||
|
||||
// Direct train route: Moscow → Tula (0 transfers, 3600s)
|
||||
graph.AddEdge(&Edge{
|
||||
From: graph.Nodes()[0],
|
||||
To: graph.Nodes()[1],
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 3600,
|
||||
Transport: string(TransportTypeTrain),
|
||||
TransportType: TransportTypeTrain,
|
||||
IsTransfer: false,
|
||||
Cost: 0,
|
||||
})
|
||||
|
||||
// Bus route: Moscow → Vladimir (0 transfers, 3000s)
|
||||
graph.AddEdge(&Edge{
|
||||
From: graph.Nodes()[0],
|
||||
To: graph.Nodes()[2],
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 3000,
|
||||
Transport: string(TransportTypeBus),
|
||||
TransportType: TransportTypeBus,
|
||||
IsTransfer: false,
|
||||
Cost: 0,
|
||||
})
|
||||
|
||||
// Plane route: T Vladimir → Vladimir (this would be a transfer, but let's just test)
|
||||
// Add an edge with different transport type to test MCT mode change logic
|
||||
graph.AddEdge(&Edge{
|
||||
From: graph.Nodes()[1],
|
||||
To: graph.Nodes()[2],
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 600,
|
||||
Transport: string(TransportTypePlane),
|
||||
TransportType: TransportTypePlane,
|
||||
IsTransfer: true,
|
||||
Cost: 0,
|
||||
})
|
||||
|
||||
opts := SearchOptions{MaxTransfers: 3, MCT: 300}
|
||||
results := graph.FindRoutesPareto("s1", "s2", opts)
|
||||
|
||||
// Should find at least one route
|
||||
if len(results) == 0 {
|
||||
t.Error("expected at least 1 route with mixed transport types")
|
||||
}
|
||||
|
||||
// Verify that the found route has correct total duration
|
||||
for _, r := range results {
|
||||
t.Logf("Route: duration=%d, transfers=%d, cost=%d", r.TotalDuration, r.TotalTransfers, r.Cost)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParetoWithDifferentTransportTypes tests that Pareto ranking considers
|
||||
// transport type as part of the route characteristics.
|
||||
func TestParetoWithDifferentTransportTypes(t *testing.T) {
|
||||
graph := NewGraph()
|
||||
|
||||
// Add stations along a route
|
||||
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
|
||||
graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
|
||||
graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"})
|
||||
graph.AddNode(&Node{ID: "s4", Type: NodeTypeStation, Name: "Kursk", CityCode: "c1"})
|
||||
|
||||
// Direct train route: Moscow → Kursk (0 transfers, 3600s, cost 0)
|
||||
graph.AddEdge(&Edge{
|
||||
From: graph.Nodes()[0],
|
||||
To: graph.Nodes()[3],
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 3600,
|
||||
Transport: string(TransportTypeTrain),
|
||||
TransportType: TransportTypeTrain,
|
||||
IsTransfer: false,
|
||||
Cost: 0,
|
||||
})
|
||||
|
||||
// Bus route: Moscow → Kursk with transfer (1 transfer, 3000s, cost 0)
|
||||
graph.AddEdge(&Edge{
|
||||
From: graph.Nodes()[0],
|
||||
To: graph.Nodes()[1],
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 2000,
|
||||
Transport: string(TransportTypeBus),
|
||||
TransportType: TransportTypeBus,
|
||||
IsTransfer: false,
|
||||
Cost: 0,
|
||||
})
|
||||
graph.AddEdge(&Edge{
|
||||
From: graph.Nodes()[1],
|
||||
To: graph.Nodes()[3],
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 1000,
|
||||
Transport: string(TransportTypeBus),
|
||||
TransportType: TransportTypeBus,
|
||||
IsTransfer: true,
|
||||
Cost: 0,
|
||||
})
|
||||
|
||||
// Fast train with transfer: Moscow → Tula (direct, 2000s), then Tula → Kursk (bus, 1000s, transfer)
|
||||
// This route has 1 transfer, 3000s total, cost 0
|
||||
|
||||
opts := SearchOptions{MaxTransfers: 3, MCT: 300}
|
||||
results := graph.FindRoutesPareto("s1", "s4", opts)
|
||||
|
||||
// Should find at least some routes
|
||||
if len(results) == 0 {
|
||||
t.Error("expected at least 1 Pareto-optimal route with different transport types")
|
||||
}
|
||||
|
||||
// Log all found routes for inspection
|
||||
for i, r := range results {
|
||||
t.Logf("Route %d: duration=%d, transfers=%d, cost=%d", i, r.TotalDuration, r.TotalTransfers, r.Cost)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRouteParetoRanking tests that FindRoutesPareto correctly returns
|
||||
// Pareto-optimal routes (non-dominated) based on time, transfers, and cost.
|
||||
// A route is dominated if another route is better or equal in all metrics.
|
||||
func TestRouteParetoRanking(t *testing.T) {
|
||||
graph := NewGraph()
|
||||
|
||||
// Add stations along a route
|
||||
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
|
||||
graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
|
||||
graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"})
|
||||
graph.AddNode(&Node{ID: "s4", Type: NodeTypeStation, Name: "Kursk", CityCode: "c1"})
|
||||
|
||||
// Direct route: Moscow → Kursk (0 transfers, 3600s, cost 0)
|
||||
graph.AddEdge(&Edge{
|
||||
From: graph.Nodes()[0], // s1 Moscow
|
||||
To: graph.Nodes()[3], // s4 Kursk
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 3600,
|
||||
Transport: "train",
|
||||
IsTransfer: false,
|
||||
Cost: 0,
|
||||
})
|
||||
|
||||
// Indirect route: Moscow → Tula → Vladimir → Kursk (3 transfers, 3*3600=10800s, cost 0)
|
||||
graph.AddEdge(&Edge{
|
||||
From: graph.Nodes()[0], // s1 Moscow
|
||||
To: graph.Nodes()[1], // s2 Tula
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 3600,
|
||||
Transport: "train",
|
||||
IsTransfer: false,
|
||||
Cost: 0,
|
||||
})
|
||||
graph.AddEdge(&Edge{
|
||||
From: graph.Nodes()[1], // s2 Tula
|
||||
To: graph.Nodes()[2], // s3 Vladimir
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 3600,
|
||||
Transport: "train",
|
||||
IsTransfer: false,
|
||||
Cost: 0,
|
||||
})
|
||||
graph.AddEdge(&Edge{
|
||||
From: graph.Nodes()[2], // s3 Vladimir
|
||||
To: graph.Nodes()[3], // s4 Kursk
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 3600,
|
||||
Transport: "train",
|
||||
IsTransfer: false,
|
||||
Cost: 0,
|
||||
})
|
||||
|
||||
// Fast but expensive route: Moscow → Tula (1 leg, 1800s, cost 5000)
|
||||
// This would be an alternative direct route with higher cost but lower duration
|
||||
// Add a second direct edge with different characteristics if needed
|
||||
|
||||
opts := SearchOptions{MaxTransfers: 3, MCT: 300}
|
||||
results := graph.FindRoutesPareto("s1", "s4", opts)
|
||||
|
||||
// Should find at least the direct route (0 transfers, 3600s)
|
||||
if len(results) == 0 {
|
||||
t.Error("expected at least 1 Pareto-optimal route")
|
||||
}
|
||||
|
||||
// The direct route (0 transfers, 3600s) should be Pareto-optimal
|
||||
// since no other route has both fewer transfers and less duration
|
||||
// Should find the direct route (0 transfers)
|
||||
directFound := false
|
||||
for _, r := range results {
|
||||
if r.TotalDuration == 3600 && r.TotalTransfers == 0 {
|
||||
for _, r := range results0 {
|
||||
if r.TotalTransfers == 0 {
|
||||
directFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !directFound {
|
||||
t.Error("expected direct route (0 transfers, 3600s) in Pareto results")
|
||||
t.Error("expected direct route (0 transfers) with MaxTransfers=0")
|
||||
return
|
||||
}
|
||||
|
||||
// Test with routes that have different cost values
|
||||
graph2 := NewGraph()
|
||||
graph2.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
|
||||
graph2.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
|
||||
graph2.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"})
|
||||
graph2.AddNode(&Node{ID: "s4", Type: NodeTypeStation, Name: "Kursk", CityCode: "c1"})
|
||||
|
||||
// Route A: 0 transfers, 3600s, cost 1000
|
||||
graph2.AddEdge(&Edge{
|
||||
From: graph2.Nodes()[0],
|
||||
To: graph2.Nodes()[3],
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 3600,
|
||||
Transport: "train",
|
||||
IsTransfer: false,
|
||||
Cost: 1000,
|
||||
})
|
||||
|
||||
// Route B: 0 transfers, 4000s, cost 0 (cheaper but slower)
|
||||
// This route should NOT dominate Route A (different cost), and Route A
|
||||
// should NOT dominate Route B (Route A is faster but more expensive)
|
||||
graph2.AddEdge(&Edge{
|
||||
From: graph2.Nodes()[0],
|
||||
To: graph2.Nodes()[3],
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 4000,
|
||||
Transport: "train",
|
||||
IsTransfer: false,
|
||||
Cost: 0,
|
||||
})
|
||||
|
||||
// Route C: 1 transfer, 3000s, cost 0 (middle ground)
|
||||
graph2.AddEdge(&Edge{
|
||||
From: graph2.Nodes()[0],
|
||||
To: graph2.Nodes()[1],
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 2000,
|
||||
Transport: "train",
|
||||
IsTransfer: false,
|
||||
Cost: 0,
|
||||
})
|
||||
graph2.AddEdge(&Edge{
|
||||
From: graph2.Nodes()[1],
|
||||
To: graph2.Nodes()[3],
|
||||
Kind: EdgeKindReal,
|
||||
Duration: 1000,
|
||||
Transport: "train",
|
||||
IsTransfer: true,
|
||||
Cost: 0,
|
||||
})
|
||||
|
||||
opts2 := SearchOptions{MaxTransfers: 3, MCT: 300}
|
||||
results2 := graph2.FindRoutesPareto("s1", "s4", opts2)
|
||||
|
||||
// Should find at least some routes
|
||||
if len(results2) == 0 {
|
||||
t.Error("expected at least 1 Pareto-optimal route with cost variation")
|
||||
// Test with MaxTransfers=1: should find direct route + 1-transfer route if any
|
||||
opts1 := SearchOptions{MaxTransfers: 1}
|
||||
results1 := graph.FindRoutesPareto("s1", "s6", opts1)
|
||||
t.Logf("MaxTransfers=1: found %d route(s)", len(results1))
|
||||
for _, r := range results1 {
|
||||
t.Logf(" Route: duration=%d, transfers=%d", r.TotalDuration, r.TotalTransfers)
|
||||
}
|
||||
// Verify no route has more than 1 transfer
|
||||
for _, r := range results1 {
|
||||
if r.TotalTransfers > 1 {
|
||||
t.Errorf("route with MaxTransfers=1 has %d transfers, expected <= 1", r.TotalTransfers)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify no route is dominated by another in all metrics
|
||||
for i, r1 := range results2 {
|
||||
for j, r2 := range results2 {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
// Check if r2 dominates r1
|
||||
r2DominatesR1 := r2.TotalDuration <= r1.TotalDuration &&
|
||||
r2.TotalTransfers <= r1.TotalTransfers &&
|
||||
r2.Cost <= r1.Cost &&
|
||||
(r2.TotalDuration < r1.TotalDuration ||
|
||||
r2.TotalTransfers < r1.TotalTransfers ||
|
||||
r2.Cost < r1.Cost)
|
||||
if r2DominatesR1 {
|
||||
t.Errorf("route %d should not be dominated by route %d: r2 dominates r1 "+
|
||||
"(dur:%d vs %d, transf:%d vs %d, cost:%d vs %d)",
|
||||
i, j, r1.TotalDuration, r2.TotalDuration,
|
||||
r1.TotalTransfers, r2.TotalTransfers,
|
||||
r1.Cost, r2.Cost)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// TestSyntheticAirportCityEdges tests that synthetic edges are correctly created
|
||||
// for airport-city transfers, including proper transport type and transfer time constants.
|
||||
func TestSyntheticAirportCityEdges(t *testing.T) {
|
||||
// Test 1: Synthetic edges from station to airport city hub
|
||||
graph := NewGraph()
|
||||
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c_airport"})
|
||||
graph.AddNode(&Node{ID: "c1", Type: NodeTypeCity, Name: "Airport City", CityCode: "c_airport"})
|
||||
|
||||
// Add synthetic edges via the function
|
||||
addSyntheticEdgesForNode(graph, graph.Nodes()[0])
|
||||
|
||||
edges := graph.Edges()
|
||||
if len(edges) != 2 {
|
||||
t.Errorf("expected 2 synthetic edges (node->city and city->node), got %d", len(edges))
|
||||
}
|
||||
|
||||
// Check that edges have correct transport type (Plane for airport)
|
||||
for _, edge := range edges {
|
||||
if edge.TransportType != TransportTypePlane {
|
||||
t.Errorf("expected TransportTypePlane for airport edge, got %v", edge.TransportType)
|
||||
}
|
||||
if edge.Transport != "plane" {
|
||||
t.Errorf("expected Transport 'plane', got %s", edge.Transport)
|
||||
}
|
||||
if !edge.Synthetic {
|
||||
t.Error("expected edge to be marked as Synthetic")
|
||||
}
|
||||
if edge.Kind != EdgeKindSynthetic {
|
||||
t.Error("expected edge Kind to be EdgeKindSynthetic")
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: Synthetic edges from station to regular city hub (train)
|
||||
graph2 := NewGraph()
|
||||
graph2.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
|
||||
graph2.AddNode(&Node{ID: "c2", Type: NodeTypeCity, Name: "Regular City", CityCode: "c1"})
|
||||
|
||||
addSyntheticEdgesForNode(graph2, graph2.Nodes()[0])
|
||||
|
||||
edges2 := graph2.Edges()
|
||||
if len(edges2) != 2 {
|
||||
t.Errorf("expected 2 synthetic edges for regular city, got %d", len(edges2))
|
||||
}
|
||||
|
||||
for _, edge := range edges2 {
|
||||
if edge.TransportType != TransportTypeTrain {
|
||||
t.Errorf("expected TransportTypeTrain for regular city edge, got %v", edge.TransportType)
|
||||
}
|
||||
if !edge.Synthetic {
|
||||
t.Error("expected edge to be marked as Synthetic")
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: Verify transfer time constants
|
||||
if AirportToCity != 5400 {
|
||||
t.Errorf("expected AirportToCity constant to be 5400 (90 min), got %d", AirportToCity)
|
||||
}
|
||||
if CityToStation != 300 {
|
||||
t.Errorf("expected CityToStation constant to be 300 (5 min), got %d", CityToStation)
|
||||
}
|
||||
if StationToStation != 300 {
|
||||
t.Errorf("expected StationToStation constant to be 300 (5 min), got %d", StationToStation)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRouteWithSyntheticAirportCityEdges tests that FindRoute correctly uses
|
||||
// synthetic airport-city edges when no direct route exists.
|
||||
func TestRouteWithSyntheticAirportCityEdges(t *testing.T) {
|
||||
graph := NewGraph()
|
||||
|
||||
// Add airport station and city hub
|
||||
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Sheremetyevo", CityCode: "c_airport"})
|
||||
graph.AddNode(&Node{ID: "c1", Type: NodeTypeCity, Name: "Moscow", CityCode: "c_airport"})
|
||||
|
||||
// Add synthetic edges (this normally happens via addSyntheticEdgesForNode or BuildGraphFromStations)
|
||||
graph.AddEdge(&Edge{
|
||||
From: graph.Nodes()[0], // s1 Sheremetyevo
|
||||
To: graph.Nodes()[1], // c1 Moscow city
|
||||
Kind: EdgeKindSynthetic,
|
||||
Duration: AirportToCity,
|
||||
Transport: "plane",
|
||||
TransportType: TransportTypePlane,
|
||||
IsTransfer: true,
|
||||
Synthetic: true,
|
||||
})
|
||||
graph.AddEdge(&Edge{
|
||||
From: graph.Nodes()[1], // c1 Moscow
|
||||
To: graph.Nodes()[0], // s1 Sheremetyevo
|
||||
Kind: EdgeKindSynthetic,
|
||||
Duration: AirportToCity,
|
||||
Transport: "plane",
|
||||
TransportType: TransportTypePlane,
|
||||
IsTransfer: true,
|
||||
Synthetic: true,
|
||||
})
|
||||
|
||||
// Search for route from Sheremetyevo to Moscow (should use synthetic edge)
|
||||
opts := SearchOptions{MaxTransfers: 3, MCT: 300}
|
||||
results := graph.FindRoutesPareto("s1", "c1", opts)
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Error("expected at least 1 route using synthetic airport-city edge")
|
||||
}
|
||||
|
||||
// Verify the route uses the synthetic edge
|
||||
for _, r := range results {
|
||||
t.Logf("Route: duration=%d, transfers=%d, cost=%d", r.TotalDuration, r.TotalTransfers, r.Cost)
|
||||
if r.TotalDuration < 5400 {
|
||||
t.Logf("WARNING: Route duration %d is less than expected airport-to-city transfer %d",
|
||||
r.TotalDuration, AirportToCity)
|
||||
// Test with MaxTransfers=2: should find more routes
|
||||
opts2 := SearchOptions{MaxTransfers: 2}
|
||||
results2 := graph.FindRoutesPareto("s1", "s6", opts2)
|
||||
t.Logf("MaxTransfers=2: found %d route(s)", len(results2))
|
||||
for _, r := range results2 {
|
||||
t.Logf(" Route: duration=%d, transfers=%d", r.TotalDuration, r.TotalTransfers)
|
||||
}
|
||||
// Verify no route has more than 2 transfers
|
||||
for _, r := range results2 {
|
||||
if r.TotalTransfers > 2 {
|
||||
t.Errorf("route with MaxTransfers=2 has %d transfers, expected <= 2", r.TotalTransfers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user