604 lines
24 KiB
Swift
604 lines
24 KiB
Swift
//
|
|
// GameDAGRouterTests.swift
|
|
// SportsTimeTests
|
|
//
|
|
// TDD specification + property tests for GameDAGRouter.
|
|
//
|
|
|
|
import Testing
|
|
import CoreLocation
|
|
@testable import SportsTime
|
|
|
|
@Suite("GameDAGRouter")
|
|
struct GameDAGRouterTests {
|
|
|
|
// MARK: - Test Data
|
|
|
|
private let constraints = DrivingConstraints.default // 1 driver, 8 hrs/day
|
|
|
|
// Stadium locations (real coordinates)
|
|
private let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
|
|
private let bostonCoord = CLLocationCoordinate2D(latitude: 42.3467, longitude: -71.0972)
|
|
private let phillyCoord = CLLocationCoordinate2D(latitude: 39.9526, longitude: -75.1652)
|
|
private let chicagoCoord = CLLocationCoordinate2D(latitude: 41.8827, longitude: -87.6233)
|
|
private let laCoord = CLLocationCoordinate2D(latitude: 34.0141, longitude: -118.2879)
|
|
private let seattleCoord = CLLocationCoordinate2D(latitude: 47.5914, longitude: -122.3316)
|
|
|
|
private let calendar = TestClock.calendar
|
|
|
|
// MARK: - Specification Tests: Edge Cases
|
|
|
|
@Test("findRoutes: empty games returns empty")
|
|
func findRoutes_emptyGames_returnsEmpty() {
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: [],
|
|
stadiums: [:],
|
|
constraints: constraints
|
|
)
|
|
#expect(routes.isEmpty)
|
|
}
|
|
|
|
@Test("findRoutes: single game with no anchors returns single-game route")
|
|
func findRoutes_singleGame_noAnchors_returnsSingleRoute() {
|
|
let (game, stadium) = makeGameAndStadium(city: "New York", date: TestClock.now)
|
|
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: [game],
|
|
stadiums: [stadium.id: stadium],
|
|
constraints: constraints
|
|
)
|
|
|
|
#expect(routes.count == 1)
|
|
#expect(routes.first?.count == 1)
|
|
#expect(routes.first?.first?.id == game.id)
|
|
}
|
|
|
|
@Test("findRoutes: single game matching anchor returns single-game route")
|
|
func findRoutes_singleGame_matchingAnchor_returnsSingleRoute() {
|
|
let (game, stadium) = makeGameAndStadium(city: "New York", date: TestClock.now)
|
|
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: [game],
|
|
stadiums: [stadium.id: stadium],
|
|
constraints: constraints,
|
|
anchorGameIds: [game.id]
|
|
)
|
|
|
|
#expect(routes.count == 1)
|
|
#expect(routes.first?.first?.id == game.id)
|
|
}
|
|
|
|
@Test("findRoutes: single game not matching anchor returns empty")
|
|
func findRoutes_singleGame_notMatchingAnchor_returnsEmpty() {
|
|
let (game, stadium) = makeGameAndStadium(city: "New York", date: TestClock.now)
|
|
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: [game],
|
|
stadiums: [stadium.id: stadium],
|
|
constraints: constraints,
|
|
anchorGameIds: ["different-game-id"]
|
|
)
|
|
|
|
#expect(routes.isEmpty)
|
|
}
|
|
|
|
// MARK: - Specification Tests: Two Games
|
|
|
|
@Test("findRoutes: two feasible games returns combined route")
|
|
func findRoutes_twoFeasibleGames_returnsCombinedRoute() {
|
|
let today = calendar.startOfDay(for: TestClock.now)
|
|
let game1Date = calendar.date(bySettingHour: 13, minute: 0, second: 0, of: today)!
|
|
let game2Date = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: today)!
|
|
|
|
// NYC to Philly is ~95 miles, ~1.5 hours - very feasible same day
|
|
let (game1, stadium1) = makeGameAndStadium(city: "New York", date: game1Date, coord: nycCoord)
|
|
let (game2, stadium2) = makeGameAndStadium(city: "Philadelphia", date: game2Date, coord: phillyCoord)
|
|
|
|
let stadiums = [stadium1.id: stadium1, stadium2.id: stadium2]
|
|
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: stadiums,
|
|
constraints: constraints
|
|
)
|
|
|
|
// Should have at least one route with both games
|
|
let combinedRoute = routes.first(where: { $0.count == 2 })
|
|
#expect(combinedRoute != nil)
|
|
|
|
// First game should be earlier one
|
|
#expect(combinedRoute?.first?.startTime ?? Date.distantFuture < combinedRoute?.last?.startTime ?? Date.distantPast)
|
|
}
|
|
|
|
@Test("findRoutes: two infeasible same-day games returns separate routes when no anchors")
|
|
func findRoutes_twoInfeasibleGames_noAnchors_returnsSeparateRoutes() {
|
|
let today = calendar.startOfDay(for: TestClock.now)
|
|
let game1Date = calendar.date(bySettingHour: 13, minute: 0, second: 0, of: today)!
|
|
let game2Date = calendar.date(bySettingHour: 15, minute: 0, second: 0, of: today)! // Only 2 hours later
|
|
|
|
// NYC to Chicago is ~790 miles - impossible in 2 hours
|
|
let (game1, stadium1) = makeGameAndStadium(city: "New York", date: game1Date, coord: nycCoord)
|
|
let (game2, stadium2) = makeGameAndStadium(city: "Chicago", date: game2Date, coord: chicagoCoord)
|
|
|
|
let stadiums = [stadium1.id: stadium1, stadium2.id: stadium2]
|
|
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: stadiums,
|
|
constraints: constraints
|
|
)
|
|
|
|
// Should return separate single-game routes
|
|
#expect(routes.count == 2)
|
|
#expect(routes.allSatisfy { $0.count == 1 })
|
|
}
|
|
|
|
@Test("findRoutes: two infeasible games with both as anchors returns empty")
|
|
func findRoutes_twoInfeasibleGames_bothAnchors_returnsEmpty() {
|
|
let today = calendar.startOfDay(for: TestClock.now)
|
|
let game1Date = calendar.date(bySettingHour: 13, minute: 0, second: 0, of: today)!
|
|
let game2Date = calendar.date(bySettingHour: 15, minute: 0, second: 0, of: today)!
|
|
|
|
let (game1, stadium1) = makeGameAndStadium(city: "New York", date: game1Date, coord: nycCoord)
|
|
let (game2, stadium2) = makeGameAndStadium(city: "Chicago", date: game2Date, coord: chicagoCoord)
|
|
|
|
let stadiums = [stadium1.id: stadium1, stadium2.id: stadium2]
|
|
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: stadiums,
|
|
constraints: constraints,
|
|
anchorGameIds: [game1.id, game2.id] // Both must be in route
|
|
)
|
|
|
|
#expect(routes.isEmpty) // Can't satisfy both anchors
|
|
}
|
|
|
|
// MARK: - Specification Tests: Anchor Games
|
|
|
|
@Test("findRoutes: routes contain all anchor games")
|
|
func findRoutes_routesContainAllAnchors() {
|
|
let today = calendar.startOfDay(for: TestClock.now)
|
|
let dates = (0..<5).map { dayOffset in
|
|
calendar.date(byAdding: .day, value: dayOffset, to: today)!
|
|
}
|
|
|
|
// Create 5 games over 5 days, all nearby (East Coast)
|
|
let gamesAndStadiums = [
|
|
makeGameAndStadium(city: "New York", date: dates[0], coord: nycCoord),
|
|
makeGameAndStadium(city: "Philadelphia", date: dates[1], coord: phillyCoord),
|
|
makeGameAndStadium(city: "Boston", date: dates[2], coord: bostonCoord),
|
|
makeGameAndStadium(city: "New York2", date: dates[3], coord: nycCoord),
|
|
makeGameAndStadium(city: "Philadelphia2", date: dates[4], coord: phillyCoord)
|
|
]
|
|
|
|
let games = gamesAndStadiums.map { $0.0 }
|
|
var stadiums: [String: Stadium] = [:]
|
|
for (_, stadium) in gamesAndStadiums {
|
|
stadiums[stadium.id] = stadium
|
|
}
|
|
|
|
// Use first and third games as anchors
|
|
let anchorIds: Set<String> = [games[0].id, games[2].id]
|
|
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: games,
|
|
stadiums: stadiums,
|
|
constraints: constraints,
|
|
anchorGameIds: anchorIds
|
|
)
|
|
|
|
// All routes must contain both anchor games
|
|
for route in routes {
|
|
let routeIds = Set(route.map { $0.id })
|
|
#expect(anchorIds.isSubset(of: routeIds), "Route must contain all anchors")
|
|
}
|
|
}
|
|
|
|
// MARK: - Specification Tests: Repeat Cities
|
|
|
|
@Test("findRoutes: allowRepeatCities=false excludes routes with duplicate cities")
|
|
func findRoutes_disallowRepeatCities_excludesDuplicates() {
|
|
let today = calendar.startOfDay(for: TestClock.now)
|
|
let dates = (0..<3).map { dayOffset in
|
|
calendar.date(byAdding: .day, value: dayOffset, to: today)!
|
|
}
|
|
|
|
// NYC -> Boston -> NYC would be a repeat
|
|
let (game1, stadium1) = makeGameAndStadium(city: "New York", date: dates[0], coord: nycCoord)
|
|
let (game2, stadium2) = makeGameAndStadium(city: "Boston", date: dates[1], coord: bostonCoord)
|
|
let (game3, stadium3) = makeGameAndStadium(city: "New York", date: dates[2], coord: nycCoord) // Same city as game1
|
|
|
|
let stadiums = [stadium1.id: stadium1, stadium2.id: stadium2, stadium3.id: stadium3]
|
|
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: [game1, game2, game3],
|
|
stadiums: stadiums,
|
|
constraints: constraints,
|
|
allowRepeatCities: false
|
|
)
|
|
|
|
// No route should have both game1 and game3 (same city)
|
|
for route in routes {
|
|
let cities = route.compactMap { game in stadiums[game.stadiumId]?.city }
|
|
let uniqueCities = Set(cities)
|
|
#expect(cities.count == uniqueCities.count, "No duplicate cities when allowRepeatCities=false")
|
|
}
|
|
}
|
|
|
|
@Test("findRoutes: allowRepeatCities=true allows routes with duplicate cities")
|
|
func findRoutes_allowRepeatCities_allowsDuplicates() {
|
|
let today = calendar.startOfDay(for: TestClock.now)
|
|
let dates = (0..<3).map { dayOffset in
|
|
calendar.date(byAdding: .day, value: dayOffset, to: today)!
|
|
}
|
|
|
|
let (game1, stadium1) = makeGameAndStadium(city: "New York", date: dates[0], coord: nycCoord)
|
|
let (game2, stadium2) = makeGameAndStadium(city: "Boston", date: dates[1], coord: bostonCoord)
|
|
let (game3, _) = makeGameAndStadium(city: "New York", date: dates[2], coord: nycCoord) // Same city
|
|
|
|
// Use same stadium ID for same city
|
|
let nyStadium = stadium1
|
|
let bosStadium = stadium2
|
|
var game3Modified = game3
|
|
game3Modified = Game(
|
|
id: game3.id,
|
|
homeTeamId: game3.homeTeamId,
|
|
awayTeamId: game3.awayTeamId,
|
|
stadiumId: nyStadium.id, // Same stadium as game1
|
|
dateTime: game3.dateTime,
|
|
sport: game3.sport,
|
|
season: game3.season,
|
|
isPlayoff: game3.isPlayoff
|
|
)
|
|
|
|
let stadiums = [nyStadium.id: nyStadium, bosStadium.id: bosStadium]
|
|
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: [game1, game2, game3Modified],
|
|
stadiums: stadiums,
|
|
constraints: constraints,
|
|
allowRepeatCities: true
|
|
)
|
|
|
|
// Should have routes with all 3 games
|
|
let fullRoutes = routes.filter { $0.count == 3 }
|
|
#expect(!fullRoutes.isEmpty, "Should allow routes with same city when allowRepeatCities=true")
|
|
}
|
|
|
|
// MARK: - Specification Tests: Chronological Order
|
|
|
|
@Test("findRoutes: all routes are chronologically ordered")
|
|
func findRoutes_allRoutesChronological() {
|
|
let today = calendar.startOfDay(for: TestClock.now)
|
|
let dates = (0..<5).map { dayOffset in
|
|
calendar.date(byAdding: .day, value: dayOffset, to: today)!
|
|
}
|
|
|
|
let gamesAndStadiums = [
|
|
makeGameAndStadium(city: "New York", date: dates[0], coord: nycCoord),
|
|
makeGameAndStadium(city: "Philadelphia", date: dates[1], coord: phillyCoord),
|
|
makeGameAndStadium(city: "Boston", date: dates[2], coord: bostonCoord),
|
|
makeGameAndStadium(city: "New York2", date: dates[3], coord: nycCoord),
|
|
makeGameAndStadium(city: "Philadelphia2", date: dates[4], coord: phillyCoord)
|
|
]
|
|
|
|
let games = gamesAndStadiums.map { $0.0 }
|
|
var stadiums: [String: Stadium] = [:]
|
|
for (_, stadium) in gamesAndStadiums {
|
|
stadiums[stadium.id] = stadium
|
|
}
|
|
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: games,
|
|
stadiums: stadiums,
|
|
constraints: constraints
|
|
)
|
|
|
|
for route in routes {
|
|
for i in 1..<route.count {
|
|
#expect(route[i].startTime > route[i-1].startTime,
|
|
"Games must be in chronological order")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Specification Tests: Driving Constraints
|
|
|
|
@Test("findRoutes: respects maxDailyDrivingHours for same-day games")
|
|
func findRoutes_respectsSameDayDrivingLimit() {
|
|
let today = calendar.startOfDay(for: TestClock.now)
|
|
let game1Time = calendar.date(bySettingHour: 13, minute: 0, second: 0, of: today)!
|
|
let game2Time = calendar.date(bySettingHour: 20, minute: 0, second: 0, of: today)!
|
|
|
|
// NYC to Chicago: ~790 miles, ~13 hours driving
|
|
// With 7 hours between games and 2hr buffer, only 5 hours available
|
|
let (game1, stadium1) = makeGameAndStadium(city: "New York", date: game1Time, coord: nycCoord)
|
|
let (game2, stadium2) = makeGameAndStadium(city: "Chicago", date: game2Time, coord: chicagoCoord)
|
|
|
|
let stadiums = [stadium1.id: stadium1, stadium2.id: stadium2]
|
|
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: stadiums,
|
|
constraints: constraints // 8 hr max per day
|
|
)
|
|
|
|
// Should NOT have a route with both games on same day
|
|
let sameDayRoutes = routes.filter { $0.count == 2 }
|
|
#expect(sameDayRoutes.isEmpty, "NYC to Chicago same-day should be infeasible")
|
|
}
|
|
|
|
@Test("findRoutes: multi-day trips allow longer total driving")
|
|
func findRoutes_multiDayTrips_allowLongerDriving() {
|
|
let today = calendar.startOfDay(for: TestClock.now)
|
|
let game1Date = today
|
|
let game2Date = calendar.date(byAdding: .day, value: 2, to: today)! // 2 days later
|
|
|
|
// NYC to Chicago: ~790 miles, ~13 hours driving
|
|
// With 2 days between, should be feasible (16 hours available)
|
|
let (game1, stadium1) = makeGameAndStadium(city: "New York", date: game1Date, coord: nycCoord)
|
|
let (game2, stadium2) = makeGameAndStadium(city: "Chicago", date: game2Date, coord: chicagoCoord)
|
|
|
|
let stadiums = [stadium1.id: stadium1, stadium2.id: stadium2]
|
|
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: stadiums,
|
|
constraints: constraints
|
|
)
|
|
|
|
// Should have a route with both games
|
|
let combinedRoutes = routes.filter { $0.count == 2 }
|
|
#expect(!combinedRoutes.isEmpty, "NYC to Chicago over 2 days should be feasible")
|
|
}
|
|
|
|
@Test("findRoutes: anchor routes can span gaps larger than 5 days")
|
|
func findRoutes_anchorRoutesAllowLongDateGaps() {
|
|
let today = calendar.startOfDay(for: TestClock.now)
|
|
let day0 = today
|
|
let day1 = calendar.date(byAdding: .day, value: 1, to: today)!
|
|
let day8 = calendar.date(byAdding: .day, value: 8, to: today)!
|
|
|
|
let sharedStadium = makeStadium(city: "New York", coord: nycCoord)
|
|
let bridgeStadium = makeStadium(city: "Boston", coord: bostonCoord)
|
|
|
|
let anchorStart = makeGame(stadiumId: sharedStadium.id, date: day0)
|
|
let bridgeGame = makeGame(stadiumId: bridgeStadium.id, date: day1)
|
|
let anchorEnd = makeGame(stadiumId: sharedStadium.id, date: day8)
|
|
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: [anchorStart, bridgeGame, anchorEnd],
|
|
stadiums: [sharedStadium.id: sharedStadium, bridgeStadium.id: bridgeStadium],
|
|
constraints: constraints,
|
|
anchorGameIds: [anchorStart.id, anchorEnd.id]
|
|
)
|
|
|
|
#expect(!routes.isEmpty, "Expected a route that includes both anchors across an 8-day gap")
|
|
|
|
for route in routes {
|
|
let ids = Set(route.map { $0.id })
|
|
#expect(ids.contains(anchorStart.id), "Route should include start anchor")
|
|
#expect(ids.contains(anchorEnd.id), "Route should include end anchor")
|
|
}
|
|
}
|
|
|
|
// MARK: - Property Tests
|
|
|
|
@Test("Property: route count never exceeds maxOptions (75)")
|
|
func property_routeCountNeverExceedsMax() {
|
|
let today = calendar.startOfDay(for: TestClock.now)
|
|
|
|
// Create many games to stress test
|
|
var games: [Game] = []
|
|
var stadiums: [String: Stadium] = [:]
|
|
|
|
for dayOffset in 0..<10 {
|
|
let date = calendar.date(byAdding: .day, value: dayOffset, to: today)!
|
|
for (city, coord) in [("NYC", nycCoord), ("BOS", bostonCoord), ("PHI", phillyCoord)] {
|
|
let (game, stadium) = makeGameAndStadium(city: "\(city)_\(dayOffset)", date: date, coord: coord)
|
|
games.append(game)
|
|
stadiums[stadium.id] = stadium
|
|
}
|
|
}
|
|
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: games,
|
|
stadiums: stadiums,
|
|
constraints: constraints
|
|
)
|
|
|
|
#expect(routes.count <= 75, "Should never exceed 75 routes")
|
|
}
|
|
|
|
@Test("Property: all routes satisfy constraints")
|
|
func property_allRoutesSatisfyConstraints() {
|
|
let today = calendar.startOfDay(for: TestClock.now)
|
|
let dates = (0..<5).map { calendar.date(byAdding: .day, value: $0, to: today)! }
|
|
|
|
let gamesAndStadiums = [
|
|
makeGameAndStadium(city: "New York", date: dates[0], coord: nycCoord),
|
|
makeGameAndStadium(city: "Philadelphia", date: dates[1], coord: phillyCoord),
|
|
makeGameAndStadium(city: "Boston", date: dates[2], coord: bostonCoord)
|
|
]
|
|
|
|
let games = gamesAndStadiums.map { $0.0 }
|
|
var stadiums: [String: Stadium] = [:]
|
|
for (_, stadium) in gamesAndStadiums {
|
|
stadiums[stadium.id] = stadium
|
|
}
|
|
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: games,
|
|
stadiums: stadiums,
|
|
constraints: constraints
|
|
)
|
|
|
|
// Every consecutive pair in every route should be feasible
|
|
for route in routes {
|
|
for i in 1..<route.count {
|
|
let from = route[i-1]
|
|
let to = route[i]
|
|
|
|
// Time must move forward
|
|
#expect(to.startTime > from.startTime, "Time must move forward")
|
|
|
|
// If different stadiums, distance must be reasonable
|
|
if from.stadiumId != to.stadiumId {
|
|
guard let fromStadium = stadiums[from.stadiumId],
|
|
let toStadium = stadiums[to.stadiumId] else {
|
|
Issue.record("Missing stadium data")
|
|
continue
|
|
}
|
|
|
|
let distance = TravelEstimator.haversineDistanceMiles(
|
|
from: CLLocationCoordinate2D(latitude: fromStadium.coordinate.latitude, longitude: fromStadium.coordinate.longitude),
|
|
to: CLLocationCoordinate2D(latitude: toStadium.coordinate.latitude, longitude: toStadium.coordinate.longitude)
|
|
) * 1.3
|
|
|
|
let daysAvailable = calendar.dateComponents([.day], from: from.startTime, to: to.startTime).day ?? 1
|
|
let maxMiles = Double(max(1, daysAvailable)) * constraints.maxDailyDrivingHours * 60 // 60 mph
|
|
|
|
#expect(distance <= maxMiles * 1.5, "Distance should be achievable") // Allow some buffer
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Edge Case Tests
|
|
|
|
@Test("Edge: games at same stadium always feasible")
|
|
func edge_sameStadium_alwaysFeasible() {
|
|
let today = calendar.startOfDay(for: TestClock.now)
|
|
let game1Time = calendar.date(bySettingHour: 13, minute: 0, second: 0, of: today)!
|
|
let game2Time = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: today)! // Doubleheader
|
|
|
|
let stadium = makeStadium(city: "New York", coord: nycCoord)
|
|
let game1 = makeGame(stadiumId: stadium.id, date: game1Time)
|
|
let game2 = makeGame(stadiumId: stadium.id, date: game2Time)
|
|
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: [stadium.id: stadium],
|
|
constraints: constraints
|
|
)
|
|
|
|
let bothGamesRoute = routes.first(where: { $0.count == 2 })
|
|
#expect(bothGamesRoute != nil, "Same stadium games should always be feasible")
|
|
}
|
|
|
|
@Test("Edge: games out of order are sorted chronologically")
|
|
func edge_unsortedGames_areSorted() {
|
|
let today = calendar.startOfDay(for: TestClock.now)
|
|
let game1Date = calendar.date(byAdding: .day, value: 2, to: today)!
|
|
let game2Date = today
|
|
let game3Date = calendar.date(byAdding: .day, value: 1, to: today)!
|
|
|
|
// Pass games out of order
|
|
let (game1, stadium1) = makeGameAndStadium(city: "City1", date: game1Date, coord: nycCoord)
|
|
let (game2, stadium2) = makeGameAndStadium(city: "City2", date: game2Date, coord: phillyCoord)
|
|
let (game3, stadium3) = makeGameAndStadium(city: "City3", date: game3Date, coord: bostonCoord)
|
|
|
|
let stadiums = [stadium1.id: stadium1, stadium2.id: stadium2, stadium3.id: stadium3]
|
|
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: [game1, game2, game3], // Out of order
|
|
stadiums: stadiums,
|
|
constraints: constraints
|
|
)
|
|
|
|
// All routes should be chronological
|
|
for route in routes {
|
|
for i in 1..<route.count {
|
|
#expect(route[i].startTime > route[i-1].startTime)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test("Edge: missing stadium for game is handled gracefully")
|
|
func edge_missingStadium_handledGracefully() {
|
|
let (game1, stadium1) = makeGameAndStadium(city: "New York", date: TestClock.now, coord: nycCoord)
|
|
let game2 = makeGame(stadiumId: "nonexistent-stadium", date: TestClock.now.addingTimeInterval(86400))
|
|
|
|
// Only provide stadium for game1
|
|
let stadiums = [stadium1.id: stadium1]
|
|
|
|
// Should not crash
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: stadiums,
|
|
constraints: constraints
|
|
)
|
|
|
|
// May return routes with just game1, or empty
|
|
#expect(routes.allSatisfy { route in
|
|
route.allSatisfy { game in stadiums[game.stadiumId] != nil || game.id == game2.id }
|
|
})
|
|
}
|
|
|
|
// MARK: - Helper Methods
|
|
|
|
private func makeGameAndStadium(
|
|
city: String,
|
|
date: Date,
|
|
coord: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 40.0, longitude: -74.0)
|
|
) -> (Game, Stadium) {
|
|
let stadiumId = "stadium-\(UUID().uuidString)"
|
|
let stadium = Stadium(
|
|
id: stadiumId,
|
|
name: "\(city) Stadium",
|
|
city: city,
|
|
state: "XX",
|
|
latitude: coord.latitude,
|
|
longitude: coord.longitude,
|
|
capacity: 40000,
|
|
sport: .mlb,
|
|
timeZoneIdentifier: "America/New_York"
|
|
)
|
|
let game = Game(
|
|
id: "game-\(UUID().uuidString)",
|
|
homeTeamId: "team1",
|
|
awayTeamId: "team2",
|
|
stadiumId: stadiumId,
|
|
dateTime: date,
|
|
sport: .mlb,
|
|
season: "2026",
|
|
isPlayoff: false
|
|
)
|
|
return (game, stadium)
|
|
}
|
|
|
|
private func makeStadium(
|
|
city: String,
|
|
coord: CLLocationCoordinate2D
|
|
) -> Stadium {
|
|
Stadium(
|
|
id: "stadium-\(UUID().uuidString)",
|
|
name: "\(city) Stadium",
|
|
city: city,
|
|
state: "XX",
|
|
latitude: coord.latitude,
|
|
longitude: coord.longitude,
|
|
capacity: 40000,
|
|
sport: .mlb,
|
|
timeZoneIdentifier: "America/New_York"
|
|
)
|
|
}
|
|
|
|
private func makeGame(
|
|
stadiumId: String,
|
|
date: Date
|
|
) -> Game {
|
|
Game(
|
|
id: "game-\(UUID().uuidString)",
|
|
homeTeamId: "team1",
|
|
awayTeamId: "team2",
|
|
stadiumId: stadiumId,
|
|
dateTime: date,
|
|
sport: .mlb,
|
|
season: "2026",
|
|
isPlayoff: false
|
|
)
|
|
}
|
|
}
|