feat: implement lazy graph expansion FindRoute with on-demand /search and transfer depth limit

This commit is contained in:
2026-08-14 15:27:17 +03:00
parent bad78ea2fa
commit 0f95e3e2f8
3 changed files with 237 additions and 10 deletions

View File

@@ -751,6 +751,140 @@ func TestExpandGraphLazy_InvalidNodeType(t *testing.T) {
}
}
// TestFindRouteWithLazyExpansion_0Transfers tests route finding with 0 transfers using lazy expansion.
func TestFindRouteWithLazyExpansion_0Transfers(t *testing.T) {
graph := NewGraphWithoutYandex()
// Add stations: A -> B direct route (0 transfers)
graph.AddNode(&Node{ID: "a", Type: NodeTypeStation, Name: "A", CityCode: "c1"})
graph.AddNode(&Node{ID: "b", Type: NodeTypeStation, Name: "B", CityCode: "c1"})
// Add real direct edge
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false})
// Search with max 0 transfers and lazy expansion enabled
opts := SearchOptions{MaxTransfers: 0, MCT: 0, DestCityCode: "c1", Date: "2026-08-20"}
result := graph.FindRoute("a", "b", opts)
if result == nil {
t.Error("expected route with 0 transfers using lazy expansion")
}
if result.TotalTransfers != 0 {
t.Errorf("expected 0 transfers, got %d", result.TotalTransfers)
}
}
// TestFindRouteWithLazyExpansion_1Transfer tests route finding with 1 transfer using lazy expansion.
func TestFindRouteWithLazyExpansion_1Transfer(t *testing.T) {
graph := NewGraphWithoutYandex()
// Add stations: A -> C -> B (1 transfer via city hub)
graph.AddNode(&Node{ID: "a", Type: NodeTypeStation, Name: "A", CityCode: "c1"})
graph.AddNode(&Node{ID: "c", Type: NodeTypeCity, Name: "CityHub", CityCode: "c1"})
graph.AddNode(&Node{ID: "b", Type: NodeTypeStation, Name: "B", CityCode: "c1"})
// Add real edges: A -> CityHub and CityHub -> B
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false})
graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false})
// Search with max 1 transfer and lazy expansion enabled
opts := SearchOptions{MaxTransfers: 1, MCT: 0, DestCityCode: "c1", Date: "2026-08-20"}
result := graph.FindRoute("a", "b", opts)
if result == nil {
t.Error("expected route with 1 transfer using lazy expansion")
}
if result.TotalTransfers > 1 {
t.Errorf("expected at most 1 transfer, got %d", result.TotalTransfers)
}
}
// TestFindRouteWithLazyExpansion_2Transfers tests route finding with 2 transfers using lazy expansion.
func TestFindRouteWithLazyExpansion_2Transfers(t *testing.T) {
graph := NewGraphWithoutYandex()
// Add stations: A -> D -> E -> B (2 transfers)
graph.AddNode(&Node{ID: "a", Type: NodeTypeStation, Name: "A", CityCode: "c1"})
graph.AddNode(&Node{ID: "d", Type: NodeTypeStation, Name: "D", CityCode: "c1"})
graph.AddNode(&Node{ID: "e", Type: NodeTypeStation, Name: "E", CityCode: "c1"})
graph.AddNode(&Node{ID: "b", Type: NodeTypeStation, Name: "B", CityCode: "c1"})
// Add real edges between consecutive stations
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 300, Transport: "train", IsTransfer: false})
graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindReal, Duration: 300, Transport: "train", IsTransfer: false})
graph.AddEdge(&Edge{From: graph.Nodes()[2], To: graph.Nodes()[3], Kind: EdgeKindReal, Duration: 300, Transport: "train", IsTransfer: false})
// Search with max 2 transfers and lazy expansion enabled
opts := SearchOptions{MaxTransfers: 2, MCT: 0, DestCityCode: "c1", Date: "2026-08-20"}
result := graph.FindRoute("a", "b", opts)
if result == nil {
t.Error("expected route with 2 transfers using lazy expansion")
}
if result.TotalTransfers != 0 {
t.Errorf("expected 0 transfers (all real edges), got %d", result.TotalTransfers)
}
}
// TestFindRoute_LazyExpansion_TransferLimitEnforcement tests that transfer limit is enforced during lazy expansion.
func TestFindRoute_LazyExpansion_TransferLimitEnforcement(t *testing.T) {
graph := NewGraphWithoutYandex()
// Add a chain of stations that would require 4 transfers (exceeds limit of 3)
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
graph.AddNode(&Node{ID: "s2", Type: NodeTypeCity, Name: "City1", CityCode: "c1"})
graph.AddNode(&Node{ID: "s3", Type: NodeTypeCity, Name: "City2", CityCode: "c1"})
graph.AddNode(&Node{ID: "s4", Type: NodeTypeCity, Name: "City3", CityCode: "c1"})
graph.AddNode(&Node{ID: "s5", Type: NodeTypeCity, Name: "City4", CityCode: "c1"})
graph.AddNode(&Node{ID: "s6", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"})
// Add synthetic transfer edges between consecutive nodes (IsTransfer: true)
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindSynthetic, Duration: 300, Transport: "train", IsTransfer: true})
graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindSynthetic, Duration: 300, Transport: "train", IsTransfer: true})
graph.AddEdge(&Edge{From: graph.Nodes()[2], To: graph.Nodes()[3], Kind: EdgeKindSynthetic, Duration: 300, Transport: "train", IsTransfer: true})
graph.AddEdge(&Edge{From: graph.Nodes()[3], To: graph.Nodes()[4], Kind: EdgeKindSynthetic, Duration: 300, Transport: "train", IsTransfer: true})
graph.AddEdge(&Edge{From: graph.Nodes()[4], To: graph.Nodes()[5], Kind: EdgeKindSynthetic, Duration: 300, Transport: "train", IsTransfer: true})
// Search with max 3 transfers - should not find route requiring 5 transfers
opts := SearchOptions{MaxTransfers: 3, MCT: 0, DestCityCode: "c1", Date: "2026-08-20"}
result := graph.FindRoute("s1", "s6", opts)
if result != nil {
t.Error("expected nil route when transfers exceed limit during lazy expansion")
}
}
// TestFindRouteWithLazyExpansion_MCTCalculation tests MCT calculation during lazy expansion.
func TestFindRouteWithLazyExpansion_MCTCalculation(t *testing.T) {
graph := NewGraphWithoutYandex()
// Create legs with city hub transfers
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
graph.AddNode(&Node{ID: "s2", Type: NodeTypeCity, Name: "City Hub", CityCode: "c1"})
graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
// Add real edges
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false})
graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false})
// Search with MCT and lazy expansion
opts := SearchOptions{MaxTransfers: 2, MCT: 300, DestCityCode: "c1", Date: "2026-08-20"}
result := graph.FindRoute("s1", "s3", opts)
if result == nil {
t.Error("expected route with MCT calculation")
}
// MCT of 300s (5 min) is applied at each transfer point during BFS
// With 1 transfer (s1 -> city_hub -> s3), total duration includes MCT addition
if result.TotalDuration < 3600 {
t.Errorf("expected total duration at least 3600 (one real edge + MCT), got %d", result.TotalDuration)
}
// Should have exactly 1 transfer (city hub transfer, not counted as extra since edges are real)
if result.TotalTransfers > 1 {
t.Errorf("expected at most 1 transfer, got %d", result.TotalTransfers)
}
}
// TestBuildGraphFromHubs_EdgeCases tests edge cases for hub graph building.
func TestBuildGraphFromHubs_EdgeCases(t *testing.T) {
// Empty stations list