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>
203 lines
8.0 KiB
Swift
203 lines
8.0 KiB
Swift
//
|
|
// TravelEstimatorTests.swift
|
|
// SportsTimeTests
|
|
//
|
|
// Phase 1: TravelEstimator Tests
|
|
// Foundation tests — all planners depend on this.
|
|
//
|
|
|
|
import Testing
|
|
import CoreLocation
|
|
@testable import SportsTime
|
|
|
|
@Suite("TravelEstimator Tests")
|
|
struct TravelEstimatorTests {
|
|
|
|
// MARK: - Test Constants
|
|
|
|
private let nyc = CLLocationCoordinate2D(latitude: 40.7128, longitude: -73.9352)
|
|
private let la = CLLocationCoordinate2D(latitude: 34.0522, longitude: -118.2437)
|
|
private let samePoint = CLLocationCoordinate2D(latitude: 40.7128, longitude: -73.9352)
|
|
|
|
// Antipodal point to NYC (roughly opposite side of Earth)
|
|
private let antipodal = CLLocationCoordinate2D(latitude: -40.7128, longitude: 106.0648)
|
|
|
|
// MARK: - 1.1 Haversine Known Distance
|
|
|
|
@Test("NYC to LA is approximately 2,451 miles (within 1% tolerance)")
|
|
func test_haversineDistanceMiles_KnownDistance() {
|
|
let distance = TravelEstimator.haversineDistanceMiles(from: nyc, to: la)
|
|
|
|
let expectedDistance = TestConstants.nycToLAMiles
|
|
let tolerance = expectedDistance * TestConstants.distanceTolerancePercent
|
|
|
|
#expect(abs(distance - expectedDistance) <= tolerance,
|
|
"Expected \(expectedDistance) ± \(tolerance) miles, got \(distance)")
|
|
}
|
|
|
|
// MARK: - 1.2 Same Point Returns Zero
|
|
|
|
@Test("Same point returns zero distance")
|
|
func test_haversineDistanceMiles_SamePoint_ReturnsZero() {
|
|
let distance = TravelEstimator.haversineDistanceMiles(from: nyc, to: samePoint)
|
|
|
|
#expect(distance == 0.0, "Expected 0.0 miles for same point, got \(distance)")
|
|
}
|
|
|
|
// MARK: - 1.3 Antipodal Distance
|
|
|
|
@Test("Antipodal points return approximately half Earth's circumference")
|
|
func test_haversineDistanceMiles_Antipodal_ReturnsHalfEarthCircumference() {
|
|
let distance = TravelEstimator.haversineDistanceMiles(from: nyc, to: antipodal)
|
|
|
|
// Half Earth circumference ≈ 12,450 miles
|
|
let halfCircumference = TestConstants.earthCircumferenceMiles / 2.0
|
|
let tolerance = halfCircumference * 0.05 // 5% tolerance for antipodal
|
|
|
|
#expect(abs(distance - halfCircumference) <= tolerance,
|
|
"Expected ~\(halfCircumference) miles for antipodal, got \(distance)")
|
|
}
|
|
|
|
// MARK: - 1.4 Nil Coordinates Returns Nil
|
|
|
|
@Test("Estimate returns nil when coordinates are missing")
|
|
func test_estimate_NilCoordinates_ReturnsNil() {
|
|
let fromLocation = LocationInput(name: "Unknown City", coordinate: nil)
|
|
let toLocation = LocationInput(name: "Another City", coordinate: nyc)
|
|
let constraints = DrivingConstraints.default
|
|
|
|
let result = TravelEstimator.estimate(from: fromLocation, to: toLocation, constraints: constraints)
|
|
|
|
#expect(result == nil, "Expected nil when from coordinate is missing")
|
|
|
|
// Also test when 'to' is nil
|
|
let fromWithCoord = LocationInput(name: "NYC", coordinate: nyc)
|
|
let toWithoutCoord = LocationInput(name: "Unknown", coordinate: nil)
|
|
|
|
let result2 = TravelEstimator.estimate(from: fromWithCoord, to: toWithoutCoord, constraints: constraints)
|
|
|
|
#expect(result2 == nil, "Expected nil when to coordinate is missing")
|
|
}
|
|
|
|
// MARK: - 1.5 Exceeds Max Daily Hours Returns Nil
|
|
|
|
@Test("Estimate returns nil when trip exceeds maximum allowed driving hours")
|
|
func test_estimate_ExceedsMaxDailyHours_ReturnsNil() {
|
|
// NYC to LA is ~2,451 miles
|
|
// At 60 mph, that's ~40.85 hours of driving
|
|
// With road routing factor of 1.3, actual route is ~3,186 miles = ~53 hours
|
|
// Max allowed is 2 days * 8 hours = 16 hours by default
|
|
// So this should return nil
|
|
|
|
let fromLocation = LocationInput(name: "NYC", coordinate: nyc)
|
|
let toLocation = LocationInput(name: "LA", coordinate: la)
|
|
let constraints = DrivingConstraints.default // 8 hours/day, 1 driver = 16 max
|
|
|
|
let result = TravelEstimator.estimate(from: fromLocation, to: toLocation, constraints: constraints)
|
|
|
|
#expect(result == nil, "Expected nil for trip exceeding max daily hours (NYC to LA with 16hr limit)")
|
|
}
|
|
|
|
// MARK: - 1.6 Valid Trip Returns Segment
|
|
|
|
@Test("Estimate returns valid segment for feasible trip")
|
|
func test_estimate_ValidTrip_ReturnsSegment() {
|
|
// Boston to NYC is ~215 miles (within 1 day driving)
|
|
let boston = CLLocationCoordinate2D(latitude: 42.3601, longitude: -71.0589)
|
|
|
|
let fromLocation = LocationInput(name: "Boston", coordinate: boston)
|
|
let toLocation = LocationInput(name: "NYC", coordinate: nyc)
|
|
let constraints = DrivingConstraints.default
|
|
|
|
let result = TravelEstimator.estimate(from: fromLocation, to: toLocation, constraints: constraints)
|
|
|
|
#expect(result != nil, "Expected a travel segment for Boston to NYC")
|
|
|
|
if let segment = result {
|
|
// Verify travel mode
|
|
#expect(segment.travelMode == .drive, "Expected drive mode")
|
|
|
|
// Distance should be reasonable (with road routing factor)
|
|
// Haversine Boston to NYC ≈ 190 miles, with 1.3 factor ≈ 247 miles
|
|
let expectedDistanceMeters = 190.0 * 1.3 * 1609.344 // miles to meters
|
|
let tolerance = expectedDistanceMeters * 0.15 // 15% tolerance
|
|
|
|
#expect(abs(segment.distanceMeters - expectedDistanceMeters) <= tolerance,
|
|
"Distance should be approximately \(expectedDistanceMeters) meters, got \(segment.distanceMeters)")
|
|
|
|
// Duration should be reasonable
|
|
// ~247 miles at 60 mph ≈ 4.1 hours = 14,760 seconds
|
|
#expect(segment.durationSeconds > 0, "Duration should be positive")
|
|
#expect(segment.durationSeconds < 8 * 3600, "Duration should be under 8 hours")
|
|
}
|
|
}
|
|
|
|
// MARK: - 1.7 Single Day Drive
|
|
|
|
@Test("4 hours of driving spans 1 day")
|
|
func test_calculateTravelDays_SingleDayDrive() {
|
|
let departure = Date()
|
|
let drivingHours = 4.0
|
|
|
|
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: drivingHours)
|
|
|
|
#expect(days.count == 1, "Expected 1 day for 4 hours of driving, got \(days.count)")
|
|
}
|
|
|
|
// MARK: - 1.8 Multi-Day Drive
|
|
|
|
@Test("20 hours of driving spans 3 days (ceil(20/8))")
|
|
func test_calculateTravelDays_MultiDayDrive() {
|
|
let departure = Date()
|
|
let drivingHours = 20.0
|
|
|
|
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: drivingHours)
|
|
|
|
// ceil(20/8) = 3 days
|
|
#expect(days.count == 3, "Expected 3 days for 20 hours of driving (ceil(20/8)), got \(days.count)")
|
|
}
|
|
|
|
// MARK: - 1.9 Fallback Distance Same City
|
|
|
|
@Test("Fallback distance returns 0 for same city")
|
|
func test_estimateFallbackDistance_SameCity_ReturnsZero() {
|
|
let stop1 = makeItineraryStop(city: "Chicago", state: "IL")
|
|
let stop2 = makeItineraryStop(city: "Chicago", state: "IL")
|
|
|
|
let distance = TravelEstimator.estimateFallbackDistance(from: stop1, to: stop2)
|
|
|
|
#expect(distance == 0.0, "Expected 0 miles for same city, got \(distance)")
|
|
}
|
|
|
|
// MARK: - 1.10 Fallback Distance Different City
|
|
|
|
@Test("Fallback distance returns 300 miles for different cities")
|
|
func test_estimateFallbackDistance_DifferentCity_Returns300() {
|
|
let stop1 = makeItineraryStop(city: "Chicago", state: "IL")
|
|
let stop2 = makeItineraryStop(city: "Milwaukee", state: "WI")
|
|
|
|
let distance = TravelEstimator.estimateFallbackDistance(from: stop1, to: stop2)
|
|
|
|
#expect(distance == 300.0, "Expected 300 miles for different cities, got \(distance)")
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func makeItineraryStop(
|
|
city: String,
|
|
state: String,
|
|
coordinate: CLLocationCoordinate2D? = nil
|
|
) -> ItineraryStop {
|
|
ItineraryStop(
|
|
city: city,
|
|
state: state,
|
|
coordinate: coordinate,
|
|
games: [],
|
|
arrivalDate: Date(),
|
|
departureDate: Date().addingTimeInterval(86400),
|
|
location: LocationInput(name: city, coordinate: coordinate),
|
|
firstGameStart: nil
|
|
)
|
|
}
|
|
}
|