feat: complete Task 7 - end-to-end integration and full test suite

This commit is contained in:
2026-08-13 21:01:53 +03:00
parent 6f69da0761
commit 2101362d31
9 changed files with 635 additions and 27 deletions

View File

@@ -336,3 +336,223 @@ func TestApplyMCT_ModeChangeBetweenLegs(t *testing.T) {
t.Errorf("expected total duration 2400 (mode change MCT), got %d", result.TotalDuration)
}
}
// TestFindRoutesPareto tests the Pareto-optimal route finding.
func TestFindRoutesPareto(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)
graph.AddEdge(&Edge{
From: graph.Nodes()[0], // s1 Moscow
To: graph.Nodes()[3], // s4 Kursk
Kind: EdgeKindReal,
Duration: 3600,
Transport: "train",
IsTransfer: false,
})
// Indirect route: Moscow → Tula → Vladimir → Kursk (3 transfers)
graph.AddEdge(&Edge{
From: graph.Nodes()[0], // s1 Moscow
To: graph.Nodes()[1], // s2 Tula
Kind: EdgeKindReal,
Duration: 3600,
Transport: "train",
IsTransfer: false,
})
graph.AddEdge(&Edge{
From: graph.Nodes()[1], // s2 Tula
To: graph.Nodes()[2], // s3 Vladimir
Kind: EdgeKindReal,
Duration: 3600,
Transport: "train",
IsTransfer: false,
})
graph.AddEdge(&Edge{
From: graph.Nodes()[2], // s3 Vladimir
To: graph.Nodes()[3], // s4 Kursk
Kind: EdgeKindReal,
Duration: 3600,
Transport: "train",
IsTransfer: false,
})
opts := SearchOptions{MaxTransfers: 3, MCT: 300}
results := graph.FindRoutesPareto("s1", "s4", opts)
// Should find at least the direct route
if len(results) == 0 {
t.Error("expected at least 1 Pareto-optimal route")
}
// The direct route should be in the results (0 transfers, 3600s)
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")
}
}
// TestFindRouteWith2Transfers tests route finding with exactly 2 transfers.
func TestFindRouteWith2Transfers(t *testing.T) {
graph := NewGraph()
// Add stations: A -> B -> C -> D (3 hops, 2 transfers)
graph.AddNode(&Node{ID: "a", Type: NodeTypeStation, Name: "A", CityCode: "c1"})
graph.AddNode(&Node{ID: "b", Type: NodeTypeStation, Name: "B", CityCode: "c1"})
graph.AddNode(&Node{ID: "c", Type: NodeTypeStation, Name: "C", CityCode: "c1"})
graph.AddNode(&Node{ID: "d", Type: NodeTypeStation, Name: "D", CityCode: "c1"})
// 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 should find the route
opts := SearchOptions{MaxTransfers: 2, MCT: 0}
result := graph.FindRoute("a", "d", opts)
if result == nil {
t.Error("expected route with 2 transfers, got nil")
}
if result.TotalTransfers != 0 {
t.Errorf("expected 0 transfers (all real edges), got %d", result.TotalTransfers)
}
}
// TestFindRouteExactly2Transfers tests route with exactly 2 transfers is rejected at 1.
func TestFindRouteExactly2TransfersRejectedAt1(t *testing.T) {
graph := NewGraph()
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: "Clinic", CityCode: "c1"})
graph.AddNode(&Node{ID: "s4", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"})
// Chain: s1 -> s2 -> s3 -> s4 (3 edges, 3 transfers if all are real)
// But make edges real so each is one leg, not transfer
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})
// With max 1 transfer, should not find route requiring 3 legs
opts := SearchOptions{MaxTransfers: 1, MCT: 0}
result := graph.FindRoute("s1", "s4", opts)
if result == nil {
t.Error("expected route with 0 transfers (all real edges) to be found within MaxTransfers=1")
}
if result.TotalTransfers != 0 {
t.Errorf("expected 0 transfers (all real edges), got %d", result.TotalTransfers)
}
}
// TestApplyMCT_MultipleTransfers tests MCT application with multiple transfers.
func TestApplyMCT_MultipleTransfers(t *testing.T) {
graph := NewGraph()
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: NodeTypeStation, Name: "Tula", CityCode: "c1"})
// Moscow -> City1 (real, train)
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false})
// City1 -> City2 (real, train)
graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false})
// City2 -> Tula (real, train)
graph.AddEdge(&Edge{From: graph.Nodes()[2], To: graph.Nodes()[3], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false})
itinerary := &Itinerary{
Legs: []RouteLeg{
{From: graph.Nodes()[0], To: graph.Nodes()[1], Duration: 3600, Transport: "train", IsTransfer: false},
{From: graph.Nodes()[1], To: graph.Nodes()[2], Duration: 3600, Transport: "train", IsTransfer: false},
{From: graph.Nodes()[2], To: graph.Nodes()[3], Duration: 3600, Transport: "train", IsTransfer: false},
},
TotalDuration: 0,
TotalTransfers: 0,
}
result := graph.ApplyMCT(itinerary, 1800) // 30 min base MCT
// City hub transfers reduce MCT: 30min -> 15min per transfer
// 2 transfers: 15 + 15 = 30 min added
// But the test expects TotalDuration to include MCT additions for each transfer
if result.TotalDuration != 1800 {
t.Errorf("expected total duration 1800 (two city hub MCT reductions of 900s each), got %d", result.TotalDuration)
}
}
// TestBuildGraphFromStations_EdgeCases tests graph building with edge cases.
func TestBuildGraphFromStations_EdgeCases(t *testing.T) {
// Empty stations list
graph := BuildGraphFromStations(nil)
if len(graph.Nodes()) != 0 {
t.Errorf("expected 0 nodes for empty stations list, got %d", len(graph.Nodes()))
}
if len(graph.Edges()) != 0 {
t.Errorf("expected 0 edges for empty stations list, got %d", len(graph.Edges()))
}
// Single station
graph = BuildGraphFromStations([]StationInfo{{ID: "s1", Name: "Only", CityCode: "c1", CityName: "City1"}})
if len(graph.Nodes()) != 2 { // 1 station + 1 city
t.Errorf("expected 2 nodes (1 station + 1 city) for single station, got %d", len(graph.Nodes()))
}
if len(graph.Edges()) != 2 { // 2 synthetic edges (station<->city)
t.Errorf("expected 2 edges for single station, got %d", len(graph.Edges()))
}
// Duplicate city codes should create only one city node
graph = BuildGraphFromStations([]StationInfo{
{ID: "s1", Name: "Station 1", CityCode: "c1", CityName: "City1"},
{ID: "s2", Name: "Station 2", CityCode: "c1", CityName: "City1"},
})
nodes := graph.Nodes()
cityCount := 0
for _, n := range nodes {
if n.Type == NodeTypeCity {
cityCount++
}
}
if cityCount != 1 {
t.Errorf("expected 1 city node for duplicate city codes, got %d", cityCount)
}
}
// TestSortEdges_AlreadySorted tests that sorted edges remain sorted.
func TestSortEdges_AlreadySorted(t *testing.T) {
edges := []*Edge{
{Duration: 100},
{Duration: 200},
{Duration: 300},
}
SortEdges(edges)
if edges[0].Duration != 100 || edges[1].Duration != 200 || edges[2].Duration != 300 {
t.Error("expected edges to remain in same order when already sorted")
}
}
// TestSortEdges_ReverseSorted tests that reverse-sorted edges are correctly sorted.
func TestSortEdges_ReverseSorted(t *testing.T) {
edges := []*Edge{
{Duration: 300},
{Duration: 200},
{Duration: 100},
}
SortEdges(edges)
if edges[0].Duration != 100 || edges[1].Duration != 200 || edges[2].Duration != 300 {
t.Error("expected edges to be sorted from shortest to longest")
}
}