Added 4 performance tests with 1K, 5K, 10K games to validate DAG algorithm scalability. Tests currently failing (RED phase). Tests: - 1K games: <2s expected - 5K games: <10s expected - 10K games: <30s expected - 10K games: memory stability Helper generateLargeDataset() creates realistic test data with distributed stadiums and games across time spans. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
647 lines
24 KiB
Swift
647 lines
24 KiB
Swift
//
|
|
// GameDAGRouterTests.swift
|
|
// SportsTimeTests
|
|
//
|
|
// TDD edge case tests for GameDAGRouter.
|
|
// Tests define correctness - code must match.
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
import CoreLocation
|
|
@testable import SportsTime
|
|
|
|
@Suite("GameDAGRouter Edge Case Tests")
|
|
struct GameDAGRouterTests {
|
|
|
|
// MARK: - Test Helpers
|
|
|
|
private func makeStadium(
|
|
id: UUID = UUID(),
|
|
city: String,
|
|
lat: Double = 34.0,
|
|
lon: Double = -118.0
|
|
) -> Stadium {
|
|
Stadium(
|
|
id: id,
|
|
name: "\(city) Stadium",
|
|
city: city,
|
|
state: "XX",
|
|
latitude: lat,
|
|
longitude: lon,
|
|
capacity: 40000,
|
|
sport: .mlb
|
|
)
|
|
}
|
|
|
|
private func makeGame(
|
|
id: UUID = UUID(),
|
|
stadiumId: UUID,
|
|
startTime: Date
|
|
) -> Game {
|
|
Game(
|
|
id: id,
|
|
homeTeamId: UUID(),
|
|
awayTeamId: UUID(),
|
|
stadiumId: stadiumId,
|
|
dateTime: startTime,
|
|
sport: .mlb,
|
|
season: "2026"
|
|
)
|
|
}
|
|
|
|
private func date(_ string: String) -> Date {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "yyyy-MM-dd HH:mm"
|
|
formatter.timeZone = TimeZone(identifier: "America/Los_Angeles")
|
|
return formatter.date(from: string)!
|
|
}
|
|
|
|
// MARK: - Standard Test Stadiums (spread across US)
|
|
|
|
private var losAngelesStadium: Stadium {
|
|
makeStadium(city: "Los Angeles", lat: 34.0739, lon: -118.2400)
|
|
}
|
|
|
|
private var sanFranciscoStadium: Stadium {
|
|
makeStadium(city: "San Francisco", lat: 37.7786, lon: -122.3893)
|
|
}
|
|
|
|
private var newYorkStadium: Stadium {
|
|
makeStadium(city: "New York", lat: 40.8296, lon: -73.9262)
|
|
}
|
|
|
|
private var chicagoStadium: Stadium {
|
|
makeStadium(city: "Chicago", lat: 41.9484, lon: -87.6553)
|
|
}
|
|
|
|
// MARK: - Test 1: Empty games array returns empty routes
|
|
|
|
@Test("Empty games array returns empty routes")
|
|
func findRoutes_EmptyGames_ReturnsEmpty() {
|
|
let result = GameDAGRouter.findRoutes(
|
|
games: [],
|
|
stadiums: [:],
|
|
constraints: .default
|
|
)
|
|
|
|
#expect(result.isEmpty)
|
|
}
|
|
|
|
// MARK: - Test 2: Single game returns that game
|
|
|
|
@Test("Single game returns single-game route")
|
|
func findRoutes_SingleGame_ReturnsSingleRoute() {
|
|
let stadium = losAngelesStadium
|
|
let game = makeGame(stadiumId: stadium.id, startTime: date("2026-06-15 19:00"))
|
|
|
|
let result = GameDAGRouter.findRoutes(
|
|
games: [game],
|
|
stadiums: [stadium.id: stadium],
|
|
constraints: .default
|
|
)
|
|
|
|
#expect(result.count == 1)
|
|
#expect(result.first?.count == 1)
|
|
#expect(result.first?.first?.id == game.id)
|
|
}
|
|
|
|
// MARK: - Test 3: Single game with non-matching anchor returns empty
|
|
|
|
@Test("Single game with non-matching anchor returns empty")
|
|
func findRoutes_SingleGame_NonMatchingAnchor_ReturnsEmpty() {
|
|
let stadium = losAngelesStadium
|
|
let game = makeGame(stadiumId: stadium.id, startTime: date("2026-06-15 19:00"))
|
|
let nonExistentAnchor = UUID()
|
|
|
|
let result = GameDAGRouter.findRoutes(
|
|
games: [game],
|
|
stadiums: [stadium.id: stadium],
|
|
constraints: .default,
|
|
anchorGameIds: [nonExistentAnchor]
|
|
)
|
|
|
|
#expect(result.isEmpty)
|
|
}
|
|
|
|
// MARK: - Test 4: Two games, chronological and feasible, returns route with both
|
|
|
|
@Test("Two chronological feasible games returns combined route")
|
|
func findRoutes_TwoGames_Chronological_Feasible_ReturnsCombined() {
|
|
let la = losAngelesStadium
|
|
let sf = sanFranciscoStadium
|
|
|
|
// LA to SF is ~380 miles, ~6 hours drive
|
|
// Games are 2 days apart - plenty of time
|
|
let game1 = makeGame(stadiumId: la.id, startTime: date("2026-06-15 19:00"))
|
|
let game2 = makeGame(stadiumId: sf.id, startTime: date("2026-06-17 19:00"))
|
|
|
|
let result = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: [la.id: la, sf.id: sf],
|
|
constraints: .default
|
|
)
|
|
|
|
// Should have route containing both games
|
|
let routeWithBoth = result.first { route in
|
|
route.count == 2 &&
|
|
route.contains { $0.id == game1.id } &&
|
|
route.contains { $0.id == game2.id }
|
|
}
|
|
|
|
#expect(routeWithBoth != nil, "Should have a route with both games")
|
|
}
|
|
|
|
// MARK: - Test 5: Two games, chronological but infeasible (too far), returns separate routes
|
|
|
|
@Test("Two games too far apart same day returns separate routes")
|
|
func findRoutes_TwoGames_TooFar_SameDay_ReturnsSeparate() {
|
|
let la = losAngelesStadium
|
|
let ny = newYorkStadium
|
|
|
|
// LA to NY is ~2800 miles - impossible in same day
|
|
let game1 = makeGame(stadiumId: la.id, startTime: date("2026-06-15 14:00"))
|
|
let game2 = makeGame(stadiumId: ny.id, startTime: date("2026-06-15 20:00"))
|
|
|
|
let result = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: [la.id: la, ny.id: ny],
|
|
constraints: .default
|
|
)
|
|
|
|
// Should return two separate single-game routes
|
|
#expect(result.count == 2, "Should return 2 separate routes")
|
|
|
|
let singleGameRoutes = result.filter { $0.count == 1 }
|
|
#expect(singleGameRoutes.count == 2, "Both routes should have single game")
|
|
}
|
|
|
|
// MARK: - Test 6: Two games, reverse chronological, returns separate routes
|
|
|
|
@Test("Two games reverse chronological returns separate routes")
|
|
func findRoutes_TwoGames_ReverseChronological_ReturnsSeparate() {
|
|
let la = losAngelesStadium
|
|
let sf = sanFranciscoStadium
|
|
|
|
// game2 starts BEFORE game1
|
|
let game1 = makeGame(stadiumId: la.id, startTime: date("2026-06-17 19:00"))
|
|
let game2 = makeGame(stadiumId: sf.id, startTime: date("2026-06-15 19:00"))
|
|
|
|
let result = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: [la.id: la, sf.id: sf],
|
|
constraints: .default
|
|
)
|
|
|
|
// With no anchors, it should return the combined route (sorted chronologically)
|
|
// OR separate routes if that's what the algorithm does
|
|
// The key point is we get some result, not empty
|
|
#expect(!result.isEmpty, "Should return at least one route")
|
|
|
|
// Check if combined route exists (sorted game2 -> game1)
|
|
let combined = result.first { route in
|
|
route.count == 2 &&
|
|
route[0].id == game2.id && // SF game (earlier)
|
|
route[1].id == game1.id // LA game (later)
|
|
}
|
|
|
|
if combined == nil {
|
|
// If no combined route, should have separate single-game routes
|
|
#expect(result.count >= 2, "Without combined route, should have separate routes")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 7: Three games where only pairs are feasible
|
|
|
|
@Test("Three games with only feasible pairs returns valid combinations")
|
|
func findRoutes_ThreeGames_OnlyPairsFeasible_ReturnsValidCombinations() {
|
|
let la = losAngelesStadium
|
|
let sf = sanFranciscoStadium
|
|
let ny = newYorkStadium
|
|
|
|
// Day 1: LA game
|
|
// Day 2: SF game (feasible from LA)
|
|
// Day 2: NY game (NOT feasible from LA same day)
|
|
let game1 = makeGame(stadiumId: la.id, startTime: date("2026-06-15 14:00"))
|
|
let game2 = makeGame(stadiumId: sf.id, startTime: date("2026-06-16 19:00"))
|
|
let game3 = makeGame(stadiumId: ny.id, startTime: date("2026-06-16 20:00"))
|
|
|
|
let result = GameDAGRouter.findRoutes(
|
|
games: [game1, game2, game3],
|
|
stadiums: [la.id: la, sf.id: sf, ny.id: ny],
|
|
constraints: .default
|
|
)
|
|
|
|
// Should return routes:
|
|
// - LA -> SF (feasible pair)
|
|
// - LA alone
|
|
// - SF alone
|
|
// - NY alone
|
|
// No LA -> NY same day (infeasible)
|
|
#expect(!result.isEmpty, "Should return at least one route")
|
|
|
|
// Verify no route has LA then NY on same day
|
|
for route in result {
|
|
let hasLA = route.contains { $0.stadiumId == la.id }
|
|
let hasNY = route.contains { $0.stadiumId == ny.id }
|
|
|
|
if hasLA && hasNY {
|
|
// If both, they shouldn't be consecutive on same day
|
|
let laIndex = route.firstIndex { $0.stadiumId == la.id }!
|
|
let nyIndex = route.firstIndex { $0.stadiumId == ny.id }!
|
|
|
|
if nyIndex == laIndex + 1 {
|
|
let laGame = route[laIndex]
|
|
let nyGame = route[nyIndex]
|
|
let calendar = Calendar.current
|
|
let sameDay = calendar.isDate(laGame.startTime, inSameDayAs: nyGame.startTime)
|
|
#expect(!sameDay, "LA -> NY same day should not be feasible")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 8: Anchor game filtering - routes missing anchors excluded
|
|
|
|
@Test("Routes missing anchor games are excluded")
|
|
func findRoutes_AnchorFiltering_ExcludesRoutesMissingAnchors() {
|
|
let la = losAngelesStadium
|
|
let sf = sanFranciscoStadium
|
|
|
|
let game1 = makeGame(stadiumId: la.id, startTime: date("2026-06-15 19:00"))
|
|
let game2 = makeGame(stadiumId: sf.id, startTime: date("2026-06-17 19:00"))
|
|
|
|
// Anchor on game2 - all routes must include it
|
|
let result = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: [la.id: la, sf.id: sf],
|
|
constraints: .default,
|
|
anchorGameIds: [game2.id]
|
|
)
|
|
|
|
// Every returned route must contain game2
|
|
for route in result {
|
|
let containsAnchor = route.contains { $0.id == game2.id }
|
|
#expect(containsAnchor, "Every route must contain anchor game")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 9: Repeat cities OFF - routes with same city twice excluded
|
|
|
|
@Test("Repeat cities OFF excludes routes visiting same city twice")
|
|
func findRoutes_RepeatCitiesOff_ExcludesSameCityTwice() {
|
|
let la1 = makeStadium(id: UUID(), city: "Los Angeles", lat: 34.0739, lon: -118.2400)
|
|
let la2 = makeStadium(id: UUID(), city: "Los Angeles", lat: 34.0140, lon: -118.2879)
|
|
let sf = sanFranciscoStadium
|
|
|
|
// Two stadiums in LA (different venues), one in SF
|
|
let game1 = makeGame(stadiumId: la1.id, startTime: date("2026-06-15 19:00"))
|
|
let game2 = makeGame(stadiumId: sf.id, startTime: date("2026-06-16 19:00"))
|
|
let game3 = makeGame(stadiumId: la2.id, startTime: date("2026-06-17 19:00"))
|
|
|
|
let result = GameDAGRouter.findRoutes(
|
|
games: [game1, game2, game3],
|
|
stadiums: [la1.id: la1, la2.id: la2, sf.id: sf],
|
|
constraints: .default,
|
|
allowRepeatCities: false
|
|
)
|
|
|
|
// No route should have both LA stadiums (same city)
|
|
for route in result {
|
|
let laCities = route.filter { game in
|
|
[la1.id, la2.id].contains(game.stadiumId)
|
|
}.count
|
|
#expect(laCities <= 1, "With repeat cities OFF, can't visit LA twice")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 10: Repeat cities ON - routes with same city twice included
|
|
|
|
@Test("Repeat cities ON allows routes visiting same city twice")
|
|
func findRoutes_RepeatCitiesOn_AllowsSameCityTwice() {
|
|
let la1 = makeStadium(id: UUID(), city: "Los Angeles", lat: 34.0739, lon: -118.2400)
|
|
let la2 = makeStadium(id: UUID(), city: "Los Angeles", lat: 34.0140, lon: -118.2879)
|
|
|
|
// Two games at different LA stadiums
|
|
let game1 = makeGame(stadiumId: la1.id, startTime: date("2026-06-15 19:00"))
|
|
let game2 = makeGame(stadiumId: la2.id, startTime: date("2026-06-17 19:00"))
|
|
|
|
let result = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: [la1.id: la1, la2.id: la2],
|
|
constraints: .default,
|
|
allowRepeatCities: true
|
|
)
|
|
|
|
// Should have a route with both games (both in LA)
|
|
let routeWithBoth = result.first { route in
|
|
route.count == 2 &&
|
|
route.contains { $0.id == game1.id } &&
|
|
route.contains { $0.id == game2.id }
|
|
}
|
|
|
|
#expect(routeWithBoth != nil, "With repeat cities ON, should allow both LA games")
|
|
}
|
|
|
|
// MARK: - canTransition Boundary Tests (via findRoutes behavior)
|
|
|
|
@Test("Same stadium same day 4 hours apart is feasible")
|
|
func findRoutes_SameStadium_SameDay_4HoursApart_Feasible() {
|
|
let stadium = losAngelesStadium
|
|
|
|
// Same stadium, 4 hours apart - should be feasible
|
|
let game1 = makeGame(stadiumId: stadium.id, startTime: date("2026-06-15 14:00"))
|
|
let game2 = makeGame(stadiumId: stadium.id, startTime: date("2026-06-15 20:00"))
|
|
|
|
let result = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: [stadium.id: stadium],
|
|
constraints: .default
|
|
)
|
|
|
|
// Should have a route with both games (same stadium is always feasible)
|
|
let routeWithBoth = result.first { route in
|
|
route.count == 2 &&
|
|
route.contains { $0.id == game1.id } &&
|
|
route.contains { $0.id == game2.id }
|
|
}
|
|
|
|
#expect(routeWithBoth != nil, "Same stadium transition should be feasible")
|
|
}
|
|
|
|
@Test("Different stadium 1000 miles apart same day is infeasible")
|
|
func findRoutes_DifferentStadium_1000Miles_SameDay_Infeasible() {
|
|
// LA to Chicago is ~1750 miles, way too far for same day
|
|
let la = losAngelesStadium
|
|
let chicago = chicagoStadium
|
|
|
|
// Same day, 6 hours apart - impossible to drive 1750 miles
|
|
let game1 = makeGame(stadiumId: la.id, startTime: date("2026-06-15 13:00"))
|
|
let game2 = makeGame(stadiumId: chicago.id, startTime: date("2026-06-15 20:00"))
|
|
|
|
let result = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: [la.id: la, chicago.id: chicago],
|
|
constraints: .default
|
|
)
|
|
|
|
// Should NOT have a combined route (too far for same day)
|
|
let routeWithBoth = result.first { route in
|
|
route.count == 2 &&
|
|
route.contains { $0.id == game1.id } &&
|
|
route.contains { $0.id == game2.id }
|
|
}
|
|
|
|
#expect(routeWithBoth == nil, "1750 miles same day should be infeasible")
|
|
}
|
|
|
|
@Test("Different stadium 1000 miles apart 2 days apart is feasible")
|
|
func findRoutes_DifferentStadium_1000Miles_2DaysApart_Feasible() {
|
|
// LA to Chicago is ~1750 miles, but 2 days gives 16 hours driving (at 60mph = 960 miles max)
|
|
// Actually need more time - let's use 3 days for 1750 miles at 60mph = ~29 hours
|
|
// 3 days * 8 hours/day = 24 hours driving - still not enough
|
|
// Use LA to SF (~380 miles) which is doable in 1-2 days
|
|
let la = losAngelesStadium
|
|
let sf = sanFranciscoStadium
|
|
|
|
// 2 days apart - 380 miles * 1.3 = 494 miles, at 60mph = 8.2 hours
|
|
// 2 days * 8 hours = 16 hours available - feasible
|
|
let game1 = makeGame(stadiumId: la.id, startTime: date("2026-06-15 19:00"))
|
|
let game2 = makeGame(stadiumId: sf.id, startTime: date("2026-06-17 19:00"))
|
|
|
|
let result = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: [la.id: la, sf.id: sf],
|
|
constraints: .default
|
|
)
|
|
|
|
let routeWithBoth = result.first { route in
|
|
route.count == 2 &&
|
|
route.contains { $0.id == game1.id } &&
|
|
route.contains { $0.id == game2.id }
|
|
}
|
|
|
|
#expect(routeWithBoth != nil, "380 miles with 2 days should be feasible")
|
|
}
|
|
|
|
@Test("Different stadium 100 miles apart 4 hours available is feasible")
|
|
func findRoutes_DifferentStadium_100Miles_4HoursAvailable_Feasible() {
|
|
// Create stadiums ~100 miles apart (roughly LA to San Diego distance)
|
|
let la = losAngelesStadium
|
|
let sanDiego = makeStadium(city: "San Diego", lat: 32.7076, lon: -117.1569)
|
|
|
|
// LA to San Diego is ~120 miles * 1.3 = 156 road miles, at 60mph = 2.6 hours
|
|
// Game 1 at 14:00, ends ~17:00 (3hr buffer), departure 17:00
|
|
// Game 2 at 21:00, must arrive by 20:00 (1hr buffer)
|
|
// Available: 3 hours - just enough for 2.6 hour drive
|
|
let game1 = makeGame(stadiumId: la.id, startTime: date("2026-06-15 14:00"))
|
|
let game2 = makeGame(stadiumId: sanDiego.id, startTime: date("2026-06-15 21:00"))
|
|
|
|
let result = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: [la.id: la, sanDiego.id: sanDiego],
|
|
constraints: .default
|
|
)
|
|
|
|
let routeWithBoth = result.first { route in
|
|
route.count == 2 &&
|
|
route.contains { $0.id == game1.id } &&
|
|
route.contains { $0.id == game2.id }
|
|
}
|
|
|
|
#expect(routeWithBoth != nil, "~120 miles with 4 hours available should be feasible")
|
|
}
|
|
|
|
@Test("Different stadium 100 miles apart 1 hour available is infeasible")
|
|
func findRoutes_DifferentStadium_100Miles_1HourAvailable_Infeasible() {
|
|
let la = losAngelesStadium
|
|
let sanDiego = makeStadium(city: "San Diego", lat: 32.7076, lon: -117.1569)
|
|
|
|
// Game 1 at 14:00, ends ~17:00 (3hr buffer)
|
|
// Game 2 at 18:00, must arrive by 17:00 (1hr buffer)
|
|
// Available: 0 hours - not enough for any driving
|
|
let game1 = makeGame(stadiumId: la.id, startTime: date("2026-06-15 14:00"))
|
|
let game2 = makeGame(stadiumId: sanDiego.id, startTime: date("2026-06-15 18:00"))
|
|
|
|
let result = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: [la.id: la, sanDiego.id: sanDiego],
|
|
constraints: .default
|
|
)
|
|
|
|
// Should NOT have a combined route (not enough time)
|
|
let routeWithBoth = result.first { route in
|
|
route.count == 2 &&
|
|
route.contains { $0.id == game1.id } &&
|
|
route.contains { $0.id == game2.id }
|
|
}
|
|
|
|
#expect(routeWithBoth == nil, "~120 miles with no available time should be infeasible")
|
|
}
|
|
|
|
@Test("Game end buffer - 3 hour buffer after game end before departure")
|
|
func findRoutes_GameEndBuffer_3Hours() {
|
|
let la = losAngelesStadium
|
|
let sanDiego = makeStadium(city: "San Diego", lat: 32.7076, lon: -117.1569)
|
|
|
|
// Game 1 at 14:00, ends + 3hr buffer = departure 17:00
|
|
// LA to SD: ~113 miles * 1.3 = 147 road miles, at 60mph = 2.45 hours
|
|
// Game 2 at 19:30 - arrival deadline 18:30 (1hr buffer)
|
|
// Available: 1.5 hours (17:00 to 18:30) - clearly infeasible for 2.45 hour drive
|
|
let game1 = makeGame(stadiumId: la.id, startTime: date("2026-06-15 14:00"))
|
|
let game2 = makeGame(stadiumId: sanDiego.id, startTime: date("2026-06-15 19:30"))
|
|
|
|
let result = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: [la.id: la, sanDiego.id: sanDiego],
|
|
constraints: .default
|
|
)
|
|
|
|
// With 3hr game end buffer: depart 17:00, arrive by 18:30 = 1.5 hours
|
|
// Need 2.45 hours driving - clearly infeasible
|
|
let routeWithBoth = result.first { route in
|
|
route.count == 2 &&
|
|
route.contains { $0.id == game1.id } &&
|
|
route.contains { $0.id == game2.id }
|
|
}
|
|
|
|
#expect(routeWithBoth == nil, "Only 1.5 hours available for 2.45 hour drive should be infeasible")
|
|
}
|
|
|
|
@Test("Arrival buffer - 1 hour buffer before next game start")
|
|
func findRoutes_ArrivalBuffer_1Hour() {
|
|
let la = losAngelesStadium
|
|
let sanDiego = makeStadium(city: "San Diego", lat: 32.7076, lon: -117.1569)
|
|
|
|
// Game 1 at 14:00, ends + 3hr buffer = depart 17:00
|
|
// Need ~2.6 hours driving
|
|
// Game 2 at 22:00 - arrival deadline 21:00
|
|
// Available: 4 hours (17:00 to 21:00) - feasible
|
|
let game1 = makeGame(stadiumId: la.id, startTime: date("2026-06-15 14:00"))
|
|
let game2 = makeGame(stadiumId: sanDiego.id, startTime: date("2026-06-15 22:00"))
|
|
|
|
let result = GameDAGRouter.findRoutes(
|
|
games: [game1, game2],
|
|
stadiums: [la.id: la, sanDiego.id: sanDiego],
|
|
constraints: .default
|
|
)
|
|
|
|
let routeWithBoth = result.first { route in
|
|
route.count == 2 &&
|
|
route.contains { $0.id == game1.id } &&
|
|
route.contains { $0.id == game2.id }
|
|
}
|
|
|
|
#expect(routeWithBoth != nil, "4 hours available (with 1hr arrival buffer) should be feasible")
|
|
}
|
|
|
|
// MARK: - Performance Tests
|
|
|
|
/// Generates a large dataset of games and stadiums for performance testing.
|
|
/// Games are distributed across stadiums and days to simulate realistic data.
|
|
private func generateLargeDataset(
|
|
gameCount: Int,
|
|
stadiumCount: Int,
|
|
daysSpan: Int
|
|
) -> (games: [Game], stadiums: [UUID: Stadium]) {
|
|
// Create stadiums distributed across the US (roughly)
|
|
var stadiums: [UUID: Stadium] = [:]
|
|
let baseDate = date("2026-06-01 19:00")
|
|
|
|
for i in 0..<stadiumCount {
|
|
// Distribute stadiums geographically (rough US coverage)
|
|
let lat = 30.0 + Double(i % 20) * 1.0 // 30-50 latitude
|
|
let lon = -120.0 + Double(i / 20) * 5.0 // -120 to -70 longitude
|
|
let stadium = makeStadium(
|
|
city: "City\(i)",
|
|
lat: lat,
|
|
lon: lon
|
|
)
|
|
stadiums[stadium.id] = stadium
|
|
}
|
|
|
|
// Create games distributed across stadiums and days
|
|
var games: [Game] = []
|
|
let stadiumIds = Array(stadiums.keys)
|
|
|
|
for i in 0..<gameCount {
|
|
// Distribute games across days
|
|
let dayOffset = Double(i * daysSpan) / Double(gameCount)
|
|
let hoursOffset = Double(i % 3) * 3.0 // Vary game times within day
|
|
let gameTime = baseDate.addingTimeInterval(dayOffset * 24 * 3600 + hoursOffset * 3600)
|
|
|
|
// Pick a stadium (cycle through them)
|
|
let stadiumId = stadiumIds[i % stadiumIds.count]
|
|
|
|
let game = makeGame(stadiumId: stadiumId, startTime: gameTime)
|
|
games.append(game)
|
|
}
|
|
|
|
return (games, stadiums)
|
|
}
|
|
|
|
@Test("Performance: 1000 games completes in under 2 seconds")
|
|
func performance_1000Games_CompletesInTime() {
|
|
let (games, stadiums) = generateLargeDataset(gameCount: 1000, stadiumCount: 50, daysSpan: 30)
|
|
|
|
let start = ContinuousClock.now
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: games,
|
|
stadiums: stadiums,
|
|
constraints: .default
|
|
)
|
|
let elapsed = start.duration(to: .now)
|
|
|
|
#expect(routes.count > 0, "Should return routes")
|
|
#expect(elapsed < .seconds(2), "Should complete within 2 seconds, actual: \(elapsed)")
|
|
}
|
|
|
|
@Test("Performance: 5000 games completes in under 10 seconds")
|
|
func performance_5000Games_CompletesInTime() {
|
|
let (games, stadiums) = generateLargeDataset(gameCount: 5000, stadiumCount: 100, daysSpan: 60)
|
|
|
|
let start = ContinuousClock.now
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: games,
|
|
stadiums: stadiums,
|
|
constraints: .default
|
|
)
|
|
let elapsed = start.duration(to: .now)
|
|
|
|
#expect(routes.count > 0, "Should return routes")
|
|
#expect(elapsed < .seconds(10), "Should complete within 10 seconds, actual: \(elapsed)")
|
|
}
|
|
|
|
@Test("Performance: 10000 games completes in under 30 seconds")
|
|
func performance_10000Games_CompletesInTime() {
|
|
let (games, stadiums) = generateLargeDataset(gameCount: 10000, stadiumCount: 150, daysSpan: 90)
|
|
|
|
let start = ContinuousClock.now
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: games,
|
|
stadiums: stadiums,
|
|
constraints: .default
|
|
)
|
|
let elapsed = start.duration(to: .now)
|
|
|
|
#expect(routes.count > 0, "Should return routes")
|
|
#expect(elapsed < .seconds(30), "Should complete within 30 seconds, actual: \(elapsed)")
|
|
}
|
|
|
|
@Test("Performance: 10000 games does not cause memory issues")
|
|
func performance_10000Games_NoMemorySpike() {
|
|
let (games, stadiums) = generateLargeDataset(gameCount: 10000, stadiumCount: 150, daysSpan: 90)
|
|
|
|
// Run the algorithm
|
|
let routes = GameDAGRouter.findRoutes(
|
|
games: games,
|
|
stadiums: stadiums,
|
|
constraints: .default
|
|
)
|
|
|
|
// Verify routes returned (not OOM)
|
|
#expect(routes.count > 0, "Should return routes without memory crash")
|
|
#expect(routes.count <= 100, "Should return reasonable number of routes")
|
|
}
|
|
}
|