package routing import ( "testing" ) // 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 directFound := false for _, r := range results { if r.TotalDuration == 3600 && r.TotalTransfers == 0 { directFound = true break } } if !directFound { t.Error("expected direct route (0 transfers, 3600s) in Pareto results") } // 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") } // 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) } } } }