- Three-scenario planning engine (A: date range, B: selected games, C: directional routes) - GeographicRouteExplorer with anchor game support for route exploration - Shared ItineraryBuilder for travel segment calculation - TravelEstimator for driving time/distance estimation - SwiftUI views for trip creation and detail display - CloudKit integration for schedule data - Python scraping scripts for sports schedules 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
656 lines
22 KiB
Swift
656 lines
22 KiB
Swift
//
|
|
// ScenarioAPlannerTests.swift
|
|
// SportsTimeTests
|
|
//
|
|
// Tests for ScenarioAPlanner tree exploration logic.
|
|
// Verifies that we correctly find all geographically sensible route variations.
|
|
//
|
|
|
|
import XCTest
|
|
import CoreLocation
|
|
@testable import SportsTime
|
|
|
|
final class ScenarioAPlannerTests: XCTestCase {
|
|
|
|
// MARK: - Test Helpers
|
|
|
|
/// Creates a stadium at a specific coordinate
|
|
private func makeStadium(
|
|
id: UUID = UUID(),
|
|
city: String,
|
|
lat: Double,
|
|
lon: Double
|
|
) -> Stadium {
|
|
Stadium(
|
|
id: id,
|
|
name: "\(city) Arena",
|
|
city: city,
|
|
state: "ST",
|
|
latitude: lat,
|
|
longitude: lon,
|
|
capacity: 20000
|
|
)
|
|
}
|
|
|
|
/// Creates a game at a stadium on a specific day
|
|
private func makeGame(
|
|
stadiumId: UUID,
|
|
daysFromNow: Int
|
|
) -> Game {
|
|
let date = Calendar.current.date(byAdding: .day, value: daysFromNow, to: Date())!
|
|
return Game(
|
|
id: UUID(),
|
|
homeTeamId: UUID(),
|
|
awayTeamId: UUID(),
|
|
stadiumId: stadiumId,
|
|
dateTime: date,
|
|
sport: .nba,
|
|
season: "2025-26"
|
|
)
|
|
}
|
|
|
|
/// Creates a date range from now
|
|
private func makeDateRange(days: Int) -> DateInterval {
|
|
let start = Date()
|
|
let end = Calendar.current.date(byAdding: .day, value: days, to: start)!
|
|
return DateInterval(start: start, end: end)
|
|
}
|
|
|
|
/// Runs ScenarioA planning and returns the result
|
|
private func plan(
|
|
games: [Game],
|
|
stadiums: [Stadium],
|
|
dateRange: DateInterval
|
|
) -> ItineraryResult {
|
|
let stadiumDict = Dictionary(uniqueKeysWithValues: stadiums.map { ($0.id, $0) })
|
|
|
|
// Create preferences with the date range
|
|
let preferences = TripPreferences(
|
|
planningMode: .dateRange,
|
|
startDate: dateRange.start,
|
|
endDate: dateRange.end,
|
|
numberOfDrivers: 1,
|
|
maxDrivingHoursPerDriver: 8.0
|
|
)
|
|
|
|
let request = PlanningRequest(
|
|
preferences: preferences,
|
|
availableGames: games,
|
|
teams: [:], // Not needed for ScenarioA tests
|
|
stadiums: stadiumDict
|
|
)
|
|
|
|
let planner = ScenarioAPlanner()
|
|
return planner.plan(request: request)
|
|
}
|
|
|
|
// MARK: - Test 1: Empty games returns failure
|
|
|
|
func test_emptyGames_returnsNoGamesInRangeFailure() {
|
|
let result = plan(
|
|
games: [],
|
|
stadiums: [],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .failure(let failure) = result {
|
|
XCTAssertEqual(failure.reason, .noGamesInRange)
|
|
} else {
|
|
XCTFail("Expected failure, got success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 2: Single game always succeeds
|
|
|
|
func test_singleGame_alwaysSucceeds() {
|
|
let stadium = makeStadium(city: "New York", lat: 40.7, lon: -74.0)
|
|
let game = makeGame(stadiumId: stadium.id, daysFromNow: 1)
|
|
|
|
let result = plan(
|
|
games: [game],
|
|
stadiums: [stadium],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
XCTAssertEqual(options.count, 1)
|
|
XCTAssertEqual(options[0].stops.count, 1)
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 3: Two games always succeeds (no zig-zag possible)
|
|
|
|
func test_twoGames_alwaysSucceeds() {
|
|
let ny = makeStadium(city: "New York", lat: 40.7, lon: -74.0)
|
|
let la = makeStadium(city: "Los Angeles", lat: 34.0, lon: -118.2)
|
|
|
|
let games = [
|
|
makeGame(stadiumId: ny.id, daysFromNow: 1),
|
|
makeGame(stadiumId: la.id, daysFromNow: 3)
|
|
]
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: [ny, la],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
XCTAssertGreaterThanOrEqual(options.count, 1)
|
|
// Should have option with both games
|
|
let twoGameOption = options.first { $0.stops.count == 2 }
|
|
XCTAssertNotNil(twoGameOption)
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 4: Linear route (West to East) - all games included
|
|
|
|
func test_linearRouteWestToEast_allGamesIncluded() {
|
|
// LA → Denver → Chicago → New York (linear progression)
|
|
let la = makeStadium(city: "Los Angeles", lat: 34.0, lon: -118.2)
|
|
let den = makeStadium(city: "Denver", lat: 39.7, lon: -104.9)
|
|
let chi = makeStadium(city: "Chicago", lat: 41.8, lon: -87.6)
|
|
let ny = makeStadium(city: "New York", lat: 40.7, lon: -74.0)
|
|
|
|
let games = [
|
|
makeGame(stadiumId: la.id, daysFromNow: 1),
|
|
makeGame(stadiumId: den.id, daysFromNow: 3),
|
|
makeGame(stadiumId: chi.id, daysFromNow: 5),
|
|
makeGame(stadiumId: ny.id, daysFromNow: 7)
|
|
]
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: [la, den, chi, ny],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
// Best option should include all 4 games
|
|
XCTAssertEqual(options[0].stops.count, 4)
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 5: Linear route (North to South) - all games included
|
|
|
|
func test_linearRouteNorthToSouth_allGamesIncluded() {
|
|
// Seattle → SF → LA → San Diego (linear south)
|
|
let sea = makeStadium(city: "Seattle", lat: 47.6, lon: -122.3)
|
|
let sf = makeStadium(city: "San Francisco", lat: 37.7, lon: -122.4)
|
|
let la = makeStadium(city: "Los Angeles", lat: 34.0, lon: -118.2)
|
|
let sd = makeStadium(city: "San Diego", lat: 32.7, lon: -117.1)
|
|
|
|
let games = [
|
|
makeGame(stadiumId: sea.id, daysFromNow: 1),
|
|
makeGame(stadiumId: sf.id, daysFromNow: 2),
|
|
makeGame(stadiumId: la.id, daysFromNow: 3),
|
|
makeGame(stadiumId: sd.id, daysFromNow: 4)
|
|
]
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: [sea, sf, la, sd],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
XCTAssertEqual(options[0].stops.count, 4)
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 6: Zig-zag pattern creates multiple options (NY → TX → SC)
|
|
|
|
func test_zigZagPattern_createsMultipleOptions() {
|
|
// NY (day 1) → TX (day 2) → SC (day 3) = zig-zag
|
|
// Should create options: [NY,TX], [NY,SC], [TX,SC], etc.
|
|
let ny = makeStadium(city: "New York", lat: 40.7, lon: -74.0)
|
|
let tx = makeStadium(city: "Dallas", lat: 32.7, lon: -96.8)
|
|
let sc = makeStadium(city: "Charleston", lat: 32.7, lon: -79.9)
|
|
|
|
let games = [
|
|
makeGame(stadiumId: ny.id, daysFromNow: 1),
|
|
makeGame(stadiumId: tx.id, daysFromNow: 2),
|
|
makeGame(stadiumId: sc.id, daysFromNow: 3)
|
|
]
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: [ny, tx, sc],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
// Should have multiple options due to zig-zag
|
|
XCTAssertGreaterThan(options.count, 1)
|
|
} else {
|
|
XCTFail("Expected success with multiple options")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 7: Cross-country zig-zag creates many branches
|
|
|
|
func test_crossCountryZigZag_createsManyBranches() {
|
|
// NY → TX → SC → CA → MN = extreme zig-zag
|
|
let ny = makeStadium(city: "New York", lat: 40.7, lon: -74.0)
|
|
let tx = makeStadium(city: "Dallas", lat: 32.7, lon: -96.8)
|
|
let sc = makeStadium(city: "Charleston", lat: 32.7, lon: -79.9)
|
|
let ca = makeStadium(city: "Los Angeles", lat: 34.0, lon: -118.2)
|
|
let mn = makeStadium(city: "Minneapolis", lat: 44.9, lon: -93.2)
|
|
|
|
let games = [
|
|
makeGame(stadiumId: ny.id, daysFromNow: 1),
|
|
makeGame(stadiumId: tx.id, daysFromNow: 2),
|
|
makeGame(stadiumId: sc.id, daysFromNow: 3),
|
|
makeGame(stadiumId: ca.id, daysFromNow: 4),
|
|
makeGame(stadiumId: mn.id, daysFromNow: 5)
|
|
]
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: [ny, tx, sc, ca, mn],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
// Should have many options from all the branching
|
|
XCTAssertGreaterThan(options.count, 3)
|
|
// No option should have all 5 games (too much zig-zag)
|
|
let maxGames = options.map { $0.stops.count }.max() ?? 0
|
|
XCTAssertLessThan(maxGames, 5)
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 8: Fork at third game - both branches explored
|
|
|
|
func test_forkAtThirdGame_bothBranchesExplored() {
|
|
// NY → Chicago → ? (fork: either Dallas OR Miami, not both)
|
|
let ny = makeStadium(city: "New York", lat: 40.7, lon: -74.0)
|
|
let chi = makeStadium(city: "Chicago", lat: 41.8, lon: -87.6)
|
|
let dal = makeStadium(city: "Dallas", lat: 32.7, lon: -96.8)
|
|
let mia = makeStadium(city: "Miami", lat: 25.7, lon: -80.2)
|
|
|
|
let games = [
|
|
makeGame(stadiumId: ny.id, daysFromNow: 1),
|
|
makeGame(stadiumId: chi.id, daysFromNow: 2),
|
|
makeGame(stadiumId: dal.id, daysFromNow: 3),
|
|
makeGame(stadiumId: mia.id, daysFromNow: 4)
|
|
]
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: [ny, chi, dal, mia],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
// Should have options including Dallas and options including Miami
|
|
let citiesInOptions = options.flatMap { $0.stops.map { $0.city } }
|
|
XCTAssertTrue(citiesInOptions.contains("Dallas") || citiesInOptions.contains("Miami"))
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 9: All games in same city - single option with all games
|
|
|
|
func test_allGamesSameCity_singleOptionWithAllGames() {
|
|
let la = makeStadium(city: "Los Angeles", lat: 34.0, lon: -118.2)
|
|
|
|
let games = [
|
|
makeGame(stadiumId: la.id, daysFromNow: 1),
|
|
makeGame(stadiumId: la.id, daysFromNow: 2),
|
|
makeGame(stadiumId: la.id, daysFromNow: 3)
|
|
]
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: [la],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
// All games at same stadium = 1 stop with 3 games
|
|
XCTAssertEqual(options[0].stops.count, 1)
|
|
XCTAssertEqual(options[0].stops[0].games.count, 3)
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 10: Nearby cities - all included (no zig-zag)
|
|
|
|
func test_nearbyCities_allIncluded() {
|
|
// LA → Anaheim → San Diego (all nearby, < 100 miles)
|
|
let la = makeStadium(city: "Los Angeles", lat: 34.0, lon: -118.2)
|
|
let ana = makeStadium(city: "Anaheim", lat: 33.8, lon: -117.9)
|
|
let sd = makeStadium(city: "San Diego", lat: 32.7, lon: -117.1)
|
|
|
|
let games = [
|
|
makeGame(stadiumId: la.id, daysFromNow: 1),
|
|
makeGame(stadiumId: ana.id, daysFromNow: 2),
|
|
makeGame(stadiumId: sd.id, daysFromNow: 3)
|
|
]
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: [la, ana, sd],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
// All nearby = should have option with all 3
|
|
XCTAssertEqual(options[0].stops.count, 3)
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 11: Options sorted by game count (most games first)
|
|
|
|
func test_optionsSortedByGameCount_mostGamesFirst() {
|
|
// Create a scenario with varying option sizes
|
|
let ny = makeStadium(city: "New York", lat: 40.7, lon: -74.0)
|
|
let chi = makeStadium(city: "Chicago", lat: 41.8, lon: -87.6)
|
|
let la = makeStadium(city: "Los Angeles", lat: 34.0, lon: -118.2)
|
|
|
|
let games = [
|
|
makeGame(stadiumId: ny.id, daysFromNow: 1),
|
|
makeGame(stadiumId: chi.id, daysFromNow: 2),
|
|
makeGame(stadiumId: la.id, daysFromNow: 3)
|
|
]
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: [ny, chi, la],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
// Options should be sorted: most games first
|
|
for i in 0..<(options.count - 1) {
|
|
XCTAssertGreaterThanOrEqual(
|
|
options[i].stops.count,
|
|
options[i + 1].stops.count
|
|
)
|
|
}
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 12: Rank numbers are sequential
|
|
|
|
func test_rankNumbers_areSequential() {
|
|
let ny = makeStadium(city: "New York", lat: 40.7, lon: -74.0)
|
|
let tx = makeStadium(city: "Dallas", lat: 32.7, lon: -96.8)
|
|
let sc = makeStadium(city: "Charleston", lat: 32.7, lon: -79.9)
|
|
|
|
let games = [
|
|
makeGame(stadiumId: ny.id, daysFromNow: 1),
|
|
makeGame(stadiumId: tx.id, daysFromNow: 2),
|
|
makeGame(stadiumId: sc.id, daysFromNow: 3)
|
|
]
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: [ny, tx, sc],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
for (index, option) in options.enumerated() {
|
|
XCTAssertEqual(option.rank, index + 1)
|
|
}
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 13: Games outside date range are excluded
|
|
|
|
func test_gamesOutsideDateRange_areExcluded() {
|
|
let la = makeStadium(city: "Los Angeles", lat: 34.0, lon: -118.2)
|
|
|
|
let games = [
|
|
makeGame(stadiumId: la.id, daysFromNow: 1), // In range
|
|
makeGame(stadiumId: la.id, daysFromNow: 15), // Out of range
|
|
makeGame(stadiumId: la.id, daysFromNow: 3) // In range
|
|
]
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: [la],
|
|
dateRange: makeDateRange(days: 5) // Only 5 days
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
// Should only have 2 games (day 1 and day 3)
|
|
XCTAssertEqual(options[0].stops[0].games.count, 2)
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 14: Maximum 10 options returned
|
|
|
|
func test_maximum10Options_returned() {
|
|
// Create many cities that could generate lots of combinations
|
|
let cities: [(String, Double, Double)] = [
|
|
("City1", 40.0, -74.0),
|
|
("City2", 38.0, -90.0),
|
|
("City3", 35.0, -106.0),
|
|
("City4", 33.0, -117.0),
|
|
("City5", 37.0, -122.0),
|
|
("City6", 45.0, -93.0),
|
|
("City7", 42.0, -83.0)
|
|
]
|
|
|
|
let stadiums = cities.map { makeStadium(city: $0.0, lat: $0.1, lon: $0.2) }
|
|
let games = stadiums.enumerated().map { makeGame(stadiumId: $1.id, daysFromNow: $0 + 1) }
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: stadiums,
|
|
dateRange: makeDateRange(days: 15)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
XCTAssertLessThanOrEqual(options.count, 10)
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 15: Each option has travel segments
|
|
|
|
func test_eachOption_hasTravelSegments() {
|
|
let la = makeStadium(city: "Los Angeles", lat: 34.0, lon: -118.2)
|
|
let sf = makeStadium(city: "San Francisco", lat: 37.7, lon: -122.4)
|
|
let sea = makeStadium(city: "Seattle", lat: 47.6, lon: -122.3)
|
|
|
|
let games = [
|
|
makeGame(stadiumId: la.id, daysFromNow: 1),
|
|
makeGame(stadiumId: sf.id, daysFromNow: 3),
|
|
makeGame(stadiumId: sea.id, daysFromNow: 5)
|
|
]
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: [la, sf, sea],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
// Invariant: travelSegments.count == stops.count - 1
|
|
if option.stops.count > 1 {
|
|
XCTAssertEqual(
|
|
option.travelSegments.count,
|
|
option.stops.count - 1
|
|
)
|
|
}
|
|
}
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 16: Single game options are included
|
|
|
|
func test_singleGameOptions_areIncluded() {
|
|
let ny = makeStadium(city: "New York", lat: 40.7, lon: -74.0)
|
|
let la = makeStadium(city: "Los Angeles", lat: 34.0, lon: -118.2)
|
|
let mia = makeStadium(city: "Miami", lat: 25.7, lon: -80.2)
|
|
|
|
let games = [
|
|
makeGame(stadiumId: ny.id, daysFromNow: 1),
|
|
makeGame(stadiumId: la.id, daysFromNow: 2),
|
|
makeGame(stadiumId: mia.id, daysFromNow: 3)
|
|
]
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: [ny, la, mia],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
// Should include single-game options
|
|
let singleGameOptions = options.filter { $0.stops.count == 1 }
|
|
XCTAssertGreaterThan(singleGameOptions.count, 0)
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 17: Chronological order preserved in each option
|
|
|
|
func test_chronologicalOrder_preservedInEachOption() {
|
|
let la = makeStadium(city: "Los Angeles", lat: 34.0, lon: -118.2)
|
|
let den = makeStadium(city: "Denver", lat: 39.7, lon: -104.9)
|
|
let chi = makeStadium(city: "Chicago", lat: 41.8, lon: -87.6)
|
|
|
|
let games = [
|
|
makeGame(stadiumId: la.id, daysFromNow: 1),
|
|
makeGame(stadiumId: den.id, daysFromNow: 3),
|
|
makeGame(stadiumId: chi.id, daysFromNow: 5)
|
|
]
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: [la, den, chi],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
// Verify stops are in chronological order
|
|
for i in 0..<(option.stops.count - 1) {
|
|
XCTAssertLessThanOrEqual(
|
|
option.stops[i].arrivalDate,
|
|
option.stops[i + 1].arrivalDate
|
|
)
|
|
}
|
|
}
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 18: Geographic rationale includes city names
|
|
|
|
func test_geographicRationale_includesCityNames() {
|
|
let la = makeStadium(city: "Los Angeles", lat: 34.0, lon: -118.2)
|
|
let sf = makeStadium(city: "San Francisco", lat: 37.7, lon: -122.4)
|
|
|
|
let games = [
|
|
makeGame(stadiumId: la.id, daysFromNow: 1),
|
|
makeGame(stadiumId: sf.id, daysFromNow: 3)
|
|
]
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: [la, sf],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
let twoStopOption = options.first { $0.stops.count == 2 }
|
|
XCTAssertNotNil(twoStopOption)
|
|
XCTAssertTrue(twoStopOption!.geographicRationale.contains("Los Angeles"))
|
|
XCTAssertTrue(twoStopOption!.geographicRationale.contains("San Francisco"))
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 19: Total driving hours calculated for each option
|
|
|
|
func test_totalDrivingHours_calculatedForEachOption() {
|
|
let la = makeStadium(city: "Los Angeles", lat: 34.0, lon: -118.2)
|
|
let sf = makeStadium(city: "San Francisco", lat: 37.7, lon: -122.4)
|
|
|
|
let games = [
|
|
makeGame(stadiumId: la.id, daysFromNow: 1),
|
|
makeGame(stadiumId: sf.id, daysFromNow: 3)
|
|
]
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: [la, sf],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
let twoStopOption = options.first { $0.stops.count == 2 }
|
|
XCTAssertNotNil(twoStopOption)
|
|
// LA to SF is ~380 miles, ~6 hours
|
|
XCTAssertGreaterThan(twoStopOption!.totalDrivingHours, 4)
|
|
XCTAssertLessThan(twoStopOption!.totalDrivingHours, 10)
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test 20: Coastal route vs inland route - both explored
|
|
|
|
func test_coastalVsInlandRoute_bothExplored() {
|
|
// SF → either Sacramento (inland) or Monterey (coastal) → LA
|
|
let sf = makeStadium(city: "San Francisco", lat: 37.7, lon: -122.4)
|
|
let sac = makeStadium(city: "Sacramento", lat: 38.5, lon: -121.4) // Inland
|
|
let mon = makeStadium(city: "Monterey", lat: 36.6, lon: -121.9) // Coastal
|
|
let la = makeStadium(city: "Los Angeles", lat: 34.0, lon: -118.2)
|
|
|
|
let games = [
|
|
makeGame(stadiumId: sf.id, daysFromNow: 1),
|
|
makeGame(stadiumId: sac.id, daysFromNow: 2),
|
|
makeGame(stadiumId: mon.id, daysFromNow: 3),
|
|
makeGame(stadiumId: la.id, daysFromNow: 4)
|
|
]
|
|
|
|
let result = plan(
|
|
games: games,
|
|
stadiums: [sf, sac, mon, la],
|
|
dateRange: makeDateRange(days: 10)
|
|
)
|
|
|
|
if case .success(let options) = result {
|
|
let citiesInOptions = Set(options.flatMap { $0.stops.map { $0.city } })
|
|
// Both Sacramento and Monterey should appear in some option
|
|
XCTAssertTrue(citiesInOptions.contains("Sacramento") || citiesInOptions.contains("Monterey"))
|
|
} else {
|
|
XCTFail("Expected success")
|
|
}
|
|
}
|
|
}
|