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:
241
SportsTimeTests/Planning/ConcurrencyTests.swift
Normal file
241
SportsTimeTests/Planning/ConcurrencyTests.swift
Normal file
@@ -0,0 +1,241 @@
|
||||
//
|
||||
// ConcurrencyTests.swift
|
||||
// SportsTimeTests
|
||||
//
|
||||
// Phase 10: Concurrency Tests
|
||||
// Documents current thread-safety behavior for future refactoring reference.
|
||||
//
|
||||
|
||||
import Testing
|
||||
import CoreLocation
|
||||
@testable import SportsTime
|
||||
|
||||
@Suite("Concurrency Tests", .serialized)
|
||||
struct ConcurrencyTests {
|
||||
|
||||
// MARK: - Test Fixtures
|
||||
|
||||
private let calendar = Calendar.current
|
||||
|
||||
/// Creates a date with specific year/month/day/hour
|
||||
private func makeDate(year: Int = 2026, month: Int = 6, day: Int, hour: Int = 19) -> Date {
|
||||
var components = DateComponents()
|
||||
components.year = year
|
||||
components.month = month
|
||||
components.day = day
|
||||
components.hour = hour
|
||||
components.minute = 0
|
||||
return calendar.date(from: components)!
|
||||
}
|
||||
|
||||
/// Creates a stadium at a known location
|
||||
private func makeStadium(
|
||||
id: UUID = UUID(),
|
||||
city: String,
|
||||
lat: Double,
|
||||
lon: Double,
|
||||
sport: Sport = .mlb
|
||||
) -> Stadium {
|
||||
Stadium(
|
||||
id: id,
|
||||
name: "\(city) Stadium",
|
||||
city: city,
|
||||
state: "ST",
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
capacity: 40000,
|
||||
sport: sport
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a game at a stadium
|
||||
private func makeGame(
|
||||
id: UUID = UUID(),
|
||||
stadiumId: UUID,
|
||||
homeTeamId: UUID = UUID(),
|
||||
awayTeamId: UUID = UUID(),
|
||||
dateTime: Date,
|
||||
sport: Sport = .mlb
|
||||
) -> Game {
|
||||
Game(
|
||||
id: id,
|
||||
homeTeamId: homeTeamId,
|
||||
awayTeamId: awayTeamId,
|
||||
stadiumId: stadiumId,
|
||||
dateTime: dateTime,
|
||||
sport: sport,
|
||||
season: "2026"
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a PlanningRequest for Scenario A (date range only)
|
||||
private func makeScenarioARequest(
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
games: [Game],
|
||||
stadiums: [UUID: Stadium]
|
||||
) -> PlanningRequest {
|
||||
let preferences = TripPreferences(
|
||||
planningMode: .dateRange,
|
||||
sports: [.mlb],
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
leisureLevel: .moderate,
|
||||
numberOfDrivers: 1,
|
||||
maxDrivingHoursPerDriver: 8.0,
|
||||
allowRepeatCities: true
|
||||
)
|
||||
|
||||
return PlanningRequest(
|
||||
preferences: preferences,
|
||||
availableGames: games,
|
||||
teams: [:],
|
||||
stadiums: stadiums
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a valid test request with nearby cities
|
||||
private func makeValidTestRequest(requestIndex: Int) -> PlanningRequest {
|
||||
// Use different but nearby city pairs for each request to create variety
|
||||
let cityPairs: [(city1: (String, Double, Double), city2: (String, Double, Double))] = [
|
||||
(("Chicago", 41.8781, -87.6298), ("Milwaukee", 43.0389, -87.9065)),
|
||||
(("New York", 40.7128, -73.9352), ("Philadelphia", 39.9526, -75.1652)),
|
||||
(("Boston", 42.3601, -71.0589), ("Providence", 41.8240, -71.4128)),
|
||||
(("Los Angeles", 34.0522, -118.2437), ("San Diego", 32.7157, -117.1611)),
|
||||
(("Seattle", 47.6062, -122.3321), ("Portland", 45.5152, -122.6784)),
|
||||
]
|
||||
|
||||
let pair = cityPairs[requestIndex % cityPairs.count]
|
||||
|
||||
let stadium1Id = UUID()
|
||||
let stadium2Id = UUID()
|
||||
|
||||
let stadium1 = makeStadium(id: stadium1Id, city: pair.city1.0, lat: pair.city1.1, lon: pair.city1.2)
|
||||
let stadium2 = makeStadium(id: stadium2Id, city: pair.city2.0, lat: pair.city2.1, lon: pair.city2.2)
|
||||
|
||||
let stadiums = [stadium1Id: stadium1, stadium2Id: stadium2]
|
||||
|
||||
// Games on different days for feasible routing
|
||||
let baseDay = 5 + (requestIndex * 2) % 20
|
||||
let game1 = makeGame(stadiumId: stadium1Id, dateTime: makeDate(day: baseDay, hour: 19))
|
||||
let game2 = makeGame(stadiumId: stadium2Id, dateTime: makeDate(day: baseDay + 2, hour: 19))
|
||||
|
||||
return makeScenarioARequest(
|
||||
startDate: makeDate(day: baseDay - 1, hour: 0),
|
||||
endDate: makeDate(day: baseDay + 5, hour: 23),
|
||||
games: [game1, game2],
|
||||
stadiums: stadiums
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - 10.1: Concurrent Requests Test
|
||||
|
||||
@Test("10.1 - Concurrent requests behavior documentation")
|
||||
func test_engine_ConcurrentRequests_CurrentlyUnsafe() async {
|
||||
// DOCUMENTATION TEST
|
||||
// Purpose: Document the current behavior when TripPlanningEngine is called concurrently.
|
||||
//
|
||||
// Current Implementation Status:
|
||||
// - TripPlanningEngine is a `final class` (not an actor)
|
||||
// - It appears stateless - no mutable instance state persists between calls
|
||||
// - Each call to planItineraries creates fresh planner instances
|
||||
//
|
||||
// Expected Behavior:
|
||||
// - If truly stateless: concurrent calls should succeed independently
|
||||
// - If hidden state exists: may see race conditions or crashes
|
||||
//
|
||||
// This test documents the current behavior for future refactoring reference.
|
||||
|
||||
let concurrentRequestCount = 10
|
||||
let engine = TripPlanningEngine()
|
||||
|
||||
// Create unique requests for each concurrent task
|
||||
let requests = (0..<concurrentRequestCount).map { makeValidTestRequest(requestIndex: $0) }
|
||||
|
||||
// Execute all requests concurrently using TaskGroup
|
||||
let results = await withTaskGroup(of: (Int, ItineraryResult).self) { group in
|
||||
for (index, request) in requests.enumerated() {
|
||||
group.addTask {
|
||||
let result = engine.planItineraries(request: request)
|
||||
return (index, result)
|
||||
}
|
||||
}
|
||||
|
||||
var collected: [(Int, ItineraryResult)] = []
|
||||
for await result in group {
|
||||
collected.append(result)
|
||||
}
|
||||
return collected.sorted { $0.0 < $1.0 }
|
||||
}
|
||||
|
||||
// Document the observed behavior
|
||||
let successCount = results.filter { $0.1.isSuccess }.count
|
||||
let failureCount = results.filter { !$0.1.isSuccess }.count
|
||||
|
||||
// Current observation: Engine appears stateless, so concurrent calls should work
|
||||
// If this test fails in the future, it indicates hidden shared state was introduced
|
||||
|
||||
// We expect most/all requests to succeed since the engine is stateless
|
||||
// Allow for some failures due to specific request constraints
|
||||
#expect(results.count == concurrentRequestCount,
|
||||
"All concurrent requests should complete (got \(results.count)/\(concurrentRequestCount))")
|
||||
|
||||
// Document: Current implementation handles concurrent requests
|
||||
// because planItineraries() creates fresh planners per call
|
||||
#expect(successCount > 0,
|
||||
"At least some concurrent requests should succeed (success: \(successCount), failure: \(failureCount))")
|
||||
|
||||
// Note for future refactoring:
|
||||
// If actor-based refactoring is done, update this test to verify:
|
||||
// 1. Proper isolation of mutable state
|
||||
// 2. Consistent results regardless of concurrent access
|
||||
// 3. No deadlocks under high concurrency
|
||||
}
|
||||
|
||||
// MARK: - 10.2: Sequential Requests Test
|
||||
|
||||
@Test("10.2 - Sequential requests succeed consistently")
|
||||
func test_engine_SequentialRequests_Succeeds() {
|
||||
// BASELINE TEST
|
||||
// Purpose: Verify that sequential requests to the same engine instance
|
||||
// always succeed when given valid input.
|
||||
//
|
||||
// This establishes the baseline behavior that any concurrency-safe
|
||||
// refactoring must preserve.
|
||||
|
||||
let sequentialRequestCount = 10
|
||||
let engine = TripPlanningEngine()
|
||||
|
||||
var results: [ItineraryResult] = []
|
||||
|
||||
// Execute requests sequentially
|
||||
for index in 0..<sequentialRequestCount {
|
||||
let request = makeValidTestRequest(requestIndex: index)
|
||||
let result = engine.planItineraries(request: request)
|
||||
results.append(result)
|
||||
}
|
||||
|
||||
// All sequential requests should complete
|
||||
#expect(results.count == sequentialRequestCount,
|
||||
"All sequential requests should complete")
|
||||
|
||||
// With valid input, all requests should succeed
|
||||
let successCount = results.filter { $0.isSuccess }.count
|
||||
|
||||
#expect(successCount == sequentialRequestCount,
|
||||
"All sequential requests with valid input should succeed (got \(successCount)/\(sequentialRequestCount))")
|
||||
|
||||
// Verify each successful result has valid data
|
||||
for (index, result) in results.enumerated() {
|
||||
if result.isSuccess {
|
||||
#expect(!result.options.isEmpty,
|
||||
"Request \(index) should return at least one route option")
|
||||
|
||||
for option in result.options {
|
||||
#expect(!option.stops.isEmpty,
|
||||
"Request \(index) options should have stops")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user