Files
Sportstime/SportsTimeTests/Planning/GameDAGRouterTests.swift
Trey t 1bd248c255 test(planning): complete test suite with Phase 11 edge cases
Implement comprehensive test infrastructure and all 124 tests across 11 phases:

- Phase 0: Test infrastructure (fixtures, mocks, helpers)
- Phases 1-10: Core planning engine tests (previously implemented)
- Phase 11: Edge case omnibus (11 new tests)
  - Data edge cases: nil stadiums, malformed dates, invalid coordinates
  - Boundary conditions: driving limits, radius boundaries
  - Time zone cases: cross-timezone games, DST transitions

Reorganize test structure under Planning/ directory with proper organization.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 01:14:40 -06:00

795 lines
32 KiB
Swift

//
// GameDAGRouterTests.swift
// SportsTimeTests
//
// Phase 2: GameDAGRouter Tests
// The "scary to touch" component extensive edge case coverage.
//
import Testing
import CoreLocation
@testable import SportsTime
@Suite("GameDAGRouter Tests")
struct GameDAGRouterTests {
// MARK: - Test Fixtures
private let calendar = Calendar.current
// Standard game times (7pm local)
private func gameDate(daysFromNow: Int, hour: Int = 19) -> Date {
let baseDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 1))!
var components = calendar.dateComponents([.year, .month, .day], from: baseDate)
components.day! += daysFromNow
components.hour = hour
components.minute = 0
return calendar.date(from: components)!
}
// Create a stadium at a known location
private func makeStadium(
id: UUID = UUID(),
city: String,
lat: Double,
lon: Double
) -> Stadium {
Stadium(
id: id,
name: "\(city) Stadium",
city: city,
state: "ST",
latitude: lat,
longitude: lon,
capacity: 40000,
sport: .mlb
)
}
// Create a game at a stadium
private func makeGame(
id: UUID = UUID(),
stadiumId: UUID,
dateTime: Date
) -> Game {
Game(
id: id,
homeTeamId: UUID(),
awayTeamId: UUID(),
stadiumId: stadiumId,
dateTime: dateTime,
sport: .mlb,
season: "2026"
)
}
// MARK: - 2A: Empty & Single-Element Cases
@Test("2.1 - Empty games returns empty array")
func test_findRoutes_EmptyGames_ReturnsEmptyArray() {
let routes = GameDAGRouter.findRoutes(
games: [],
stadiums: [:],
constraints: .default
)
#expect(routes.isEmpty, "Expected empty array for empty games input")
}
@Test("2.2 - Single game returns single route")
func test_findRoutes_SingleGame_ReturnsSingleRoute() {
let stadiumId = UUID()
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let game = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
let routes = GameDAGRouter.findRoutes(
games: [game],
stadiums: [stadiumId: stadium],
constraints: .default
)
#expect(routes.count == 1, "Expected exactly 1 route for single game")
#expect(routes.first?.count == 1, "Route should contain exactly 1 game")
#expect(routes.first?.first?.id == game.id, "Route should contain the input game")
}
@Test("2.3 - Single game with matching anchor returns single route")
func test_findRoutes_SingleGame_WithMatchingAnchor_ReturnsSingleRoute() {
let stadiumId = UUID()
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let game = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
let routes = GameDAGRouter.findRoutes(
games: [game],
stadiums: [stadiumId: stadium],
constraints: .default,
anchorGameIds: [game.id]
)
#expect(routes.count == 1, "Expected 1 route when anchor matches the only game")
#expect(routes.first?.contains(where: { $0.id == game.id }) == true)
}
@Test("2.4 - Single game with non-matching anchor returns empty")
func test_findRoutes_SingleGame_WithNonMatchingAnchor_ReturnsEmpty() {
let stadiumId = UUID()
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let game = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
let nonExistentAnchor = UUID()
let routes = GameDAGRouter.findRoutes(
games: [game],
stadiums: [stadiumId: stadium],
constraints: .default,
anchorGameIds: [nonExistentAnchor]
)
#expect(routes.isEmpty, "Expected empty when anchor doesn't match any game")
}
// MARK: - 2B: Two-Game Cases
@Test("2.5 - Two games with feasible transition returns both in order")
func test_findRoutes_TwoGames_FeasibleTransition_ReturnsBothInOrder() {
// Chicago to Milwaukee is ~90 miles - easily feasible
let chicagoStadiumId = UUID()
let milwaukeeStadiumId = UUID()
let chicagoStadium = makeStadium(id: chicagoStadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let milwaukeeStadium = makeStadium(id: milwaukeeStadiumId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
let game1 = makeGame(stadiumId: chicagoStadiumId, dateTime: gameDate(daysFromNow: 1, hour: 14)) // Day 1, 2pm
let game2 = makeGame(stadiumId: milwaukeeStadiumId, dateTime: gameDate(daysFromNow: 2, hour: 19)) // Day 2, 7pm
let stadiums = [chicagoStadiumId: chicagoStadium, milwaukeeStadiumId: milwaukeeStadium]
let routes = GameDAGRouter.findRoutes(
games: [game1, game2],
stadiums: stadiums,
constraints: .default
)
// Should have at least one route with both games
let routeWithBoth = routes.first { $0.count == 2 }
#expect(routeWithBoth != nil, "Expected a route containing both games")
if let route = routeWithBoth {
#expect(route[0].id == game1.id, "First game should be Chicago (earlier)")
#expect(route[1].id == game2.id, "Second game should be Milwaukee (later)")
}
}
@Test("2.6 - Two games with infeasible transition returns separate routes")
func test_findRoutes_TwoGames_InfeasibleTransition_ReturnsSeparateRoutes() {
// NYC to LA on same day is infeasible
let nycStadiumId = UUID()
let laStadiumId = UUID()
let nycStadium = makeStadium(id: nycStadiumId, city: "New York", lat: 40.7128, lon: -73.9352)
let laStadium = makeStadium(id: laStadiumId, city: "Los Angeles", lat: 34.0522, lon: -118.2437)
// Games on same day, 5 hours apart (can't drive 2500 miles in 5 hours)
let game1 = makeGame(stadiumId: nycStadiumId, dateTime: gameDate(daysFromNow: 1, hour: 13)) // 1pm
let game2 = makeGame(stadiumId: laStadiumId, dateTime: gameDate(daysFromNow: 1, hour: 21)) // 9pm
let stadiums = [nycStadiumId: nycStadium, laStadiumId: laStadium]
let routes = GameDAGRouter.findRoutes(
games: [game1, game2],
stadiums: stadiums,
constraints: .default
)
// Should NOT have a route with both games (infeasible)
let routeWithBoth = routes.first { $0.count == 2 }
#expect(routeWithBoth == nil, "Should not have a combined route for infeasible transition")
// Should have separate single-game routes
let singleGameRoutes = routes.filter { $0.count == 1 }
#expect(singleGameRoutes.count >= 2, "Should have separate routes for each game")
}
@Test("2.7 - Two games same stadium same day (doubleheader) succeeds")
func test_findRoutes_TwoGames_SameStadiumSameDay_Succeeds() {
let stadiumId = UUID()
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
// Doubleheader: 1pm and 7pm same day, same stadium
let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1, hour: 13))
let game2 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1, hour: 19))
let stadiums = [stadiumId: stadium]
let routes = GameDAGRouter.findRoutes(
games: [game1, game2],
stadiums: stadiums,
constraints: .default
)
// Should have a route with both games
let routeWithBoth = routes.first { $0.count == 2 }
#expect(routeWithBoth != nil, "Doubleheader at same stadium should be feasible")
if let route = routeWithBoth {
#expect(route[0].startTime < route[1].startTime, "Games should be in chronological order")
}
}
// MARK: - 2C: Anchor Game Constraints
@Test("2.8 - With anchors only returns routes containing all anchors")
func test_findRoutes_WithAnchors_OnlyReturnsRoutesContainingAllAnchors() {
let stadiumId = UUID()
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
let game2 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 2))
let game3 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 3))
let stadiums = [stadiumId: stadium]
let anchor = game2.id
let routes = GameDAGRouter.findRoutes(
games: [game1, game2, game3],
stadiums: stadiums,
constraints: .default,
anchorGameIds: [anchor]
)
// All routes must contain the anchor game
for route in routes {
let containsAnchor = route.contains { $0.id == anchor }
#expect(containsAnchor, "Every route must contain the anchor game")
}
}
@Test("2.9 - Impossible anchors returns empty")
func test_findRoutes_ImpossibleAnchors_ReturnsEmpty() {
// Two anchors at opposite ends of country on same day - impossible to attend both
let nycStadiumId = UUID()
let laStadiumId = UUID()
let nycStadium = makeStadium(id: nycStadiumId, city: "New York", lat: 40.7128, lon: -73.9352)
let laStadium = makeStadium(id: laStadiumId, city: "Los Angeles", lat: 34.0522, lon: -118.2437)
// Same day, same time - physically impossible
let game1 = makeGame(stadiumId: nycStadiumId, dateTime: gameDate(daysFromNow: 1, hour: 19))
let game2 = makeGame(stadiumId: laStadiumId, dateTime: gameDate(daysFromNow: 1, hour: 19))
let stadiums = [nycStadiumId: nycStadium, laStadiumId: laStadium]
let routes = GameDAGRouter.findRoutes(
games: [game1, game2],
stadiums: stadiums,
constraints: .default,
anchorGameIds: [game1.id, game2.id] // Both are anchors
)
#expect(routes.isEmpty, "Should return empty for impossible anchor combination")
}
@Test("2.10 - Multiple anchors route must contain all")
func test_findRoutes_MultipleAnchors_RouteMustContainAll() {
// Three games in nearby cities over 3 days - all feasible
let chicagoId = UUID()
let milwaukeeId = UUID()
let detroitId = UUID()
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
let detroit = makeStadium(id: detroitId, city: "Detroit", lat: 42.3314, lon: -83.0458)
let game1 = makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 1))
let game2 = makeGame(stadiumId: milwaukeeId, dateTime: gameDate(daysFromNow: 2))
let game3 = makeGame(stadiumId: detroitId, dateTime: gameDate(daysFromNow: 3))
let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee, detroitId: detroit]
// Make game1 and game3 anchors
let routes = GameDAGRouter.findRoutes(
games: [game1, game2, game3],
stadiums: stadiums,
constraints: .default,
anchorGameIds: [game1.id, game3.id]
)
#expect(!routes.isEmpty, "Should find routes with both anchors")
for route in routes {
let hasGame1 = route.contains { $0.id == game1.id }
let hasGame3 = route.contains { $0.id == game3.id }
#expect(hasGame1 && hasGame3, "Every route must contain both anchor games")
}
}
// MARK: - 2D: Repeat Cities Toggle
@Test("2.11 - Allow repeat cities same city multiple days allowed")
func test_findRoutes_AllowRepeatCities_SameCityMultipleDays_Allowed() {
let stadiumId = UUID()
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
// Three games in Chicago over 3 days
let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
let game2 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 2))
let game3 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 3))
let stadiums = [stadiumId: stadium]
let routes = GameDAGRouter.findRoutes(
games: [game1, game2, game3],
stadiums: stadiums,
constraints: .default,
allowRepeatCities: true
)
// Should have routes with all 3 games (same city allowed)
let routeWithAll = routes.first { $0.count == 3 }
#expect(routeWithAll != nil, "Should allow visiting same city multiple days when repeat cities enabled")
}
@Test("2.12 - Disallow repeat cities skips second visit")
func test_findRoutes_DisallowRepeatCities_SkipsSecondVisit() {
let chicagoId = UUID()
let milwaukeeId = UUID()
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
// Day 1: Chicago, Day 2: Milwaukee, Day 3: Back to Chicago
let game1 = makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 1))
let game2 = makeGame(stadiumId: milwaukeeId, dateTime: gameDate(daysFromNow: 2))
let game3 = makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 3)) // Return to Chicago
let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee]
let routes = GameDAGRouter.findRoutes(
games: [game1, game2, game3],
stadiums: stadiums,
constraints: .default,
allowRepeatCities: false
)
// Should NOT have a route with both Chicago games
for route in routes {
let chicagoGames = route.filter { stadiums[$0.stadiumId]?.city == "Chicago" }
#expect(chicagoGames.count <= 1, "Should not repeat Chicago when repeat cities disabled")
}
}
@Test("2.13 - Disallow repeat cities only option is repeat overrides with warning")
func test_findRoutes_DisallowRepeatCities_OnlyOptionIsRepeat_OverridesWithWarning() {
// When only games available are in the same city, we still need to produce routes
let stadiumId = UUID()
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
// Only Chicago games available
let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
let game2 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 2))
let stadiums = [stadiumId: stadium]
let routes = GameDAGRouter.findRoutes(
games: [game1, game2],
stadiums: stadiums,
constraints: .default,
allowRepeatCities: false
)
// Should still return single-game routes even with repeat cities disabled
#expect(!routes.isEmpty, "Should return routes even when only option is repeat city")
// Note: TDD defines Trip.warnings property (test 2.13 in plan)
// For now, we verify routes exist; warning system will be added when implementing
}
// MARK: - 2E: Driving Constraints
@Test("2.14 - Exceeds max daily driving transition rejected")
func test_findRoutes_ExceedsMaxDailyDriving_TransitionRejected() {
// NYC to Denver is ~1,800 miles, way over 8 hours of driving (480 miles at 60mph)
let nycId = UUID()
let denverId = UUID()
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
let denver = makeStadium(id: denverId, city: "Denver", lat: 39.7392, lon: -104.9903)
// Games on consecutive days - can't drive 1800 miles in one day
let game1 = makeGame(stadiumId: nycId, dateTime: gameDate(daysFromNow: 1, hour: 14))
let game2 = makeGame(stadiumId: denverId, dateTime: gameDate(daysFromNow: 2, hour: 19))
let stadiums = [nycId: nyc, denverId: denver]
// Use strict constraints (8 hours max)
let strictConstraints = DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: 8.0)
let routes = GameDAGRouter.findRoutes(
games: [game1, game2],
stadiums: stadiums,
constraints: strictConstraints
)
// Should not have a combined route (distance too far for 1 day)
let routeWithBoth = routes.first { $0.count == 2 }
#expect(routeWithBoth == nil, "Should reject transition exceeding max daily driving")
}
@Test("2.15 - Multi-day drive allowed if within daily limits")
func test_findRoutes_MultiDayDrive_Allowed_IfWithinDailyLimits() {
// NYC to Chicago is ~790 miles - doable over multiple days
let nycId = UUID()
let chicagoId = UUID()
let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352)
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
// Games 3 days apart - enough time to drive 790 miles
let game1 = makeGame(stadiumId: nycId, dateTime: gameDate(daysFromNow: 1, hour: 14))
let game2 = makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 4, hour: 19))
let stadiums = [nycId: nyc, chicagoId: chicago]
let routes = GameDAGRouter.findRoutes(
games: [game1, game2],
stadiums: stadiums,
constraints: .default
)
// Should have a route with both (multi-day driving allowed)
let routeWithBoth = routes.first { $0.count == 2 }
#expect(routeWithBoth != nil, "Should allow multi-day drive when time permits")
}
@Test("2.16 - Same day different stadiums checks available time")
func test_findRoutes_SameDayDifferentStadiums_ChecksAvailableTime() {
// Chicago to Milwaukee is ~90 miles (~1.5 hours driving)
let chicagoId = UUID()
let milwaukeeId = UUID()
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
// Same day: Chicago at 1pm, Milwaukee at 7pm (6 hours apart - feasible)
let game1 = makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 1, hour: 13))
let game2 = makeGame(stadiumId: milwaukeeId, dateTime: gameDate(daysFromNow: 1, hour: 19))
let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee]
let routes = GameDAGRouter.findRoutes(
games: [game1, game2],
stadiums: stadiums,
constraints: .default
)
// Should be feasible (1pm game + 3hr duration + 1.5hr drive = arrives ~5:30pm for 7pm game)
let routeWithBoth = routes.first { $0.count == 2 }
#expect(routeWithBoth != nil, "Should allow same-day travel when time permits")
// Now test too tight timing
let game3 = makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 2, hour: 16)) // 4pm
let game4 = makeGame(stadiumId: milwaukeeId, dateTime: gameDate(daysFromNow: 2, hour: 17)) // 5pm (only 1 hr apart)
let routes2 = GameDAGRouter.findRoutes(
games: [game3, game4],
stadiums: stadiums,
constraints: .default
)
let tooTightRoute = routes2.first { $0.count == 2 }
#expect(tooTightRoute == nil, "Should reject same-day travel when not enough time")
}
// MARK: - 2F: Calendar Day Logic
@Test("2.17 - Max day lookahead respects limit")
func test_findRoutes_MaxDayLookahead_RespectsLimit() {
// Games more than 5 days apart should not connect directly
let stadiumId = UUID()
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
let game2 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 8)) // 7 days later
let stadiums = [stadiumId: stadium]
let routes = GameDAGRouter.findRoutes(
games: [game1, game2],
stadiums: stadiums,
constraints: .default
)
// With max lookahead of 5, these shouldn't directly connect
// (Though they might still appear in separate routes)
let routeWithBoth = routes.first { $0.count == 2 }
// Note: Implementation uses maxDayLookahead = 5
// Games 7 days apart may not connect directly
// This test verifies the behavior
if routeWithBoth != nil {
// If they do connect, verify they're in order
#expect(routeWithBoth![0].startTime < routeWithBoth![1].startTime)
}
}
@Test("2.18 - DST transition handles correctly")
func test_findRoutes_DSTTransition_HandlesCorrectly() {
// Test around DST transition (March 9, 2026 - spring forward)
let stadiumId = UUID()
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
// Create dates around DST transition
var components1 = DateComponents()
components1.year = 2026
components1.month = 3
components1.day = 8 // Day before spring forward
components1.hour = 19
let preDST = calendar.date(from: components1)!
var components2 = DateComponents()
components2.year = 2026
components2.month = 3
components2.day = 9 // Spring forward day
components2.hour = 19
let postDST = calendar.date(from: components2)!
let game1 = makeGame(stadiumId: stadiumId, dateTime: preDST)
let game2 = makeGame(stadiumId: stadiumId, dateTime: postDST)
let stadiums = [stadiumId: stadium]
let routes = GameDAGRouter.findRoutes(
games: [game1, game2],
stadiums: stadiums,
constraints: .default
)
// Should handle DST correctly - both games should be connectable
let routeWithBoth = routes.first { $0.count == 2 }
#expect(routeWithBoth != nil, "Should handle DST transition correctly")
}
@Test("2.19 - Midnight game assigns to correct day")
func test_findRoutes_MidnightGame_AssignsToCorrectDay() {
let stadiumId = UUID()
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
// Game at 12:05 AM belongs to the new day
var components = DateComponents()
components.year = 2026
components.month = 6
components.day = 2
components.hour = 0
components.minute = 5
let midnightGame = calendar.date(from: components)!
let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1, hour: 19)) // Day 1, 7pm
let game2 = makeGame(stadiumId: stadiumId, dateTime: midnightGame) // Day 2, 12:05am
let stadiums = [stadiumId: stadium]
let routes = GameDAGRouter.findRoutes(
games: [game1, game2],
stadiums: stadiums,
constraints: .default
)
// Midnight game should be on day 2, making transition feasible
let routeWithBoth = routes.first { $0.count == 2 }
#expect(routeWithBoth != nil, "Midnight game should be assigned to correct calendar day")
}
// MARK: - 2G: Diversity Selection
@Test("2.20 - Select diverse routes includes short and long trips")
func test_selectDiverseRoutes_ShortAndLongTrips_BothRepresented() {
// Create a mix of games over a week
let stadiumId = UUID()
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
var games: [Game] = []
for day in 1...7 {
games.append(makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: day)))
}
let stadiums = [stadiumId: stadium]
let routes = GameDAGRouter.findRoutes(
games: games,
stadiums: stadiums,
constraints: .default,
allowRepeatCities: true
)
// Should have both short (2-3 game) and long (5+ game) routes
let shortRoutes = routes.filter { $0.count <= 3 }
let longRoutes = routes.filter { $0.count >= 5 }
#expect(!shortRoutes.isEmpty, "Should include short trip options")
#expect(!longRoutes.isEmpty, "Should include long trip options")
}
@Test("2.21 - Select diverse routes includes high and low mileage")
func test_selectDiverseRoutes_HighAndLowMileage_BothRepresented() {
// Create games in both nearby and distant cities
let chicagoId = UUID()
let milwaukeeId = UUID()
let laId = UUID()
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
let la = makeStadium(id: laId, city: "Los Angeles", lat: 34.0522, lon: -118.2437)
let games = [
makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 1)),
makeGame(stadiumId: milwaukeeId, dateTime: gameDate(daysFromNow: 2)),
makeGame(stadiumId: laId, dateTime: gameDate(daysFromNow: 8)), // Far away, needs time
]
let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee, laId: la]
let routes = GameDAGRouter.findRoutes(
games: games,
stadiums: stadiums,
constraints: .default
)
// Should have routes with varying mileage
#expect(!routes.isEmpty, "Should produce diverse mileage routes")
}
@Test("2.22 - Select diverse routes includes few and many cities")
func test_selectDiverseRoutes_FewAndManyCities_BothRepresented() {
// Create games across multiple cities
let cities = [
("Chicago", 41.8781, -87.6298),
("Milwaukee", 43.0389, -87.9065),
("Detroit", 42.3314, -83.0458),
("Cleveland", 41.4993, -81.6944),
]
var stadiums: [UUID: Stadium] = [:]
var games: [Game] = []
for (index, city) in cities.enumerated() {
let stadiumId = UUID()
stadiums[stadiumId] = makeStadium(id: stadiumId, city: city.0, lat: city.1, lon: city.2)
games.append(makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: index + 1)))
}
let routes = GameDAGRouter.findRoutes(
games: games,
stadiums: stadiums,
constraints: .default
)
// Should have routes with varying city counts
let cityCounts = routes.map { route in
Set(route.compactMap { stadiums[$0.stadiumId]?.city }).count
}
let minCities = cityCounts.min() ?? 0
let maxCities = cityCounts.max() ?? 0
#expect(minCities < maxCities || routes.count <= 1, "Should have routes with varying city counts")
}
@Test("2.23 - Select diverse routes deduplicates")
func test_selectDiverseRoutes_DuplicateRoutes_Deduplicated() {
let stadiumId = UUID()
let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1))
let game2 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 2))
let stadiums = [stadiumId: stadium]
let routes = GameDAGRouter.findRoutes(
games: [game1, game2],
stadiums: stadiums,
constraints: .default,
allowRepeatCities: true
)
// Check for duplicates
var seen = Set<String>()
for route in routes {
let key = route.map { $0.id.uuidString }.joined(separator: "-")
#expect(!seen.contains(key), "Routes should be deduplicated")
seen.insert(key)
}
}
// MARK: - 2H: Cycle Handling
@Test("2.24 - Graph with potential cycle handles silently")
func test_findRoutes_GraphWithPotentialCycle_HandlesSilently() {
// Create a scenario where a naive algorithm might get stuck in a loop
let chicagoId = UUID()
let milwaukeeId = UUID()
let detroitId = UUID()
let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298)
let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065)
let detroit = makeStadium(id: detroitId, city: "Detroit", lat: 42.3314, lon: -83.0458)
// Multiple games at each city over several days (potential for cycles)
let games = [
makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 1)),
makeGame(stadiumId: milwaukeeId, dateTime: gameDate(daysFromNow: 2)),
makeGame(stadiumId: chicagoId, dateTime: gameDate(daysFromNow: 3)), // Back to Chicago
makeGame(stadiumId: detroitId, dateTime: gameDate(daysFromNow: 4)),
makeGame(stadiumId: milwaukeeId, dateTime: gameDate(daysFromNow: 5)), // Back to Milwaukee
]
let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee, detroitId: detroit]
// Should complete without hanging or infinite loop
let routes = GameDAGRouter.findRoutes(
games: games,
stadiums: stadiums,
constraints: .default,
allowRepeatCities: true
)
// Just verify it completes and returns valid routes
#expect(routes.allSatisfy { !$0.isEmpty }, "All routes should be non-empty")
// Verify chronological order in each route
for route in routes {
for i in 0..<(route.count - 1) {
#expect(route[i].startTime < route[i + 1].startTime, "Games should be in chronological order")
}
}
}
// MARK: - 2I: Beam Search Behavior
@Test("2.25 - Large dataset scales beam width")
func test_findRoutes_LargeDataset_ScalesBeamWidth() {
// Generate a large dataset (use fixture generator)
let data = FixtureGenerator.generate(with: .medium) // 500 games
let routes = GameDAGRouter.findRoutes(
games: data.games,
stadiums: data.stadiumsById,
constraints: .default
)
// Should complete and return routes
#expect(!routes.isEmpty, "Should produce routes for large dataset")
// Verify routes are valid
for route in routes {
for i in 0..<(route.count - 1) {
#expect(route[i].startTime <= route[i + 1].startTime, "Games should be chronologically ordered")
}
}
}
@Test("2.26 - Early termination triggers when beam full")
func test_findRoutes_EarlyTermination_TriggersWhenBeamFull() {
// Generate a dataset that would take very long without early termination
let config = FixtureGenerator.Configuration(
seed: 42,
gameCount: 100,
stadiumCount: 20,
teamCount: 20,
geographicSpread: .regional
)
let data = FixtureGenerator.generate(with: config)
let startTime = Date()
let routes = GameDAGRouter.findRoutes(
games: data.games,
stadiums: data.stadiumsById,
constraints: .default,
beamWidth: 50 // Moderate beam width
)
let elapsed = Date().timeIntervalSince(startTime)
// Should complete in reasonable time (< 30 seconds indicates early termination is working)
#expect(elapsed < TestConstants.hangTimeout, "Should complete before hang timeout (early termination)")
#expect(!routes.isEmpty, "Should produce routes")
}
}