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,190 +2,442 @@
|
||||
// TravelEstimatorTests.swift
|
||||
// SportsTimeTests
|
||||
//
|
||||
// Phase 1: TravelEstimator Tests
|
||||
// Foundation tests — all planners depend on this.
|
||||
// TDD specification + property tests for TravelEstimator.
|
||||
//
|
||||
|
||||
import Testing
|
||||
import CoreLocation
|
||||
@testable import SportsTime
|
||||
|
||||
@Suite("TravelEstimator Tests")
|
||||
@Suite("TravelEstimator")
|
||||
struct TravelEstimatorTests {
|
||||
|
||||
// MARK: - Test Constants
|
||||
// MARK: - Test Data
|
||||
|
||||
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)
|
||||
private let nyc = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
|
||||
private let boston = CLLocationCoordinate2D(latitude: 42.3467, longitude: -71.0972)
|
||||
private let chicago = CLLocationCoordinate2D(latitude: 41.8827, longitude: -87.6233)
|
||||
private let losAngeles = CLLocationCoordinate2D(latitude: 34.0141, longitude: -118.2879)
|
||||
private let seattle = CLLocationCoordinate2D(latitude: 47.5914, longitude: -122.3316)
|
||||
|
||||
// Antipodal point to NYC (roughly opposite side of Earth)
|
||||
private let antipodal = CLLocationCoordinate2D(latitude: -40.7128, longitude: 106.0648)
|
||||
private let defaultConstraints = DrivingConstraints.default // 1 driver, 8 hrs/day
|
||||
|
||||
// MARK: - 1.1 Haversine Known Distance
|
||||
// MARK: - Specification Tests: haversineDistanceMiles
|
||||
|
||||
@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)")
|
||||
@Test("haversineDistanceMiles: same point returns zero")
|
||||
func haversineDistanceMiles_samePoint_returnsZero() {
|
||||
let distance = TravelEstimator.haversineDistanceMiles(from: nyc, to: nyc)
|
||||
#expect(distance == 0)
|
||||
}
|
||||
|
||||
// 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)")
|
||||
@Test("haversineDistanceMiles: NYC to Boston approximately 190 miles")
|
||||
func haversineDistanceMiles_nycToBoston_approximately190() {
|
||||
let distance = TravelEstimator.haversineDistanceMiles(from: nyc, to: boston)
|
||||
// NYC to Boston is approximately 190 miles as the crow flies
|
||||
#expect(distance > 180 && distance < 200)
|
||||
}
|
||||
|
||||
// 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)")
|
||||
@Test("haversineDistanceMiles: NYC to LA approximately 2450 miles")
|
||||
func haversineDistanceMiles_nycToLA_approximately2450() {
|
||||
let distance = TravelEstimator.haversineDistanceMiles(from: nyc, to: losAngeles)
|
||||
// NYC to LA is approximately 2450 miles as the crow flies
|
||||
#expect(distance > 2400 && distance < 2500)
|
||||
}
|
||||
|
||||
// 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")
|
||||
@Test("haversineDistanceMiles: symmetric - distance(A,B) equals distance(B,A)")
|
||||
func haversineDistanceMiles_symmetric() {
|
||||
let distanceAB = TravelEstimator.haversineDistanceMiles(from: nyc, to: boston)
|
||||
let distanceBA = TravelEstimator.haversineDistanceMiles(from: boston, to: nyc)
|
||||
#expect(distanceAB == distanceBA)
|
||||
}
|
||||
|
||||
// MARK: - 1.5 Exceeds Max Daily Hours Returns Nil
|
||||
// MARK: - Specification Tests: haversineDistanceMeters
|
||||
|
||||
@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)")
|
||||
@Test("haversineDistanceMeters: same point returns zero")
|
||||
func haversineDistanceMeters_samePoint_returnsZero() {
|
||||
let distance = TravelEstimator.haversineDistanceMeters(from: nyc, to: nyc)
|
||||
#expect(distance == 0)
|
||||
}
|
||||
|
||||
// MARK: - 1.6 Valid Trip Returns Segment
|
||||
@Test("haversineDistanceMeters: consistent with miles calculation")
|
||||
func haversineDistanceMeters_consistentWithMiles() {
|
||||
let meters = TravelEstimator.haversineDistanceMeters(from: nyc, to: boston)
|
||||
let miles = TravelEstimator.haversineDistanceMiles(from: nyc, to: boston)
|
||||
|
||||
@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)
|
||||
// Convert meters to miles: 1 mile = 1609.34 meters
|
||||
let convertedMiles = meters / 1609.34
|
||||
#expect(abs(convertedMiles - miles) < 1.0) // Within 1 mile tolerance
|
||||
}
|
||||
|
||||
let fromLocation = LocationInput(name: "Boston", coordinate: boston)
|
||||
let toLocation = LocationInput(name: "NYC", coordinate: nyc)
|
||||
let constraints = DrivingConstraints.default
|
||||
// MARK: - Specification Tests: estimateFallbackDistance
|
||||
|
||||
let result = TravelEstimator.estimate(from: fromLocation, to: toLocation, constraints: constraints)
|
||||
@Test("estimateFallbackDistance: same city returns zero")
|
||||
func estimateFallbackDistance_sameCity_returnsZero() {
|
||||
let from = makeStop(city: "New York")
|
||||
let to = makeStop(city: "New York")
|
||||
|
||||
#expect(result != nil, "Expected a travel segment for Boston to NYC")
|
||||
let distance = TravelEstimator.estimateFallbackDistance(from: from, to: to)
|
||||
#expect(distance == 0)
|
||||
}
|
||||
|
||||
if let segment = result {
|
||||
// Verify travel mode
|
||||
#expect(segment.travelMode == .drive, "Expected drive mode")
|
||||
@Test("estimateFallbackDistance: different cities returns 300 miles")
|
||||
func estimateFallbackDistance_differentCities_returns300() {
|
||||
let from = makeStop(city: "New York")
|
||||
let to = makeStop(city: "Boston")
|
||||
|
||||
// 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
|
||||
let distance = TravelEstimator.estimateFallbackDistance(from: from, to: to)
|
||||
#expect(distance == 300)
|
||||
}
|
||||
|
||||
#expect(abs(segment.distanceMeters - expectedDistanceMeters) <= tolerance,
|
||||
"Distance should be approximately \(expectedDistanceMeters) meters, got \(segment.distanceMeters)")
|
||||
// MARK: - Specification Tests: calculateDistanceMiles
|
||||
|
||||
// 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")
|
||||
@Test("calculateDistanceMiles: with coordinates uses Haversine times routing factor")
|
||||
func calculateDistanceMiles_withCoordinates_usesHaversineTimesRoutingFactor() {
|
||||
let from = makeStop(city: "New York", coordinate: nyc)
|
||||
let to = makeStop(city: "Boston", coordinate: boston)
|
||||
|
||||
let distance = TravelEstimator.calculateDistanceMiles(from: from, to: to)
|
||||
let haversine = TravelEstimator.haversineDistanceMiles(from: nyc, to: boston)
|
||||
|
||||
// Road distance = Haversine * 1.3
|
||||
#expect(abs(distance - haversine * 1.3) < 0.1)
|
||||
}
|
||||
|
||||
@Test("calculateDistanceMiles: missing coordinates uses fallback")
|
||||
func calculateDistanceMiles_missingCoordinates_usesFallback() {
|
||||
let from = makeStop(city: "New York", coordinate: nil)
|
||||
let to = makeStop(city: "Boston", coordinate: nil)
|
||||
|
||||
let distance = TravelEstimator.calculateDistanceMiles(from: from, to: to)
|
||||
#expect(distance == 300) // Fallback distance
|
||||
}
|
||||
|
||||
@Test("calculateDistanceMiles: same city without coordinates returns zero")
|
||||
func calculateDistanceMiles_sameCityNoCoords_returnsZero() {
|
||||
let from = makeStop(city: "New York", coordinate: nil)
|
||||
let to = makeStop(city: "New York", coordinate: nil)
|
||||
|
||||
let distance = TravelEstimator.calculateDistanceMiles(from: from, to: to)
|
||||
#expect(distance == 0)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: estimate(ItineraryStop, ItineraryStop)
|
||||
|
||||
@Test("estimate: valid coordinates returns TravelSegment")
|
||||
func estimate_validCoordinates_returnsTravelSegment() {
|
||||
let from = makeStop(city: "New York", coordinate: nyc)
|
||||
let to = makeStop(city: "Boston", coordinate: boston)
|
||||
|
||||
let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints)
|
||||
|
||||
#expect(segment != nil)
|
||||
#expect(segment?.distanceMeters ?? 0 > 0)
|
||||
#expect(segment?.durationSeconds ?? 0 > 0)
|
||||
}
|
||||
|
||||
@Test("estimate: distance and duration are calculated correctly")
|
||||
func estimate_distanceAndDuration_calculatedCorrectly() {
|
||||
let from = makeStop(city: "New York", coordinate: nyc)
|
||||
let to = makeStop(city: "Boston", coordinate: boston)
|
||||
|
||||
let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints)!
|
||||
|
||||
let expectedMiles = TravelEstimator.calculateDistanceMiles(from: from, to: to)
|
||||
let expectedMeters = expectedMiles * 1609.34
|
||||
let expectedHours = expectedMiles / 60.0
|
||||
let expectedSeconds = expectedHours * 3600
|
||||
|
||||
#expect(abs(segment.distanceMeters - expectedMeters) < 100) // Within 100m
|
||||
#expect(abs(segment.durationSeconds - expectedSeconds) < 60) // Within 1 minute
|
||||
}
|
||||
|
||||
@Test("estimate: exceeding max driving hours returns nil")
|
||||
func estimate_exceedingMaxDrivingHours_returnsNil() {
|
||||
// NYC to Seattle is ~2850 miles, ~47.5 hours driving
|
||||
// With 1 driver at 8 hrs/day, max is 40 hours (5 days)
|
||||
let from = makeStop(city: "New York", coordinate: nyc)
|
||||
let to = makeStop(city: "Seattle", coordinate: seattle)
|
||||
|
||||
let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints)
|
||||
|
||||
#expect(segment == nil)
|
||||
}
|
||||
|
||||
@Test("estimate: within max driving hours with multiple drivers succeeds")
|
||||
func estimate_withinMaxWithMultipleDrivers_succeeds() {
|
||||
// NYC to Seattle with 2 drivers: max is 80 hours (2*8*5)
|
||||
let from = makeStop(city: "New York", coordinate: nyc)
|
||||
let to = makeStop(city: "Seattle", coordinate: seattle)
|
||||
let twoDrivers = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 8.0)
|
||||
|
||||
let segment = TravelEstimator.estimate(from: from, to: to, constraints: twoDrivers)
|
||||
|
||||
#expect(segment != nil)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: estimate(LocationInput, LocationInput)
|
||||
|
||||
@Test("estimate LocationInput: missing from coordinate returns nil")
|
||||
func estimateLocationInput_missingFromCoordinate_returnsNil() {
|
||||
let from = LocationInput(name: "Unknown", coordinate: nil)
|
||||
let to = LocationInput(name: "Boston", coordinate: boston)
|
||||
|
||||
let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints)
|
||||
|
||||
#expect(segment == nil)
|
||||
}
|
||||
|
||||
@Test("estimate LocationInput: missing to coordinate returns nil")
|
||||
func estimateLocationInput_missingToCoordinate_returnsNil() {
|
||||
let from = LocationInput(name: "New York", coordinate: nyc)
|
||||
let to = LocationInput(name: "Unknown", coordinate: nil)
|
||||
|
||||
let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints)
|
||||
|
||||
#expect(segment == nil)
|
||||
}
|
||||
|
||||
@Test("estimate LocationInput: valid coordinates returns TravelSegment")
|
||||
func estimateLocationInput_validCoordinates_returnsTravelSegment() {
|
||||
let from = LocationInput(name: "New York", coordinate: nyc)
|
||||
let to = LocationInput(name: "Boston", coordinate: boston)
|
||||
|
||||
let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints)
|
||||
|
||||
#expect(segment != nil)
|
||||
#expect(segment?.distanceMeters ?? 0 > 0)
|
||||
#expect(segment?.durationSeconds ?? 0 > 0)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: calculateTravelDays
|
||||
|
||||
@Test("calculateTravelDays: zero hours returns departure day only")
|
||||
func calculateTravelDays_zeroHours_returnsDepartureDay() {
|
||||
let departure = Date()
|
||||
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 0)
|
||||
|
||||
#expect(days.count == 1)
|
||||
}
|
||||
|
||||
@Test("calculateTravelDays: 1-8 hours returns single day")
|
||||
func calculateTravelDays_1to8Hours_returnsSingleDay() {
|
||||
let departure = Date()
|
||||
|
||||
for hours in [1.0, 4.0, 7.0, 8.0] {
|
||||
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: hours)
|
||||
#expect(days.count == 1, "Expected 1 day for \(hours) hours, got \(days.count)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 1.7 Single Day Drive
|
||||
|
||||
@Test("4 hours of driving spans 1 day")
|
||||
func test_calculateTravelDays_SingleDayDrive() {
|
||||
@Test("calculateTravelDays: 8.01-16 hours returns two days")
|
||||
func calculateTravelDays_8to16Hours_returnsTwoDays() {
|
||||
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)")
|
||||
for hours in [8.01, 12.0, 16.0] {
|
||||
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: hours)
|
||||
#expect(days.count == 2, "Expected 2 days for \(hours) hours, got \(days.count)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 1.8 Multi-Day Drive
|
||||
|
||||
@Test("20 hours of driving spans 3 days (ceil(20/8))")
|
||||
func test_calculateTravelDays_MultiDayDrive() {
|
||||
@Test("calculateTravelDays: 16.01-24 hours returns three days")
|
||||
func calculateTravelDays_16to24Hours_returnsThreeDays() {
|
||||
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)")
|
||||
for hours in [16.01, 20.0, 24.0] {
|
||||
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: hours)
|
||||
#expect(days.count == 3, "Expected 3 days for \(hours) hours, got \(days.count)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 1.9 Fallback Distance Same City
|
||||
@Test("calculateTravelDays: all dates are start of day")
|
||||
func calculateTravelDays_allDatesAreStartOfDay() {
|
||||
let calendar = Calendar.current
|
||||
// Use a specific time that's not midnight
|
||||
var components = calendar.dateComponents([.year, .month, .day], from: Date())
|
||||
components.hour = 14
|
||||
components.minute = 30
|
||||
let departure = calendar.date(from: components)!
|
||||
|
||||
@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 days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 20)
|
||||
|
||||
let distance = TravelEstimator.estimateFallbackDistance(from: stop1, to: stop2)
|
||||
|
||||
#expect(distance == 0.0, "Expected 0 miles for same city, got \(distance)")
|
||||
for day in days {
|
||||
let hour = calendar.component(.hour, from: day)
|
||||
let minute = calendar.component(.minute, from: day)
|
||||
#expect(hour == 0 && minute == 0, "Expected midnight, got \(hour):\(minute)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 1.10 Fallback Distance Different City
|
||||
@Test("calculateTravelDays: consecutive days")
|
||||
func calculateTravelDays_consecutiveDays() {
|
||||
let calendar = Calendar.current
|
||||
let departure = Date()
|
||||
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 24)
|
||||
|
||||
@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")
|
||||
#expect(days.count == 3)
|
||||
|
||||
let distance = TravelEstimator.estimateFallbackDistance(from: stop1, to: stop2)
|
||||
|
||||
#expect(distance == 300.0, "Expected 300 miles for different cities, got \(distance)")
|
||||
for i in 1..<days.count {
|
||||
let diff = calendar.dateComponents([.day], from: days[i-1], to: days[i])
|
||||
#expect(diff.day == 1, "Days should be consecutive")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
// MARK: - Property Tests
|
||||
|
||||
private func makeItineraryStop(
|
||||
@Test("Property: haversine distance is always non-negative")
|
||||
func property_haversineDistanceNonNegative() {
|
||||
let coordinates = [nyc, boston, chicago, losAngeles, seattle]
|
||||
|
||||
for from in coordinates {
|
||||
for to in coordinates {
|
||||
let distance = TravelEstimator.haversineDistanceMiles(from: from, to: to)
|
||||
#expect(distance >= 0, "Distance should be non-negative")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Property: haversine distance is symmetric")
|
||||
func property_haversineDistanceSymmetric() {
|
||||
let coordinates = [nyc, boston, chicago, losAngeles, seattle]
|
||||
|
||||
for from in coordinates {
|
||||
for to in coordinates {
|
||||
let distanceAB = TravelEstimator.haversineDistanceMiles(from: from, to: to)
|
||||
let distanceBA = TravelEstimator.haversineDistanceMiles(from: to, to: from)
|
||||
#expect(distanceAB == distanceBA, "Distance should be symmetric")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Property: triangle inequality holds")
|
||||
func property_triangleInequality() {
|
||||
// For any three points A, B, C: distance(A,C) <= distance(A,B) + distance(B,C)
|
||||
let a = nyc
|
||||
let b = chicago
|
||||
let c = losAngeles
|
||||
|
||||
let ac = TravelEstimator.haversineDistanceMiles(from: a, to: c)
|
||||
let ab = TravelEstimator.haversineDistanceMiles(from: a, to: b)
|
||||
let bc = TravelEstimator.haversineDistanceMiles(from: b, to: c)
|
||||
|
||||
#expect(ac <= ab + bc + 0.001, "Triangle inequality should hold")
|
||||
}
|
||||
|
||||
@Test("Property: road distance >= straight line distance")
|
||||
func property_roadDistanceGreaterThanStraightLine() {
|
||||
let from = makeStop(city: "New York", coordinate: nyc)
|
||||
let to = makeStop(city: "Boston", coordinate: boston)
|
||||
|
||||
let roadDistance = TravelEstimator.calculateDistanceMiles(from: from, to: to)
|
||||
let straightLine = TravelEstimator.haversineDistanceMiles(from: nyc, to: boston)
|
||||
|
||||
#expect(roadDistance >= straightLine, "Road distance should be >= straight line")
|
||||
}
|
||||
|
||||
@Test("Property: estimate duration proportional to distance")
|
||||
func property_durationProportionalToDistance() {
|
||||
let from = makeStop(city: "New York", coordinate: nyc)
|
||||
let to = makeStop(city: "Boston", coordinate: boston)
|
||||
let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints)!
|
||||
|
||||
// Duration should be distance / 60 mph
|
||||
let miles = segment.distanceMeters / 1609.34
|
||||
let expectedHours = miles / 60.0
|
||||
let actualHours = segment.durationSeconds / 3600.0
|
||||
|
||||
#expect(abs(actualHours - expectedHours) < 0.1, "Duration should be distance/60mph")
|
||||
}
|
||||
|
||||
@Test("Property: more drivers allows longer trips")
|
||||
func property_moreDriversAllowsLongerTrips() {
|
||||
// NYC to LA is ~2450 miles, ~53 hours driving with routing factor
|
||||
let from = makeStop(city: "New York", coordinate: nyc)
|
||||
let to = makeStop(city: "Los Angeles", coordinate: losAngeles)
|
||||
|
||||
let oneDriver = DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: 8.0)
|
||||
let twoDrivers = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 8.0)
|
||||
|
||||
let withOne = TravelEstimator.estimate(from: from, to: to, constraints: oneDriver)
|
||||
let withTwo = TravelEstimator.estimate(from: from, to: to, constraints: twoDrivers)
|
||||
|
||||
// With more drivers, trips that fail with one driver should succeed
|
||||
// (or both succeed/fail, but never one succeeds and more drivers fails)
|
||||
if withOne != nil {
|
||||
#expect(withTwo != nil, "More drivers should not reduce capability")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Edge Case Tests
|
||||
|
||||
@Test("Edge: antipodal points (maximum distance)")
|
||||
func edge_antipodalPoints() {
|
||||
// NYC to a point roughly opposite on Earth
|
||||
let antipode = CLLocationCoordinate2D(
|
||||
latitude: -nyc.latitude,
|
||||
longitude: nyc.longitude + 180
|
||||
)
|
||||
|
||||
let distance = TravelEstimator.haversineDistanceMiles(from: nyc, to: antipode)
|
||||
// Half Earth circumference is about 12,450 miles
|
||||
#expect(distance > 12000 && distance < 13000)
|
||||
}
|
||||
|
||||
@Test("Edge: very close points")
|
||||
func edge_veryClosePoints() {
|
||||
let nearby = CLLocationCoordinate2D(
|
||||
latitude: nyc.latitude + 0.0001,
|
||||
longitude: nyc.longitude + 0.0001
|
||||
)
|
||||
|
||||
let distance = TravelEstimator.haversineDistanceMiles(from: nyc, to: nearby)
|
||||
#expect(distance < 0.1, "Very close points should have near-zero distance")
|
||||
}
|
||||
|
||||
@Test("Edge: crossing prime meridian")
|
||||
func edge_crossingPrimeMeridian() {
|
||||
let london = CLLocationCoordinate2D(latitude: 51.5074, longitude: -0.1278)
|
||||
let paris = CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522)
|
||||
|
||||
let distance = TravelEstimator.haversineDistanceMiles(from: london, to: paris)
|
||||
// London to Paris is about 213 miles
|
||||
#expect(distance > 200 && distance < 230)
|
||||
}
|
||||
|
||||
@Test("Edge: crossing date line")
|
||||
func edge_crossingDateLine() {
|
||||
let tokyo = CLLocationCoordinate2D(latitude: 35.6762, longitude: 139.6503)
|
||||
let honolulu = CLLocationCoordinate2D(latitude: 21.3069, longitude: -157.8583)
|
||||
|
||||
let distance = TravelEstimator.haversineDistanceMiles(from: tokyo, to: honolulu)
|
||||
// Tokyo to Honolulu is about 3850 miles
|
||||
#expect(distance > 3700 && distance < 4000)
|
||||
}
|
||||
|
||||
@Test("Edge: calculateTravelDays with exactly 8 hours")
|
||||
func edge_calculateTravelDays_exactly8Hours() {
|
||||
let departure = Date()
|
||||
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 8.0)
|
||||
#expect(days.count == 1, "Exactly 8 hours should be 1 day")
|
||||
}
|
||||
|
||||
@Test("Edge: calculateTravelDays just over 8 hours")
|
||||
func edge_calculateTravelDays_justOver8Hours() {
|
||||
let departure = Date()
|
||||
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 8.001)
|
||||
#expect(days.count == 2, "Just over 8 hours should be 2 days")
|
||||
}
|
||||
|
||||
@Test("Edge: negative driving hours treated as minimum 1 day")
|
||||
func edge_negativeDrivingHours() {
|
||||
let departure = Date()
|
||||
let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: -5)
|
||||
#expect(days.count >= 1, "Negative hours should still return at least 1 day")
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func makeStop(
|
||||
city: String,
|
||||
state: String,
|
||||
state: String = "XX",
|
||||
coordinate: CLLocationCoordinate2D? = nil
|
||||
) -> ItineraryStop {
|
||||
ItineraryStop(
|
||||
@@ -194,7 +446,7 @@ struct TravelEstimatorTests {
|
||||
coordinate: coordinate,
|
||||
games: [],
|
||||
arrivalDate: Date(),
|
||||
departureDate: Date().addingTimeInterval(86400),
|
||||
departureDate: Date(),
|
||||
location: LocationInput(name: city, coordinate: coordinate),
|
||||
firstGameStart: nil
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user