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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user