787 lines
27 KiB
Go
787 lines
27 KiB
Go
package routing
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestGraphNodeCreation(t *testing.T) {
|
|
// Test Node creation with Station type
|
|
station := &Node{
|
|
ID: "s9600213",
|
|
Type: NodeTypeStation,
|
|
Name: "Шереметьево",
|
|
}
|
|
|
|
if station.ID != "s9600213" {
|
|
t.Errorf("expected node ID s9600213, got %s", station.ID)
|
|
}
|
|
if station.Type != NodeTypeStation {
|
|
t.Errorf("expected NodeTypeStation, got %v", station.Type)
|
|
}
|
|
if station.Name != "Шереметьево" {
|
|
t.Errorf("expected name Шереметьево, got %s", station.Name)
|
|
}
|
|
|
|
// Test Node creation with City type
|
|
city := &Node{
|
|
ID: "city:c146",
|
|
Type: NodeTypeCity,
|
|
Name: "Simferopol",
|
|
}
|
|
|
|
if city.ID != "city:c146" {
|
|
t.Errorf("expected node ID city:c146, got %s", city.ID)
|
|
}
|
|
if city.Type != NodeTypeCity {
|
|
t.Errorf("expected NodeTypeCity, got %v", city.Type)
|
|
}
|
|
if city.Name != "Simferopol" {
|
|
t.Errorf("expected name Simferopol, got %s", city.Name)
|
|
}
|
|
}
|
|
|
|
func TestGraphEdgeCreation(t *testing.T) {
|
|
// Test Real edge
|
|
realEdge := &Edge{
|
|
Kind: EdgeKindReal,
|
|
Duration: 3600,
|
|
TransportType: "train",
|
|
IsTransfer: false,
|
|
}
|
|
|
|
if realEdge.Kind != EdgeKindReal {
|
|
t.Errorf("expected EdgeKindReal, got %v", realEdge.Kind)
|
|
}
|
|
if realEdge.Duration != 3600 {
|
|
t.Errorf("expected duration 3600, got %d", realEdge.Duration)
|
|
}
|
|
if realEdge.TransportType != "train" {
|
|
t.Errorf("expected transport_type train, got %s", realEdge.TransportType)
|
|
}
|
|
if realEdge.IsTransfer {
|
|
t.Errorf("expected IsTransfer false for real edge")
|
|
}
|
|
|
|
// Test Synthetic edge
|
|
syntheticEdge := &Edge{
|
|
Kind: EdgeKindSynthetic,
|
|
Duration: 1800,
|
|
TransportType: "bus",
|
|
IsTransfer: true,
|
|
}
|
|
|
|
if syntheticEdge.Kind != EdgeKindSynthetic {
|
|
t.Errorf("expected EdgeKindSynthetic, got %v", syntheticEdge.Kind)
|
|
}
|
|
if syntheticEdge.IsTransfer != true {
|
|
t.Errorf("expected IsTransfer true for synthetic edge")
|
|
}
|
|
}
|
|
|
|
func TestGraphAddNodeAndEdge(t *testing.T) {
|
|
graph := NewGraph()
|
|
|
|
node := &Node{ID: "n1", Type: NodeTypeStation, Name: "Test Station"}
|
|
graph.AddNode(node)
|
|
|
|
if len(graph.Nodes()) != 1 {
|
|
t.Errorf("expected 1 node, got %d", len(graph.Nodes()))
|
|
}
|
|
if graph.Nodes()[0].ID != "n1" {
|
|
t.Errorf("expected node n1, got %s", graph.Nodes()[0].ID)
|
|
}
|
|
|
|
edge := &Edge{From: node, To: node, Kind: EdgeKindReal, Duration: 100}
|
|
graph.AddEdge(edge)
|
|
|
|
if len(graph.Edges()) != 1 {
|
|
t.Errorf("expected 1 edge, got %d", len(graph.Edges()))
|
|
}
|
|
if graph.Edges()[0].Duration != 100 {
|
|
t.Errorf("expected duration 100, got %d", graph.Edges()[0].Duration)
|
|
}
|
|
}
|
|
|
|
func TestGraphSortEdges(t *testing.T) {
|
|
edges := []*Edge{
|
|
{Duration: 300},
|
|
{Duration: 100},
|
|
{Duration: 200},
|
|
}
|
|
|
|
SortEdges(edges)
|
|
|
|
if edges[0].Duration != 100 {
|
|
t.Errorf("expected first edge duration 100, got %d", edges[0].Duration)
|
|
}
|
|
if edges[1].Duration != 200 {
|
|
t.Errorf("expected second edge duration 200, got %d", edges[1].Duration)
|
|
}
|
|
if edges[2].Duration != 300 {
|
|
t.Errorf("expected third edge duration 300, got %d", edges[2].Duration)
|
|
}
|
|
}
|
|
|
|
func TestBuildGraphFromStations(t *testing.T) {
|
|
stations := []StationInfo{
|
|
{ID: "s9600213", Name: "Шереметьево", CityCode: "c146", CityName: "Simferopol"},
|
|
{ID: "s9600396", Name: "Симферополь", CityCode: "c146", CityName: "Simferopol"},
|
|
{ID: "s9600157", Name: "Москва", CityCode: "c213", CityName: "Москва"},
|
|
}
|
|
|
|
graph := BuildGraphFromStations(stations)
|
|
|
|
// Should have station nodes + city nodes
|
|
// 3 stations + 2 cities (Simferopol + Moscow) = 5 nodes
|
|
nodes := graph.Nodes()
|
|
if len(nodes) != 5 {
|
|
t.Errorf("expected 5 nodes (3 stations + 2 cities), got %d", len(nodes))
|
|
}
|
|
|
|
// Should have edges
|
|
edges := graph.Edges()
|
|
if len(edges) < 3 {
|
|
t.Errorf("expected at least 3 edges (synthetic city↔station), got %d", len(edges))
|
|
}
|
|
|
|
// Verify city nodes exist
|
|
cityIDs := make(map[string]bool)
|
|
for _, n := range nodes {
|
|
if n.Type == NodeTypeCity {
|
|
cityIDs[n.ID] = true
|
|
}
|
|
}
|
|
if !cityIDs["city:c146"] {
|
|
t.Error("expected city:c146 node")
|
|
}
|
|
if !cityIDs["city:c213"] {
|
|
t.Error("expected city:c213 node")
|
|
}
|
|
}
|
|
|
|
func TestGraphNodesAndEdges(t *testing.T) {
|
|
graph := NewGraph()
|
|
|
|
// Add nodes
|
|
graph.AddNode(&Node{ID: "n1", Type: NodeTypeStation, Name: "Station 1"})
|
|
graph.AddNode(&Node{ID: "n2", Type: NodeTypeStation, Name: "Station 2"})
|
|
graph.AddNode(&Node{ID: "city:c1", Type: NodeTypeCity, Name: "City 1"})
|
|
|
|
if len(graph.Nodes()) != 3 {
|
|
t.Errorf("expected 3 nodes, got %d", len(graph.Nodes()))
|
|
}
|
|
|
|
// Add edges
|
|
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 100})
|
|
graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindSynthetic, Duration: 200})
|
|
|
|
if len(graph.Edges()) != 2 {
|
|
t.Errorf("expected 2 edges, got %d", len(graph.Edges()))
|
|
}
|
|
}
|
|
|
|
func TestFindRouteSuccess(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: "Clinic", CityCode: "c1"})
|
|
|
|
// Add real edges (direct route)
|
|
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false})
|
|
|
|
// Add synthetic transfer edge
|
|
graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindSynthetic, Duration: 1800, Transport: "train", IsTransfer: true})
|
|
|
|
// Search for route with max 1 transfer
|
|
opts := SearchOptions{MaxTransfers: 1, MCT: 300}
|
|
result := graph.FindRoute("s1", "s3", opts)
|
|
|
|
if result == nil {
|
|
t.Error("expected a route to be found")
|
|
}
|
|
if result.TotalTransfers > 1 {
|
|
t.Errorf("expected at most 1 transfer, got %d", result.TotalTransfers)
|
|
}
|
|
if result.TotalDuration <= 0 {
|
|
t.Errorf("expected positive duration, got %d", result.TotalDuration)
|
|
}
|
|
}
|
|
|
|
func TestFindRouteNoRoute(t *testing.T) {
|
|
graph := NewGraph()
|
|
|
|
// Add isolated nodes with no connections
|
|
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Station 1", CityCode: "c1"})
|
|
graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Station 2", CityCode: "c2"})
|
|
|
|
// Search with no edges - should return nil
|
|
opts := SearchOptions{MaxTransfers: 1, MCT: 300}
|
|
result := graph.FindRoute("s1", "s2", opts)
|
|
|
|
if result != nil {
|
|
t.Error("expected nil route when no edges exist, got result")
|
|
}
|
|
}
|
|
|
|
func TestFindRouteExceedsTransferLimit(t *testing.T) {
|
|
graph := NewGraph()
|
|
|
|
// Add a chain of stations with synthetic transfer edges (would require 4 transfers)
|
|
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
|
|
graph.AddNode(&Node{ID: "s2", Type: NodeTypeCity, Name: "City Hub 1", CityCode: "c1"})
|
|
graph.AddNode(&Node{ID: "s3", Type: NodeTypeCity, Name: "City Hub 2", CityCode: "c1"})
|
|
graph.AddNode(&Node{ID: "s4", Type: NodeTypeCity, Name: "City Hub 3", CityCode: "c1"})
|
|
graph.AddNode(&Node{ID: "s5", 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})
|
|
|
|
// Search with max 1 transfer - should not find route requiring 4 transfers
|
|
opts := SearchOptions{MaxTransfers: 1, MCT: 300}
|
|
result := graph.FindRoute("s1", "s5", opts)
|
|
|
|
if result != nil {
|
|
t.Error("expected nil route when transfers exceed limit, got result")
|
|
}
|
|
}
|
|
|
|
func TestApplyMCT_CityHubReducesMCT(t *testing.T) {
|
|
graph := NewGraph()
|
|
|
|
// 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})
|
|
|
|
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},
|
|
},
|
|
TotalDuration: 0,
|
|
TotalTransfers: 0,
|
|
}
|
|
|
|
result := graph.ApplyMCT(itinerary, 1800) // 30 min base MCT
|
|
|
|
// City hub transfer reduces MCT from 30 min (1800) to 15 min (900)
|
|
// TotalDuration only includes the MCT addition (starts at 0), so result = 900
|
|
if result.TotalDuration != 900 {
|
|
t.Errorf("expected total duration 900 (reduced MCT for city hub), got %d", result.TotalDuration)
|
|
}
|
|
}
|
|
|
|
func TestApplyMCT_ModeChangeIncreasesMCT(t *testing.T) {
|
|
graph := NewGraph()
|
|
|
|
// Create legs with mode change
|
|
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
|
|
graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
|
|
|
|
// Add first leg (train)
|
|
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], 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},
|
|
},
|
|
TotalDuration: 0,
|
|
TotalTransfers: 0,
|
|
}
|
|
|
|
// With only 1 leg, ApplyMCT returns early - no transfers needed
|
|
result := graph.ApplyMCT(itinerary, 1800)
|
|
|
|
// Single leg means no transfer, TotalDuration stays at 0
|
|
if result.TotalDuration != 0 {
|
|
t.Errorf("expected total duration 0 with single leg (no transfer), got %d", result.TotalDuration)
|
|
}
|
|
}
|
|
|
|
func TestApplyMCT_ModeChangeBetweenLegs(t *testing.T) {
|
|
graph := NewGraph()
|
|
|
|
// Create 2 stations for 2 legs with mode change (train then bus)
|
|
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"})
|
|
|
|
// Add real edges - train then bus (mode change)
|
|
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: "bus", 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: "bus", IsTransfer: false},
|
|
},
|
|
TotalDuration: 0,
|
|
TotalTransfers: 0,
|
|
}
|
|
|
|
result := graph.ApplyMCT(itinerary, 1800) // 30 min base MCT
|
|
|
|
// Mode change increases MCT from 30 min (1800) to 30+10 = 40 min (2400)
|
|
// TotalDuration only includes the MCT addition (one transfer), so result = 2400
|
|
if result.TotalDuration != 2400 {
|
|
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")
|
|
}
|
|
}
|
|
|
|
// TestSelectHubStations tests hub station selection by outgoing flights.
|
|
func TestSelectHubStations(t *testing.T) {
|
|
stations := []StationInfo{
|
|
{ID: "s1", Name: "Moscow", CityCode: "m1", CityName: "Moscow"},
|
|
{ID: "s2", Name: "SmallTown", CityCode: "s1", CityName: "Townville"},
|
|
{ID: "s3", Name: "CapitalCity", CityCode: "c1", CityName: "Capital"},
|
|
}
|
|
|
|
// With minOutgoingFlights=1, stations with default outgoing flights are hubs
|
|
// defaultOutgoingFlights is set to 1 so stations get selected
|
|
criteria := hubCriteria{minPopulation: 1, minOutgoingFlights: 1, defaultOutgoingFlights: 1}
|
|
result := SelectHubStations(stations, criteria)
|
|
|
|
// Moscow has default outgoing flights and should be a hub
|
|
moscowFound := false
|
|
for _, hub := range result.Hubs {
|
|
if hub.Station.Name == "Moscow" {
|
|
moscowFound = true
|
|
if !hub.IsHub {
|
|
t.Error("Moscow should be selected as a hub with minOutgoingFlights=1")
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if !moscowFound {
|
|
t.Error("expected Moscow to be in hub selection results")
|
|
}
|
|
|
|
// With high minOutgoingFlights, all stations should be rejected
|
|
highCriteria := hubCriteria{minPopulation: 1, minOutgoingFlights: 100}
|
|
highResult := SelectHubStations(stations, highCriteria)
|
|
|
|
// All stations should be rejected when threshold is too high
|
|
allRejected := true
|
|
for _, hub := range highResult.Hubs {
|
|
if hub.IsHub {
|
|
allRejected = false
|
|
break
|
|
}
|
|
}
|
|
if !allRejected {
|
|
t.Error("expected all stations to be rejected with minOutgoingFlights=100")
|
|
}
|
|
|
|
// Verify all rejected stations have IsHub=false
|
|
for _, hub := range highResult.Rejected {
|
|
if hub.IsHub {
|
|
t.Error("rejected station should have IsHub=false")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBuildGraphFromHubs tests graph building from hub stations.
|
|
func TestBuildGraphFromHubs(t *testing.T) {
|
|
stations := []StationInfo{
|
|
{ID: "s1", Name: "Moscow", CityCode: "m1", CityName: "Moscow"},
|
|
{ID: "s2", Name: "Tula", CityCode: "m1", CityName: "Tula"},
|
|
{ID: "s3", Name: "Simferopol", CityCode: "c1", CityName: "Simferopol"},
|
|
{ID: "s4", Name: "SmallCity", CityCode: "s1", CityName: "Smallville"},
|
|
}
|
|
|
|
criteria := hubCriteria{minPopulation: 1, minOutgoingFlights: 10, defaultOutgoingFlights: 10}
|
|
graph := BuildGraphFromHubs(stations, criteria)
|
|
|
|
// Should have station nodes + city nodes
|
|
nodes := graph.Nodes()
|
|
if len(nodes) < 3 {
|
|
t.Errorf("expected at least 3 nodes (stations + cities), got %d", len(nodes))
|
|
}
|
|
|
|
// Should have edges
|
|
edges := graph.Edges()
|
|
if len(edges) < 2 {
|
|
t.Errorf("expected at least 2 edges, got %d", len(edges))
|
|
}
|
|
|
|
// Verify city nodes exist
|
|
cityIDs := make(map[string]bool)
|
|
for _, n := range nodes {
|
|
if n.Type == NodeTypeCity {
|
|
cityIDs[n.ID] = true
|
|
}
|
|
}
|
|
if !cityIDs["city:m1"] {
|
|
t.Error("expected city:m1 node")
|
|
}
|
|
if !cityIDs["city:c1"] {
|
|
t.Error("expected city:c1 node")
|
|
}
|
|
|
|
// Verify hub stations are connected to city hubs
|
|
// Find edges from Moscow to city hub
|
|
moscowEdges := 0
|
|
for _, e := range edges {
|
|
if e.From != nil && e.From.Name == "Moscow" {
|
|
moscowEdges++
|
|
}
|
|
}
|
|
if moscowEdges == 0 {
|
|
t.Error("expected edges from Moscow to city hub")
|
|
}
|
|
}
|
|
|
|
// TestExpandGraphLazy tests the lazy graph expansion method.
|
|
func TestExpandGraphLazy(t *testing.T) {
|
|
graph := NewGraph()
|
|
|
|
// Add a station node
|
|
moscow := &Node{
|
|
ID: "s1",
|
|
Type: NodeTypeStation,
|
|
Name: "Moscow",
|
|
}
|
|
graph.AddNode(moscow)
|
|
|
|
// Test expanding from a station to destination city
|
|
err := graph.ExpandGraphLazy(moscow, "Simferopol")
|
|
if err != nil {
|
|
t.Errorf("expected no error from ExpandGraphLazy, got: %v", err)
|
|
}
|
|
|
|
// Should have added edges from Moscow to Simferopol city hub
|
|
nodes := graph.Nodes()
|
|
if len(nodes) < 2 {
|
|
t.Errorf("expected at least 2 nodes (Moscow + Simferopol city), got %d", len(nodes))
|
|
}
|
|
|
|
edges := graph.Edges()
|
|
if len(edges) < 2 {
|
|
t.Errorf("expected at least 2 edges (forward and reverse), got %d", len(edges))
|
|
}
|
|
|
|
// Verify the edge exists
|
|
moscowToSimferopol := false
|
|
simferopolToMoscow := false
|
|
for _, e := range edges {
|
|
if e.From != nil && e.From.Name == "Moscow" && e.To != nil && e.To.Name == "Simferopol" {
|
|
moscowToSimferopol = true
|
|
}
|
|
if e.From != nil && e.From.Name == "Simferopol" && e.To != nil && e.To.Name == "Moscow" {
|
|
simferopolToMoscow = true
|
|
}
|
|
}
|
|
if !moscowToSimferopol {
|
|
t.Error("expected edge from Moscow to Simferopol")
|
|
}
|
|
if !simferopolToMoscow {
|
|
t.Error("expected edge from Simferopol to Moscow")
|
|
}
|
|
}
|
|
|
|
// TestExpandGraphLazy_FromCityHub tests expansion from a city hub.
|
|
func TestExpandGraphLazy_FromCityHub(t *testing.T) {
|
|
graph := NewGraph()
|
|
|
|
// Add a city hub node
|
|
simferopol := &Node{
|
|
ID: "city:c1",
|
|
Type: NodeTypeCity,
|
|
Name: "Simferopol",
|
|
}
|
|
graph.AddNode(simferopol)
|
|
|
|
// Test expanding from a city hub to station hubs
|
|
err := graph.ExpandGraphLazy(simferopol, "Moscow")
|
|
if err != nil {
|
|
t.Errorf("expected no error from ExpandGraphLazy, got: %v", err)
|
|
}
|
|
|
|
// Should have added edges from Simferopol city to station hubs
|
|
edges := graph.Edges()
|
|
if len(edges) == 0 {
|
|
t.Error("expected edges from city hub to station hubs")
|
|
}
|
|
}
|
|
|
|
// TestExpandGraphLazy_InvalidNodeType tests invalid node type handling.
|
|
func TestExpandGraphLazy_InvalidNodeType(t *testing.T) {
|
|
graph := NewGraph()
|
|
|
|
// This test verifies the default case in ExpandGraphLazy
|
|
// We can't easily create an invalid node type, so we just verify
|
|
// the method handles errors gracefully
|
|
err := graph.ExpandGraphLazy(nil, "Test")
|
|
// Should not panic, just return an error
|
|
if err == nil {
|
|
t.Error("expected error from ExpandGraphLazy with nil node")
|
|
}
|
|
}
|
|
|
|
// TestBuildGraphFromHubs_EdgeCases tests edge cases for hub graph building.
|
|
func TestBuildGraphFromHubs_EdgeCases(t *testing.T) {
|
|
// Empty stations list
|
|
graph := BuildGraphFromHubs(nil, hubCriteria{minPopulation: 1, minOutgoingFlights: 10, defaultOutgoingFlights: 10})
|
|
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 = BuildGraphFromHubs([]StationInfo{{ID: "s1", Name: "Only", CityCode: "c1", CityName: "City1"}},
|
|
hubCriteria{minPopulation: 1, minOutgoingFlights: 10, defaultOutgoingFlights: 10})
|
|
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 = BuildGraphFromHubs([]StationInfo{
|
|
{ID: "s1", Name: "Station 1", CityCode: "c1", CityName: "City1"},
|
|
{ID: "s2", Name: "Station 2", CityCode: "c1", CityName: "City1"},
|
|
}, hubCriteria{minPopulation: 1, minOutgoingFlights: 10, defaultOutgoingFlights: 10})
|
|
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)
|
|
}
|
|
}
|