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>
303 lines
12 KiB
Swift
303 lines
12 KiB
Swift
//
|
|
// ItineraryBuilderTests.swift
|
|
// SportsTimeTests
|
|
//
|
|
// Phase 8: ItineraryBuilder Tests
|
|
// Builds day-by-day itinerary from route with travel segments.
|
|
//
|
|
|
|
import Testing
|
|
import CoreLocation
|
|
@testable import SportsTime
|
|
|
|
@Suite("ItineraryBuilder Tests")
|
|
struct ItineraryBuilderTests {
|
|
|
|
// MARK: - Test Constants
|
|
|
|
private let defaultConstraints = DrivingConstraints.default
|
|
private let calendar = Calendar.current
|
|
|
|
// Known locations for testing
|
|
private let nyc = CLLocationCoordinate2D(latitude: 40.7128, longitude: -73.9352)
|
|
private let boston = CLLocationCoordinate2D(latitude: 42.3601, longitude: -71.0589)
|
|
private let chicago = CLLocationCoordinate2D(latitude: 41.8781, longitude: -87.6298)
|
|
private let la = CLLocationCoordinate2D(latitude: 34.0522, longitude: -118.2437)
|
|
|
|
// MARK: - 8.1 Single Game Creates Single Day
|
|
|
|
@Test("Single stop creates itinerary with one stop and no travel segments")
|
|
func test_builder_SingleGame_CreatesSingleDay() {
|
|
// Arrange
|
|
let gameId = UUID()
|
|
let stop = makeItineraryStop(
|
|
city: "New York",
|
|
state: "NY",
|
|
coordinate: nyc,
|
|
games: [gameId]
|
|
)
|
|
|
|
// Act
|
|
let result = ItineraryBuilder.build(
|
|
stops: [stop],
|
|
constraints: defaultConstraints
|
|
)
|
|
|
|
// Assert
|
|
#expect(result != nil, "Single stop should produce a valid itinerary")
|
|
|
|
if let itinerary = result {
|
|
#expect(itinerary.stops.count == 1, "Should have exactly 1 stop")
|
|
#expect(itinerary.travelSegments.isEmpty, "Should have no travel segments")
|
|
#expect(itinerary.totalDrivingHours == 0, "Should have 0 driving hours")
|
|
#expect(itinerary.totalDistanceMiles == 0, "Should have 0 distance")
|
|
}
|
|
}
|
|
|
|
// MARK: - 8.2 Multi-City Creates Travel Segments Between
|
|
|
|
@Test("Multiple cities creates travel segments between consecutive stops")
|
|
func test_builder_MultiCity_CreatesTravelSegmentsBetween() {
|
|
// Arrange
|
|
let stops = [
|
|
makeItineraryStop(city: "Boston", state: "MA", coordinate: boston, games: [UUID()]),
|
|
makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: [UUID()]),
|
|
makeItineraryStop(city: "Chicago", state: "IL", coordinate: chicago, games: [UUID()])
|
|
]
|
|
|
|
// Act
|
|
let result = ItineraryBuilder.build(
|
|
stops: stops,
|
|
constraints: defaultConstraints
|
|
)
|
|
|
|
// Assert
|
|
#expect(result != nil, "Multi-city trip should produce a valid itinerary")
|
|
|
|
if let itinerary = result {
|
|
#expect(itinerary.stops.count == 3, "Should have 3 stops")
|
|
#expect(itinerary.travelSegments.count == 2, "Should have 2 travel segments (stops - 1)")
|
|
|
|
// Verify segment 1: Boston -> NYC
|
|
let segment1 = itinerary.travelSegments[0]
|
|
#expect(segment1.fromLocation.name == "Boston", "First segment should start from Boston")
|
|
#expect(segment1.toLocation.name == "New York", "First segment should end at New York")
|
|
#expect(segment1.travelMode == .drive, "Travel mode should be drive")
|
|
#expect(segment1.distanceMeters > 0, "Distance should be positive")
|
|
#expect(segment1.durationSeconds > 0, "Duration should be positive")
|
|
|
|
// Verify segment 2: NYC -> Chicago
|
|
let segment2 = itinerary.travelSegments[1]
|
|
#expect(segment2.fromLocation.name == "New York", "Second segment should start from New York")
|
|
#expect(segment2.toLocation.name == "Chicago", "Second segment should end at Chicago")
|
|
|
|
// Verify totals are accumulated
|
|
#expect(itinerary.totalDrivingHours > 0, "Total driving hours should be positive")
|
|
#expect(itinerary.totalDistanceMiles > 0, "Total distance should be positive")
|
|
}
|
|
}
|
|
|
|
// MARK: - 8.3 Same City Multiple Games Groups On Same Day
|
|
|
|
@Test("Same city multiple stops have zero distance travel between them")
|
|
func test_builder_SameCity_MultipleGames_GroupsOnSameDay() {
|
|
// Arrange - Two stops in the same city (different games, same location)
|
|
let stops = [
|
|
makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: [UUID()]),
|
|
makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: [UUID()])
|
|
]
|
|
|
|
// Act
|
|
let result = ItineraryBuilder.build(
|
|
stops: stops,
|
|
constraints: defaultConstraints
|
|
)
|
|
|
|
// Assert
|
|
#expect(result != nil, "Same city stops should produce a valid itinerary")
|
|
|
|
if let itinerary = result {
|
|
#expect(itinerary.stops.count == 2, "Should have 2 stops")
|
|
#expect(itinerary.travelSegments.count == 1, "Should have 1 travel segment")
|
|
|
|
// Travel within same city should be minimal/zero distance
|
|
let segment = itinerary.travelSegments[0]
|
|
#expect(segment.estimatedDistanceMiles < 1.0,
|
|
"Same city travel should have near-zero distance, got \(segment.estimatedDistanceMiles)")
|
|
#expect(segment.estimatedDrivingHours < 0.1,
|
|
"Same city travel should have near-zero duration, got \(segment.estimatedDrivingHours)")
|
|
|
|
// Total driving should be minimal
|
|
#expect(itinerary.totalDrivingHours < 0.1,
|
|
"Total driving hours should be near zero for same city")
|
|
}
|
|
}
|
|
|
|
// MARK: - 8.4 Travel Days Inserted When Driving Exceeds 8 Hours
|
|
|
|
@Test("Multi-day driving is calculated correctly when exceeding 8 hours per day")
|
|
func test_builder_TravelDays_InsertedWhenDrivingExceeds8Hours() {
|
|
// Arrange - Create a trip that requires multi-day driving
|
|
// Boston to Chicago is ~850 miles haversine, ~1100 with road factor
|
|
// At 60 mph, that's ~18 hours = 3 travel days (ceil(18/8) = 3)
|
|
let stops = [
|
|
makeItineraryStop(city: "Boston", state: "MA", coordinate: boston, games: [UUID()]),
|
|
makeItineraryStop(city: "Chicago", state: "IL", coordinate: chicago, games: [UUID()])
|
|
]
|
|
|
|
// Use constraints that allow long trips
|
|
let constraints = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 8.0)
|
|
|
|
// Act
|
|
let result = ItineraryBuilder.build(
|
|
stops: stops,
|
|
constraints: constraints
|
|
)
|
|
|
|
// Assert
|
|
#expect(result != nil, "Long-distance trip should produce a valid itinerary")
|
|
|
|
if let itinerary = result {
|
|
// Get the travel segment
|
|
#expect(itinerary.travelSegments.count == 1, "Should have 1 travel segment")
|
|
|
|
let segment = itinerary.travelSegments[0]
|
|
let drivingHours = segment.estimatedDrivingHours
|
|
|
|
// Verify this is a multi-day drive
|
|
#expect(drivingHours > 8.0, "Boston to Chicago should require more than 8 hours driving")
|
|
|
|
// Calculate travel days using TravelEstimator
|
|
let travelDays = TravelEstimator.calculateTravelDays(
|
|
departure: Date(),
|
|
drivingHours: drivingHours
|
|
)
|
|
|
|
// Should span multiple days (ceil(hours/8))
|
|
let expectedDays = Int(ceil(drivingHours / 8.0))
|
|
#expect(travelDays.count == expectedDays,
|
|
"Travel should span \(expectedDays) days for \(drivingHours) hours driving, got \(travelDays.count)")
|
|
}
|
|
}
|
|
|
|
// MARK: - 8.5 Arrival Time Before Game Calculated
|
|
|
|
@Test("Segment validator rejects trips where arrival is after game start")
|
|
func test_builder_ArrivalTimeBeforeGame_Calculated() {
|
|
// Arrange - Create stops where travel time makes arriving on time impossible
|
|
let now = Date()
|
|
let gameStartSoon = now.addingTimeInterval(2 * 3600) // Game starts in 2 hours
|
|
|
|
let fromStop = makeItineraryStop(
|
|
city: "Boston",
|
|
state: "MA",
|
|
coordinate: boston,
|
|
games: [UUID()],
|
|
departureDate: now
|
|
)
|
|
|
|
// NYC game starts in 2 hours, but travel is ~4 hours
|
|
let toStop = makeItineraryStop(
|
|
city: "New York",
|
|
state: "NY",
|
|
coordinate: nyc,
|
|
games: [UUID()],
|
|
firstGameStart: gameStartSoon
|
|
)
|
|
|
|
// Use the arrival validator
|
|
let arrivalValidator = ItineraryBuilder.arrivalBeforeGameStart(bufferSeconds: 3600)
|
|
|
|
// Act
|
|
let result = ItineraryBuilder.build(
|
|
stops: [fromStop, toStop],
|
|
constraints: defaultConstraints,
|
|
segmentValidator: arrivalValidator
|
|
)
|
|
|
|
// Assert - Should return nil because we can't arrive 1 hour before game
|
|
// Boston to NYC is ~4 hours, game starts in 2 hours, need 1 hour buffer
|
|
// 4 hours travel > 2 hours - 1 hour buffer = 1 hour available
|
|
#expect(result == nil, "Should return nil when arrival would be after game start minus buffer")
|
|
}
|
|
|
|
@Test("Segment validator accepts trips where arrival is before game start")
|
|
func test_builder_ArrivalTimeBeforeGame_Succeeds() {
|
|
// Arrange - Create stops where there's plenty of time
|
|
let now = Date()
|
|
let gameLater = now.addingTimeInterval(10 * 3600) // Game in 10 hours
|
|
|
|
let fromStop = makeItineraryStop(
|
|
city: "Boston",
|
|
state: "MA",
|
|
coordinate: boston,
|
|
games: [UUID()],
|
|
departureDate: now
|
|
)
|
|
|
|
let toStop = makeItineraryStop(
|
|
city: "New York",
|
|
state: "NY",
|
|
coordinate: nyc,
|
|
games: [UUID()],
|
|
firstGameStart: gameLater
|
|
)
|
|
|
|
let arrivalValidator = ItineraryBuilder.arrivalBeforeGameStart(bufferSeconds: 3600)
|
|
|
|
// Act
|
|
let result = ItineraryBuilder.build(
|
|
stops: [fromStop, toStop],
|
|
constraints: defaultConstraints,
|
|
segmentValidator: arrivalValidator
|
|
)
|
|
|
|
// Assert - Should succeed, 4 hours travel leaves 5 hours before 10-hour deadline
|
|
#expect(result != nil, "Should return valid itinerary when arrival is well before game")
|
|
}
|
|
|
|
// MARK: - 8.6 Empty Route Returns Empty Itinerary
|
|
|
|
@Test("Empty stops array returns empty itinerary")
|
|
func test_builder_EmptyRoute_ReturnsEmptyItinerary() {
|
|
// Act
|
|
let result = ItineraryBuilder.build(
|
|
stops: [],
|
|
constraints: defaultConstraints
|
|
)
|
|
|
|
// Assert
|
|
#expect(result != nil, "Empty stops should still return a valid (empty) itinerary")
|
|
|
|
if let itinerary = result {
|
|
#expect(itinerary.stops.isEmpty, "Should have no stops")
|
|
#expect(itinerary.travelSegments.isEmpty, "Should have no travel segments")
|
|
#expect(itinerary.totalDrivingHours == 0, "Should have 0 driving hours")
|
|
#expect(itinerary.totalDistanceMiles == 0, "Should have 0 distance")
|
|
}
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func makeItineraryStop(
|
|
city: String,
|
|
state: String,
|
|
coordinate: CLLocationCoordinate2D? = nil,
|
|
games: [UUID] = [],
|
|
arrivalDate: Date = Date(),
|
|
departureDate: Date? = nil,
|
|
firstGameStart: Date? = nil
|
|
) -> ItineraryStop {
|
|
ItineraryStop(
|
|
city: city,
|
|
state: state,
|
|
coordinate: coordinate,
|
|
games: games,
|
|
arrivalDate: arrivalDate,
|
|
departureDate: departureDate ?? calendar.date(byAdding: .day, value: 1, to: arrivalDate)!,
|
|
location: LocationInput(name: city, coordinate: coordinate),
|
|
firstGameStart: firstGameStart
|
|
)
|
|
}
|
|
}
|