refactor(tests): TDD rewrite of all unit tests with spec documentation
Complete rewrite of unit test suite using TDD methodology: Planning Engine Tests: - GameDAGRouterTests: Beam search, anchor games, transitions - ItineraryBuilderTests: Stop connection, validators, EV enrichment - RouteFiltersTests: Region, time window, scoring filters - ScenarioA/B/C/D PlannerTests: All planning scenarios - TravelEstimatorTests: Distance, duration, travel days - TripPlanningEngineTests: Orchestration, caching, preferences Domain Model Tests: - AchievementDefinitionsTests, AnySportTests, DivisionTests - GameTests, ProgressTests, RegionTests, StadiumTests - TeamTests, TravelSegmentTests, TripTests, TripPollTests - TripPreferencesTests, TripStopTests, SportTests Service Tests: - FreeScoreAPITests, RouteDescriptionGeneratorTests - SuggestedTripsGeneratorTests Export Tests: - ShareableContentTests (card types, themes, dimensions) Bug fixes discovered through TDD: - ShareCardDimensions: mapSnapshotSize exceeded available width (960x480) - ScenarioBPlanner: Added anchor game validation filter All tests include: - Specification tests (expected behavior) - Invariant tests (properties that must always hold) - Edge case tests (boundary conditions) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,300 +2,367 @@
|
||||
// ItineraryBuilderTests.swift
|
||||
// SportsTimeTests
|
||||
//
|
||||
// Phase 8: ItineraryBuilder Tests
|
||||
// Builds day-by-day itinerary from route with travel segments.
|
||||
// TDD specification + property tests for ItineraryBuilder.
|
||||
//
|
||||
|
||||
import Testing
|
||||
import CoreLocation
|
||||
@testable import SportsTime
|
||||
|
||||
@Suite("ItineraryBuilder Tests")
|
||||
@Suite("ItineraryBuilder")
|
||||
struct ItineraryBuilderTests {
|
||||
|
||||
// MARK: - Test Constants
|
||||
// MARK: - Test Data
|
||||
|
||||
private let constraints = DrivingConstraints.default // 1 driver, 8 hrs/day
|
||||
|
||||
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 seattleCoord = CLLocationCoordinate2D(latitude: 47.5914, longitude: -122.3316)
|
||||
|
||||
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: - Specification Tests: build()
|
||||
|
||||
// MARK: - 8.1 Single Game Creates Single Day
|
||||
@Test("build: empty stops returns empty itinerary")
|
||||
func build_emptyStops_returnsEmptyItinerary() {
|
||||
let result = ItineraryBuilder.build(stops: [], constraints: constraints)
|
||||
|
||||
@Test("Single stop creates itinerary with one stop and no travel segments")
|
||||
func test_builder_SingleGame_CreatesSingleDay() {
|
||||
// Arrange
|
||||
let gameId = "game_test_\(UUID().uuidString)"
|
||||
let stop = makeItineraryStop(
|
||||
#expect(result != nil)
|
||||
#expect(result?.stops.isEmpty == true)
|
||||
#expect(result?.travelSegments.isEmpty == true)
|
||||
#expect(result?.totalDrivingHours == 0)
|
||||
#expect(result?.totalDistanceMiles == 0)
|
||||
}
|
||||
|
||||
@Test("build: single stop returns single-stop itinerary")
|
||||
func build_singleStop_returnsSingleStopItinerary() {
|
||||
let stop = makeStop(city: "New York", coordinate: nycCoord)
|
||||
|
||||
let result = ItineraryBuilder.build(stops: [stop], constraints: constraints)
|
||||
|
||||
#expect(result != nil)
|
||||
#expect(result?.stops.count == 1)
|
||||
#expect(result?.travelSegments.isEmpty == true)
|
||||
#expect(result?.totalDrivingHours == 0)
|
||||
#expect(result?.totalDistanceMiles == 0)
|
||||
}
|
||||
|
||||
@Test("build: two stops creates one segment")
|
||||
func build_twoStops_createsOneSegment() {
|
||||
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
||||
let stop2 = makeStop(city: "Boston", coordinate: bostonCoord)
|
||||
|
||||
let result = ItineraryBuilder.build(stops: [stop1, stop2], constraints: constraints)
|
||||
|
||||
#expect(result != nil)
|
||||
#expect(result?.stops.count == 2)
|
||||
#expect(result?.travelSegments.count == 1)
|
||||
#expect(result?.totalDrivingHours ?? 0 > 0)
|
||||
#expect(result?.totalDistanceMiles ?? 0 > 0)
|
||||
}
|
||||
|
||||
@Test("build: three stops creates two segments")
|
||||
func build_threeStops_createsTwoSegments() {
|
||||
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
||||
let stop2 = makeStop(city: "Philadelphia", coordinate: phillyCoord)
|
||||
let stop3 = makeStop(city: "Boston", coordinate: bostonCoord)
|
||||
|
||||
let result = ItineraryBuilder.build(stops: [stop1, stop2, stop3], constraints: constraints)
|
||||
|
||||
#expect(result != nil)
|
||||
#expect(result?.stops.count == 3)
|
||||
#expect(result?.travelSegments.count == 2)
|
||||
}
|
||||
|
||||
@Test("build: totalDrivingHours is sum of segments")
|
||||
func build_totalDrivingHours_isSumOfSegments() {
|
||||
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
||||
let stop2 = makeStop(city: "Philadelphia", coordinate: phillyCoord)
|
||||
let stop3 = makeStop(city: "Boston", coordinate: bostonCoord)
|
||||
|
||||
let result = ItineraryBuilder.build(stops: [stop1, stop2, stop3], constraints: constraints)!
|
||||
|
||||
let segmentHours = result.travelSegments.reduce(0.0) { $0 + $1.estimatedDrivingHours }
|
||||
#expect(abs(result.totalDrivingHours - segmentHours) < 0.01)
|
||||
}
|
||||
|
||||
@Test("build: totalDistanceMiles is sum of segments")
|
||||
func build_totalDistanceMiles_isSumOfSegments() {
|
||||
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
||||
let stop2 = makeStop(city: "Philadelphia", coordinate: phillyCoord)
|
||||
let stop3 = makeStop(city: "Boston", coordinate: bostonCoord)
|
||||
|
||||
let result = ItineraryBuilder.build(stops: [stop1, stop2, stop3], constraints: constraints)!
|
||||
|
||||
let segmentMiles = result.travelSegments.reduce(0.0) { $0 + $1.estimatedDistanceMiles }
|
||||
#expect(abs(result.totalDistanceMiles - segmentMiles) < 0.01)
|
||||
}
|
||||
|
||||
@Test("build: infeasible segment returns nil")
|
||||
func build_infeasibleSegment_returnsNil() {
|
||||
// NYC to Seattle is ~2850 miles, ~47 hours driving
|
||||
// With 1 driver at 8 hrs/day, max is 40 hours (5 days)
|
||||
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
||||
let stop2 = makeStop(city: "Seattle", coordinate: seattleCoord)
|
||||
|
||||
let result = ItineraryBuilder.build(stops: [stop1, stop2], constraints: constraints)
|
||||
|
||||
#expect(result == nil)
|
||||
}
|
||||
|
||||
@Test("build: feasible with more drivers succeeds")
|
||||
func build_feasibleWithMoreDrivers_succeeds() {
|
||||
// NYC to Seattle with 2 drivers: max is 80 hours (2*8*5)
|
||||
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
||||
let stop2 = makeStop(city: "Seattle", coordinate: seattleCoord)
|
||||
let twoDrivers = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 8.0)
|
||||
|
||||
let result = ItineraryBuilder.build(stops: [stop1, stop2], constraints: twoDrivers)
|
||||
|
||||
#expect(result != nil)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: Custom Validator
|
||||
|
||||
@Test("build: validator returning true allows segment")
|
||||
func build_validatorReturnsTrue_allowsSegment() {
|
||||
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
||||
let stop2 = makeStop(city: "Boston", coordinate: bostonCoord)
|
||||
|
||||
let alwaysValid: ItineraryBuilder.SegmentValidator = { _, _, _ in true }
|
||||
|
||||
let result = ItineraryBuilder.build(
|
||||
stops: [stop1, stop2],
|
||||
constraints: constraints,
|
||||
segmentValidator: alwaysValid
|
||||
)
|
||||
|
||||
#expect(result != nil)
|
||||
}
|
||||
|
||||
@Test("build: validator returning false rejects itinerary")
|
||||
func build_validatorReturnsFalse_rejectsItinerary() {
|
||||
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
||||
let stop2 = makeStop(city: "Boston", coordinate: bostonCoord)
|
||||
|
||||
let alwaysInvalid: ItineraryBuilder.SegmentValidator = { _, _, _ in false }
|
||||
|
||||
let result = ItineraryBuilder.build(
|
||||
stops: [stop1, stop2],
|
||||
constraints: constraints,
|
||||
segmentValidator: alwaysInvalid
|
||||
)
|
||||
|
||||
#expect(result == nil)
|
||||
}
|
||||
|
||||
@Test("build: validator receives correct stops")
|
||||
func build_validatorReceivesCorrectStops() {
|
||||
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
||||
let stop2 = makeStop(city: "Boston", coordinate: bostonCoord)
|
||||
|
||||
var capturedFromCity: String?
|
||||
var capturedToCity: String?
|
||||
|
||||
let captureValidator: ItineraryBuilder.SegmentValidator = { _, fromStop, toStop in
|
||||
capturedFromCity = fromStop.city
|
||||
capturedToCity = toStop.city
|
||||
return true
|
||||
}
|
||||
|
||||
_ = ItineraryBuilder.build(
|
||||
stops: [stop1, stop2],
|
||||
constraints: constraints,
|
||||
segmentValidator: captureValidator
|
||||
)
|
||||
|
||||
#expect(capturedFromCity == "New York")
|
||||
#expect(capturedToCity == "Boston")
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: arrivalBeforeGameStart Validator
|
||||
|
||||
@Test("arrivalBeforeGameStart: no game start time always passes")
|
||||
func arrivalBeforeGameStart_noGameStart_alwaysPasses() {
|
||||
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
||||
let stop2 = makeStop(city: "Boston", coordinate: bostonCoord, firstGameStart: nil)
|
||||
|
||||
let validator = ItineraryBuilder.arrivalBeforeGameStart(bufferSeconds: 3600)
|
||||
|
||||
let result = ItineraryBuilder.build(
|
||||
stops: [stop1, stop2],
|
||||
constraints: constraints,
|
||||
segmentValidator: validator
|
||||
)
|
||||
|
||||
#expect(result != nil)
|
||||
}
|
||||
|
||||
@Test("arrivalBeforeGameStart: sufficient time passes")
|
||||
func arrivalBeforeGameStart_sufficientTime_passes() {
|
||||
let now = Date()
|
||||
let tomorrow = calendar.date(byAdding: .day, value: 1, to: now)!
|
||||
let gameTime = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: tomorrow)!
|
||||
|
||||
let stop1 = makeStop(
|
||||
city: "New York",
|
||||
state: "NY",
|
||||
coordinate: nyc,
|
||||
games: [gameId]
|
||||
coordinate: nycCoord,
|
||||
departureDate: now
|
||||
)
|
||||
let stop2 = makeStop(
|
||||
city: "Boston",
|
||||
coordinate: bostonCoord,
|
||||
firstGameStart: gameTime
|
||||
)
|
||||
|
||||
// Act
|
||||
// NYC to Boston is ~4 hours, game is tomorrow at 7pm, plenty of time
|
||||
let validator = ItineraryBuilder.arrivalBeforeGameStart(bufferSeconds: 3600)
|
||||
|
||||
let result = ItineraryBuilder.build(
|
||||
stops: [stop],
|
||||
constraints: defaultConstraints
|
||||
stops: [stop1, stop2],
|
||||
constraints: constraints,
|
||||
segmentValidator: validator
|
||||
)
|
||||
|
||||
// Assert
|
||||
#expect(result != nil, "Single stop should produce a valid itinerary")
|
||||
#expect(result != nil)
|
||||
}
|
||||
|
||||
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")
|
||||
@Test("arrivalBeforeGameStart: insufficient time fails")
|
||||
func arrivalBeforeGameStart_insufficientTime_fails() {
|
||||
let now = Date()
|
||||
let gameTime = now.addingTimeInterval(2 * 3600) // Game in 2 hours
|
||||
|
||||
let stop1 = makeStop(
|
||||
city: "New York",
|
||||
coordinate: nycCoord,
|
||||
departureDate: now
|
||||
)
|
||||
let stop2 = makeStop(
|
||||
city: "Chicago", // ~13 hours away
|
||||
coordinate: chicagoCoord,
|
||||
firstGameStart: gameTime
|
||||
)
|
||||
|
||||
let validator = ItineraryBuilder.arrivalBeforeGameStart(bufferSeconds: 3600)
|
||||
|
||||
let result = ItineraryBuilder.build(
|
||||
stops: [stop1, stop2],
|
||||
constraints: constraints,
|
||||
segmentValidator: validator
|
||||
)
|
||||
|
||||
// Should fail because we can't get to Chicago in 2 hours
|
||||
// (assuming the segment is even feasible, which it isn't for 1 driver)
|
||||
// Either the segment is infeasible OR the validator rejects it
|
||||
#expect(result == nil)
|
||||
}
|
||||
|
||||
// MARK: - Property Tests
|
||||
|
||||
@Test("Property: segments count equals stops count minus one")
|
||||
func property_segmentsCountEqualsStopsMinusOne() {
|
||||
for count in [2, 3, 4, 5] {
|
||||
let stops = (0..<count).map { i in
|
||||
makeStop(city: "City\(i)", coordinate: nycCoord) // Same coord for all = always feasible
|
||||
}
|
||||
|
||||
if let result = ItineraryBuilder.build(stops: stops, constraints: constraints) {
|
||||
#expect(result.travelSegments.count == stops.count - 1,
|
||||
"For \(count) stops, should have \(count - 1) segments")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 8.2 Multi-City Creates Travel Segments Between
|
||||
|
||||
@Test("Multiple cities creates travel segments between consecutive stops")
|
||||
func test_builder_MultiCity_CreatesTravelSegmentsBetween() {
|
||||
// Arrange
|
||||
@Test("Property: totals are non-negative")
|
||||
func property_totalsNonNegative() {
|
||||
let stops = [
|
||||
makeItineraryStop(city: "Boston", state: "MA", coordinate: boston, games: ["game_boston_\(UUID().uuidString)"]),
|
||||
makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: ["game_nyc_\(UUID().uuidString)"]),
|
||||
makeItineraryStop(city: "Chicago", state: "IL", coordinate: chicago, games: ["game_chicago_\(UUID().uuidString)"])
|
||||
makeStop(city: "New York", coordinate: nycCoord),
|
||||
makeStop(city: "Philadelphia", coordinate: phillyCoord)
|
||||
]
|
||||
|
||||
// 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")
|
||||
if let result = ItineraryBuilder.build(stops: stops, constraints: constraints) {
|
||||
#expect(result.totalDrivingHours >= 0)
|
||||
#expect(result.totalDistanceMiles >= 0)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 8.3 Same City Multiple Games Groups On Same Day
|
||||
@Test("Property: empty/single stop always succeeds")
|
||||
func property_emptyOrSingleStopAlwaysSucceeds() {
|
||||
let emptyResult = ItineraryBuilder.build(stops: [], constraints: constraints)
|
||||
#expect(emptyResult != nil)
|
||||
|
||||
@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: ["game_nyc_1_\(UUID().uuidString)"]),
|
||||
makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: ["game_nyc_2_\(UUID().uuidString)"])
|
||||
]
|
||||
|
||||
// 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: ["game_boston_\(UUID().uuidString)"]),
|
||||
makeItineraryStop(city: "Chicago", state: "IL", coordinate: chicago, games: ["game_chicago_\(UUID().uuidString)"])
|
||||
]
|
||||
|
||||
// Use constraints that allow long trips
|
||||
let constraints = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 8.0)
|
||||
|
||||
// Act
|
||||
let result = ItineraryBuilder.build(
|
||||
stops: stops,
|
||||
let singleResult = ItineraryBuilder.build(
|
||||
stops: [makeStop(city: "NYC", coordinate: nycCoord)],
|
||||
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)")
|
||||
}
|
||||
#expect(singleResult != nil)
|
||||
}
|
||||
|
||||
// MARK: - 8.5 Arrival Time Before Game Calculated
|
||||
// MARK: - Edge Case Tests
|
||||
|
||||
@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
|
||||
@Test("Edge: stops with nil coordinates use fallback")
|
||||
func edge_nilCoordinates_useFallback() {
|
||||
let stop1 = makeStop(city: "City1", coordinate: nil)
|
||||
let stop2 = makeStop(city: "City2", coordinate: nil)
|
||||
|
||||
let fromStop = makeItineraryStop(
|
||||
city: "Boston",
|
||||
state: "MA",
|
||||
coordinate: boston,
|
||||
games: ["game_boston_\(UUID().uuidString)"],
|
||||
departureDate: now
|
||||
)
|
||||
let result = ItineraryBuilder.build(stops: [stop1, stop2], constraints: constraints)
|
||||
|
||||
// NYC game starts in 2 hours, but travel is ~4 hours
|
||||
let toStop = makeItineraryStop(
|
||||
city: "New York",
|
||||
state: "NY",
|
||||
coordinate: nyc,
|
||||
games: ["game_nyc_\(UUID().uuidString)"],
|
||||
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")
|
||||
// Should use fallback distance (300 miles)
|
||||
#expect(result != nil)
|
||||
#expect(result?.totalDistanceMiles ?? 0 > 0)
|
||||
}
|
||||
|
||||
@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
|
||||
@Test("Edge: same city stops have zero distance")
|
||||
func edge_sameCityStops_zeroDistance() {
|
||||
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
||||
let stop2 = makeStop(city: "New York", coordinate: nycCoord) // Same location
|
||||
|
||||
let fromStop = makeItineraryStop(
|
||||
city: "Boston",
|
||||
state: "MA",
|
||||
coordinate: boston,
|
||||
games: ["game_boston_\(UUID().uuidString)"],
|
||||
departureDate: now
|
||||
)
|
||||
let result = ItineraryBuilder.build(stops: [stop1, stop2], constraints: constraints)
|
||||
|
||||
let toStop = makeItineraryStop(
|
||||
city: "New York",
|
||||
state: "NY",
|
||||
coordinate: nyc,
|
||||
games: ["game_nyc_\(UUID().uuidString)"],
|
||||
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")
|
||||
#expect(result != nil)
|
||||
// Same coordinates should result in ~0 distance
|
||||
#expect(result?.totalDistanceMiles ?? 1000 < 1)
|
||||
}
|
||||
|
||||
// MARK: - 8.6 Empty Route Returns Empty Itinerary
|
||||
@Test("Edge: very long trip is still feasible with multiple drivers")
|
||||
func edge_veryLongTrip_feasibleWithMultipleDrivers() {
|
||||
// NYC -> Chicago -> Seattle
|
||||
let stops = [
|
||||
makeStop(city: "New York", coordinate: nycCoord),
|
||||
makeStop(city: "Chicago", coordinate: chicagoCoord),
|
||||
makeStop(city: "Seattle", coordinate: seattleCoord)
|
||||
]
|
||||
|
||||
@Test("Empty stops array returns empty itinerary")
|
||||
func test_builder_EmptyRoute_ReturnsEmptyItinerary() {
|
||||
// Act
|
||||
let result = ItineraryBuilder.build(
|
||||
stops: [],
|
||||
constraints: defaultConstraints
|
||||
)
|
||||
// With 3 drivers, max is 120 hours (3*8*5)
|
||||
let threeDrivers = DrivingConstraints(numberOfDrivers: 3, maxHoursPerDriverPerDay: 8.0)
|
||||
|
||||
// Assert
|
||||
#expect(result != nil, "Empty stops should still return a valid (empty) itinerary")
|
||||
let result = ItineraryBuilder.build(stops: stops, constraints: threeDrivers)
|
||||
|
||||
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")
|
||||
}
|
||||
#expect(result != nil)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func makeItineraryStop(
|
||||
private func makeStop(
|
||||
city: String,
|
||||
state: String,
|
||||
coordinate: CLLocationCoordinate2D? = nil,
|
||||
games: [String] = [],
|
||||
arrivalDate: Date = Date(),
|
||||
departureDate: Date? = nil,
|
||||
coordinate: CLLocationCoordinate2D?,
|
||||
departureDate: Date = Date(),
|
||||
firstGameStart: Date? = nil
|
||||
) -> ItineraryStop {
|
||||
ItineraryStop(
|
||||
city: city,
|
||||
state: state,
|
||||
state: "XX",
|
||||
coordinate: coordinate,
|
||||
games: games,
|
||||
arrivalDate: arrivalDate,
|
||||
departureDate: departureDate ?? calendar.date(byAdding: .day, value: 1, to: arrivalDate)!,
|
||||
location: LocationInput(name: city, coordinate: coordinate),
|
||||
games: [],
|
||||
arrivalDate: departureDate,
|
||||
departureDate: departureDate,
|
||||
location: LocationInput(
|
||||
name: city,
|
||||
coordinate: coordinate
|
||||
),
|
||||
firstGameStart: firstGameStart
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user