test(planning): complete test suite with Phase 11 edge cases
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>
This commit is contained in:
305
SportsTimeTests/Helpers/BruteForceRouteVerifier.swift
Normal file
305
SportsTimeTests/Helpers/BruteForceRouteVerifier.swift
Normal file
@@ -0,0 +1,305 @@
|
||||
//
|
||||
// BruteForceRouteVerifier.swift
|
||||
// SportsTimeTests
|
||||
//
|
||||
// Exhaustively enumerates all route permutations to verify optimality.
|
||||
// Used for inputs with ≤8 stops where brute force is feasible.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
@testable import SportsTime
|
||||
|
||||
// MARK: - Route Verifier
|
||||
|
||||
struct BruteForceRouteVerifier {
|
||||
|
||||
// MARK: - Route Comparison Result
|
||||
|
||||
struct VerificationResult {
|
||||
let isOptimal: Bool
|
||||
let proposedRouteDistance: Double
|
||||
let optimalRouteDistance: Double
|
||||
let optimalRoute: [UUID]?
|
||||
let improvement: Double? // Percentage improvement if not optimal
|
||||
let permutationsChecked: Int
|
||||
|
||||
var improvementPercentage: Double? {
|
||||
guard let improvement = improvement else { return nil }
|
||||
return improvement * 100
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Verification
|
||||
|
||||
/// Verify that a proposed route is optimal (or near-optimal) by checking all permutations
|
||||
/// - Parameters:
|
||||
/// - proposedRoute: The route to verify (ordered list of stop IDs)
|
||||
/// - stops: Dictionary mapping stop IDs to their coordinates
|
||||
/// - tolerance: Percentage tolerance for "near-optimal" (default 0 = must be exactly optimal)
|
||||
/// - Returns: Verification result
|
||||
static func verify(
|
||||
proposedRoute: [UUID],
|
||||
stops: [UUID: CLLocationCoordinate2D],
|
||||
tolerance: Double = 0
|
||||
) -> VerificationResult {
|
||||
guard proposedRoute.count <= TestConstants.bruteForceMaxStops else {
|
||||
fatalError("BruteForceRouteVerifier should only be used for ≤\(TestConstants.bruteForceMaxStops) stops")
|
||||
}
|
||||
|
||||
guard proposedRoute.count >= 2 else {
|
||||
// Single stop or empty - trivially optimal
|
||||
return VerificationResult(
|
||||
isOptimal: true,
|
||||
proposedRouteDistance: 0,
|
||||
optimalRouteDistance: 0,
|
||||
optimalRoute: proposedRoute,
|
||||
improvement: nil,
|
||||
permutationsChecked: 1
|
||||
)
|
||||
}
|
||||
|
||||
let proposedDistance = calculateRouteDistance(proposedRoute, stops: stops)
|
||||
|
||||
// Find optimal route by checking all permutations
|
||||
let allPermutations = permutations(of: proposedRoute)
|
||||
var optimalDistance = Double.infinity
|
||||
var optimalRoute: [UUID] = []
|
||||
|
||||
for permutation in allPermutations {
|
||||
let distance = calculateRouteDistance(permutation, stops: stops)
|
||||
if distance < optimalDistance {
|
||||
optimalDistance = distance
|
||||
optimalRoute = permutation
|
||||
}
|
||||
}
|
||||
|
||||
let isOptimal: Bool
|
||||
var improvement: Double? = nil
|
||||
|
||||
if tolerance == 0 {
|
||||
// Exact optimality check with floating point tolerance
|
||||
isOptimal = abs(proposedDistance - optimalDistance) < 0.001
|
||||
} else {
|
||||
// Within tolerance
|
||||
let maxAllowed = optimalDistance * (1 + tolerance)
|
||||
isOptimal = proposedDistance <= maxAllowed
|
||||
}
|
||||
|
||||
if !isOptimal && optimalDistance > 0 {
|
||||
improvement = (proposedDistance - optimalDistance) / optimalDistance
|
||||
}
|
||||
|
||||
return VerificationResult(
|
||||
isOptimal: isOptimal,
|
||||
proposedRouteDistance: proposedDistance,
|
||||
optimalRouteDistance: optimalDistance,
|
||||
optimalRoute: optimalRoute,
|
||||
improvement: improvement,
|
||||
permutationsChecked: allPermutations.count
|
||||
)
|
||||
}
|
||||
|
||||
/// Verify a route is optimal with a fixed start and end point
|
||||
static func verifyWithFixedEndpoints(
|
||||
proposedRoute: [UUID],
|
||||
stops: [UUID: CLLocationCoordinate2D],
|
||||
startId: UUID,
|
||||
endId: UUID,
|
||||
tolerance: Double = 0
|
||||
) -> VerificationResult {
|
||||
guard proposedRoute.first == startId && proposedRoute.last == endId else {
|
||||
// Invalid route - doesn't match required endpoints
|
||||
return VerificationResult(
|
||||
isOptimal: false,
|
||||
proposedRouteDistance: Double.infinity,
|
||||
optimalRouteDistance: 0,
|
||||
optimalRoute: nil,
|
||||
improvement: nil,
|
||||
permutationsChecked: 0
|
||||
)
|
||||
}
|
||||
|
||||
// Get intermediate stops (excluding start and end)
|
||||
let intermediateStops = proposedRoute.dropFirst().dropLast()
|
||||
|
||||
guard intermediateStops.count <= TestConstants.bruteForceMaxStops - 2 else {
|
||||
fatalError("BruteForceRouteVerifier: too many intermediate stops")
|
||||
}
|
||||
|
||||
let proposedDistance = calculateRouteDistance(proposedRoute, stops: stops)
|
||||
|
||||
// Generate all permutations of intermediate stops
|
||||
let allPermutations = permutations(of: Array(intermediateStops))
|
||||
var optimalDistance = Double.infinity
|
||||
var optimalRoute: [UUID] = []
|
||||
|
||||
for permutation in allPermutations {
|
||||
var fullRoute = [startId]
|
||||
fullRoute.append(contentsOf: permutation)
|
||||
fullRoute.append(endId)
|
||||
|
||||
let distance = calculateRouteDistance(fullRoute, stops: stops)
|
||||
if distance < optimalDistance {
|
||||
optimalDistance = distance
|
||||
optimalRoute = fullRoute
|
||||
}
|
||||
}
|
||||
|
||||
let isOptimal: Bool
|
||||
var improvement: Double? = nil
|
||||
|
||||
if tolerance == 0 {
|
||||
isOptimal = abs(proposedDistance - optimalDistance) < 0.001
|
||||
} else {
|
||||
let maxAllowed = optimalDistance * (1 + tolerance)
|
||||
isOptimal = proposedDistance <= maxAllowed
|
||||
}
|
||||
|
||||
if !isOptimal && optimalDistance > 0 {
|
||||
improvement = (proposedDistance - optimalDistance) / optimalDistance
|
||||
}
|
||||
|
||||
return VerificationResult(
|
||||
isOptimal: isOptimal,
|
||||
proposedRouteDistance: proposedDistance,
|
||||
optimalRouteDistance: optimalDistance,
|
||||
optimalRoute: optimalRoute,
|
||||
improvement: improvement,
|
||||
permutationsChecked: allPermutations.count
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if there's an obviously better route (significantly shorter)
|
||||
static func hasObviouslyBetterRoute(
|
||||
proposedRoute: [UUID],
|
||||
stops: [UUID: CLLocationCoordinate2D],
|
||||
threshold: Double = 0.1 // 10% improvement threshold
|
||||
) -> (hasBetter: Bool, improvement: Double?) {
|
||||
let result = verify(proposedRoute: proposedRoute, stops: stops, tolerance: threshold)
|
||||
return (!result.isOptimal, result.improvement)
|
||||
}
|
||||
|
||||
// MARK: - Distance Calculation
|
||||
|
||||
/// Calculate total route distance using haversine formula
|
||||
static func calculateRouteDistance(
|
||||
_ route: [UUID],
|
||||
stops: [UUID: CLLocationCoordinate2D]
|
||||
) -> Double {
|
||||
guard route.count >= 2 else { return 0 }
|
||||
|
||||
var totalDistance: Double = 0
|
||||
|
||||
for i in 0..<(route.count - 1) {
|
||||
guard let from = stops[route[i]],
|
||||
let to = stops[route[i + 1]] else {
|
||||
continue
|
||||
}
|
||||
totalDistance += haversineDistanceMiles(from: from, to: to)
|
||||
}
|
||||
|
||||
return totalDistance
|
||||
}
|
||||
|
||||
/// Haversine distance between two coordinates in miles
|
||||
static func haversineDistanceMiles(
|
||||
from: CLLocationCoordinate2D,
|
||||
to: CLLocationCoordinate2D
|
||||
) -> Double {
|
||||
let earthRadiusMiles = TestConstants.earthRadiusMiles
|
||||
|
||||
let lat1 = from.latitude * .pi / 180
|
||||
let lat2 = to.latitude * .pi / 180
|
||||
let deltaLat = (to.latitude - from.latitude) * .pi / 180
|
||||
let deltaLon = (to.longitude - from.longitude) * .pi / 180
|
||||
|
||||
let a = sin(deltaLat / 2) * sin(deltaLat / 2) +
|
||||
cos(lat1) * cos(lat2) *
|
||||
sin(deltaLon / 2) * sin(deltaLon / 2)
|
||||
let c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
|
||||
return earthRadiusMiles * c
|
||||
}
|
||||
|
||||
// MARK: - Permutation Generation
|
||||
|
||||
/// Generate all permutations of an array (Heap's algorithm)
|
||||
static func permutations<T>(of array: [T]) -> [[T]] {
|
||||
var result: [[T]] = []
|
||||
var arr = array
|
||||
|
||||
func generate(_ n: Int) {
|
||||
if n == 1 {
|
||||
result.append(arr)
|
||||
return
|
||||
}
|
||||
|
||||
for i in 0..<n {
|
||||
generate(n - 1)
|
||||
if n % 2 == 0 {
|
||||
arr.swapAt(i, n - 1)
|
||||
} else {
|
||||
arr.swapAt(0, n - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generate(array.count)
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: - Factorial
|
||||
|
||||
/// Calculate factorial (for estimating permutation count)
|
||||
static func factorial(_ n: Int) -> Int {
|
||||
guard n > 1 else { return 1 }
|
||||
return (1...n).reduce(1, *)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Extensions
|
||||
|
||||
extension BruteForceRouteVerifier {
|
||||
/// Verify a trip's route is optimal
|
||||
static func verifyTrip(_ trip: Trip) -> VerificationResult {
|
||||
var stops: [UUID: CLLocationCoordinate2D] = [:]
|
||||
|
||||
for stop in trip.stops {
|
||||
if let coord = stop.coordinate {
|
||||
stops[stop.id] = coord
|
||||
}
|
||||
}
|
||||
|
||||
let routeIds = trip.stops.map { $0.id }
|
||||
return verify(proposedRoute: routeIds, stops: stops)
|
||||
}
|
||||
|
||||
/// Verify a list of stadiums forms an optimal route
|
||||
static func verifyStadiumRoute(_ stadiums: [Stadium]) -> VerificationResult {
|
||||
let stops = Dictionary(uniqueKeysWithValues: stadiums.map { ($0.id, $0.coordinate) })
|
||||
let routeIds = stadiums.map { $0.id }
|
||||
return verify(proposedRoute: routeIds, stops: stops)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Assertions
|
||||
|
||||
extension BruteForceRouteVerifier.VerificationResult {
|
||||
/// Returns a detailed failure message if not optimal
|
||||
var failureMessage: String? {
|
||||
guard !isOptimal else { return nil }
|
||||
|
||||
var message = "Route is not optimal. "
|
||||
message += "Proposed: \(String(format: "%.1f", proposedRouteDistance)) miles, "
|
||||
message += "Optimal: \(String(format: "%.1f", optimalRouteDistance)) miles"
|
||||
|
||||
if let improvement = improvementPercentage {
|
||||
message += " (\(String(format: "%.1f", improvement))% longer)"
|
||||
}
|
||||
|
||||
message += ". Checked \(permutationsChecked) permutations."
|
||||
|
||||
return message
|
||||
}
|
||||
}
|
||||
87
SportsTimeTests/Helpers/TestConstants.swift
Normal file
87
SportsTimeTests/Helpers/TestConstants.swift
Normal file
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// TestConstants.swift
|
||||
// SportsTimeTests
|
||||
//
|
||||
// Constants used across test suites for consistent test configuration.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum TestConstants {
|
||||
// MARK: - Distance & Radius
|
||||
|
||||
/// Standard radius for "nearby" game filtering (miles)
|
||||
static let nearbyRadiusMiles: Double = 50.0
|
||||
|
||||
/// Meters per mile conversion
|
||||
static let metersPerMile: Double = 1609.344
|
||||
|
||||
/// Nearby radius in meters
|
||||
static var nearbyRadiusMeters: Double { nearbyRadiusMiles * metersPerMile }
|
||||
|
||||
// MARK: - Timeouts
|
||||
|
||||
/// Maximum time for performance/scale tests (5 minutes)
|
||||
static let performanceTimeout: TimeInterval = 300.0
|
||||
|
||||
/// Maximum time before a test is considered hung (30 seconds)
|
||||
static let hangTimeout: TimeInterval = 30.0
|
||||
|
||||
/// Standard async test timeout
|
||||
static let standardTimeout: TimeInterval = 10.0
|
||||
|
||||
// MARK: - Performance Baselines
|
||||
// These will be recorded after initial runs and updated
|
||||
|
||||
/// Baseline time for 500 games (to be determined)
|
||||
static var baseline500Games: TimeInterval = 0
|
||||
|
||||
/// Baseline time for 2000 games (to be determined)
|
||||
static var baseline2000Games: TimeInterval = 0
|
||||
|
||||
/// Baseline time for 10000 games (to be determined)
|
||||
static var baseline10000Games: TimeInterval = 0
|
||||
|
||||
// MARK: - Driving Constraints
|
||||
|
||||
/// Default max driving hours per day (single driver)
|
||||
static let defaultMaxDrivingHoursPerDay: Double = 8.0
|
||||
|
||||
/// Average driving speed (mph) for estimates
|
||||
static let averageDrivingSpeedMPH: Double = 60.0
|
||||
|
||||
/// Max days lookahead for game transitions
|
||||
static let maxDayLookahead: Int = 5
|
||||
|
||||
// MARK: - Brute Force Verification
|
||||
|
||||
/// Maximum number of stops for brute force verification
|
||||
static let bruteForceMaxStops: Int = 8
|
||||
|
||||
// MARK: - Test Data Sizes
|
||||
|
||||
enum DataSize: Int {
|
||||
case tiny = 5
|
||||
case small = 50
|
||||
case medium = 500
|
||||
case large = 2000
|
||||
case stress = 10000
|
||||
case extreme = 50000
|
||||
}
|
||||
|
||||
// MARK: - Geographic Constants
|
||||
|
||||
/// Earth radius in miles (for haversine)
|
||||
static let earthRadiusMiles: Double = 3958.8
|
||||
|
||||
/// Earth circumference in miles
|
||||
static let earthCircumferenceMiles: Double = 24901.0
|
||||
|
||||
// MARK: - Known Distances (for validation)
|
||||
|
||||
/// NYC to LA approximate distance in miles
|
||||
static let nycToLAMiles: Double = 2451.0
|
||||
|
||||
/// Distance tolerance percentage for validation
|
||||
static let distanceTolerancePercent: Double = 0.01 // 1%
|
||||
}
|
||||
Reference in New Issue
Block a user