Systematic audit of 1,191 tests found tests written to pass rather than verify correctness. Key fixes: Infrastructure: - TestClock: fixed timezone from .current to America/New_York (deterministic) - TestFixtures: added 1.3x road routing factor to match production - ItineraryTestHelpers: real per-city coordinates instead of hardcoded (40,-80) Planning tests: - Added missing Scenario E factory dispatch tests - Tightened 12 loose assertions (>= 1 → == 8.0, > 0 → range checks) - Fixed 4 no-op tests that accepted both success and failure - Fixed wrong repeat-city invariant (was checking same-day, not different-day) - Fixed tautological assertion in missing-stadium edge case Services/Domain/Export tests: - Replaced 4 placeholder tests (#expect(true)) with real assertions - Fixed tautological assertions in POISearchServiceTests - Fixed Chicago coordinate in RegionMapSelectorTests (-89 → -87.6553) - Added sort order verification to ItineraryRowFlatteningTests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
369 lines
14 KiB
Swift
369 lines
14 KiB
Swift
//
|
|
// ItineraryBuilderTests.swift
|
|
// SportsTimeTests
|
|
//
|
|
// TDD specification + property tests for ItineraryBuilder.
|
|
//
|
|
|
|
import Testing
|
|
import CoreLocation
|
|
@testable import SportsTime
|
|
|
|
@Suite("ItineraryBuilder")
|
|
struct ItineraryBuilderTests {
|
|
|
|
// MARK: - Test Data
|
|
|
|
private let constraints = DrivingConstraints.default // 1 driver, 8 hrs/day
|
|
|
|
private let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
|
|
private let bostonCoord = CLLocationCoordinate2D(latitude: 42.3467, longitude: -71.0972)
|
|
private let phillyCoord = CLLocationCoordinate2D(latitude: 39.9526, longitude: -75.1652)
|
|
private let chicagoCoord = CLLocationCoordinate2D(latitude: 41.8827, longitude: -87.6233)
|
|
private let seattleCoord = CLLocationCoordinate2D(latitude: 47.5914, longitude: -122.3316)
|
|
|
|
private let calendar = TestClock.calendar
|
|
|
|
// MARK: - Specification Tests: build()
|
|
|
|
@Test("build: empty stops returns empty itinerary")
|
|
func build_emptyStops_returnsEmptyItinerary() {
|
|
let result = ItineraryBuilder.build(stops: [], constraints: constraints)
|
|
|
|
#expect(result != nil)
|
|
#expect(result?.stops.isEmpty == true)
|
|
#expect(result?.travelSegments.isEmpty == true)
|
|
#expect(result?.totalDrivingHours == 0)
|
|
#expect(result?.totalDistanceMiles == 0)
|
|
}
|
|
|
|
@Test("build: single stop returns single-stop itinerary")
|
|
func build_singleStop_returnsSingleStopItinerary() {
|
|
let stop = makeStop(city: "New York", coordinate: nycCoord)
|
|
|
|
let result = ItineraryBuilder.build(stops: [stop], constraints: constraints)
|
|
|
|
#expect(result != nil)
|
|
#expect(result?.stops.count == 1)
|
|
#expect(result?.travelSegments.isEmpty == true)
|
|
#expect(result?.totalDrivingHours == 0)
|
|
#expect(result?.totalDistanceMiles == 0)
|
|
}
|
|
|
|
@Test("build: two stops creates one segment")
|
|
func build_twoStops_createsOneSegment() {
|
|
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
|
let stop2 = makeStop(city: "Boston", coordinate: bostonCoord)
|
|
|
|
let result = ItineraryBuilder.build(stops: [stop1, stop2], constraints: constraints)
|
|
|
|
#expect(result != nil)
|
|
#expect(result?.stops.count == 2)
|
|
#expect(result?.travelSegments.count == 1)
|
|
#expect((3.0...6.0).contains(result?.totalDrivingHours ?? 0), "NYC→Boston should be 3-6 hours driving")
|
|
#expect((200...400).contains(result?.totalDistanceMiles ?? 0), "NYC→Boston should be 200-400 road miles")
|
|
}
|
|
|
|
@Test("build: three stops creates two segments")
|
|
func build_threeStops_createsTwoSegments() {
|
|
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
|
let stop2 = makeStop(city: "Philadelphia", coordinate: phillyCoord)
|
|
let stop3 = makeStop(city: "Boston", coordinate: bostonCoord)
|
|
|
|
let result = ItineraryBuilder.build(stops: [stop1, stop2, stop3], constraints: constraints)
|
|
|
|
#expect(result != nil)
|
|
#expect(result?.stops.count == 3)
|
|
#expect(result?.travelSegments.count == 2)
|
|
}
|
|
|
|
@Test("build: totalDrivingHours is sum of segments")
|
|
func build_totalDrivingHours_isSumOfSegments() {
|
|
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
|
let stop2 = makeStop(city: "Philadelphia", coordinate: phillyCoord)
|
|
let stop3 = makeStop(city: "Boston", coordinate: bostonCoord)
|
|
|
|
let result = ItineraryBuilder.build(stops: [stop1, stop2, stop3], constraints: constraints)!
|
|
|
|
let segmentHours = result.travelSegments.reduce(0.0) { $0 + $1.estimatedDrivingHours }
|
|
#expect(abs(result.totalDrivingHours - segmentHours) < 0.01)
|
|
}
|
|
|
|
@Test("build: totalDistanceMiles is sum of segments")
|
|
func build_totalDistanceMiles_isSumOfSegments() {
|
|
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
|
let stop2 = makeStop(city: "Philadelphia", coordinate: phillyCoord)
|
|
let stop3 = makeStop(city: "Boston", coordinate: bostonCoord)
|
|
|
|
let result = ItineraryBuilder.build(stops: [stop1, stop2, stop3], constraints: constraints)!
|
|
|
|
let segmentMiles = result.travelSegments.reduce(0.0) { $0 + $1.estimatedDistanceMiles }
|
|
#expect(abs(result.totalDistanceMiles - segmentMiles) < 0.01)
|
|
}
|
|
|
|
@Test("build: infeasible segment returns nil")
|
|
func build_infeasibleSegment_returnsNil() {
|
|
// NYC to Seattle is ~2850 miles, ~47 hours driving
|
|
// With 1 driver at 8 hrs/day, max is 40 hours (5 days)
|
|
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
|
let stop2 = makeStop(city: "Seattle", coordinate: seattleCoord)
|
|
|
|
let result = ItineraryBuilder.build(stops: [stop1, stop2], constraints: constraints)
|
|
|
|
#expect(result == nil)
|
|
}
|
|
|
|
@Test("build: feasible with more drivers succeeds")
|
|
func build_feasibleWithMoreDrivers_succeeds() {
|
|
// NYC to Seattle with 2 drivers: max is 80 hours (2*8*5)
|
|
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
|
let stop2 = makeStop(city: "Seattle", coordinate: seattleCoord)
|
|
let twoDrivers = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 8.0)
|
|
|
|
let result = ItineraryBuilder.build(stops: [stop1, stop2], constraints: twoDrivers)
|
|
|
|
#expect(result != nil)
|
|
}
|
|
|
|
// MARK: - Specification Tests: Custom Validator
|
|
|
|
@Test("build: validator returning true allows segment")
|
|
func build_validatorReturnsTrue_allowsSegment() {
|
|
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
|
let stop2 = makeStop(city: "Boston", coordinate: bostonCoord)
|
|
|
|
let alwaysValid: ItineraryBuilder.SegmentValidator = { _, _, _ in true }
|
|
|
|
let result = ItineraryBuilder.build(
|
|
stops: [stop1, stop2],
|
|
constraints: constraints,
|
|
segmentValidator: alwaysValid
|
|
)
|
|
|
|
#expect(result != nil)
|
|
}
|
|
|
|
@Test("build: validator returning false rejects itinerary")
|
|
func build_validatorReturnsFalse_rejectsItinerary() {
|
|
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
|
let stop2 = makeStop(city: "Boston", coordinate: bostonCoord)
|
|
|
|
let alwaysInvalid: ItineraryBuilder.SegmentValidator = { _, _, _ in false }
|
|
|
|
let result = ItineraryBuilder.build(
|
|
stops: [stop1, stop2],
|
|
constraints: constraints,
|
|
segmentValidator: alwaysInvalid
|
|
)
|
|
|
|
#expect(result == nil)
|
|
}
|
|
|
|
@Test("build: validator receives correct stops")
|
|
func build_validatorReceivesCorrectStops() {
|
|
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
|
let stop2 = makeStop(city: "Boston", coordinate: bostonCoord)
|
|
|
|
var capturedFromCity: String?
|
|
var capturedToCity: String?
|
|
|
|
let captureValidator: ItineraryBuilder.SegmentValidator = { _, fromStop, toStop in
|
|
capturedFromCity = fromStop.city
|
|
capturedToCity = toStop.city
|
|
return true
|
|
}
|
|
|
|
_ = ItineraryBuilder.build(
|
|
stops: [stop1, stop2],
|
|
constraints: constraints,
|
|
segmentValidator: captureValidator
|
|
)
|
|
|
|
#expect(capturedFromCity == "New York")
|
|
#expect(capturedToCity == "Boston")
|
|
}
|
|
|
|
// MARK: - Specification Tests: arrivalBeforeGameStart Validator
|
|
|
|
@Test("arrivalBeforeGameStart: no game start time always passes")
|
|
func arrivalBeforeGameStart_noGameStart_alwaysPasses() {
|
|
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
|
let stop2 = makeStop(city: "Boston", coordinate: bostonCoord, firstGameStart: nil)
|
|
|
|
let validator = ItineraryBuilder.arrivalBeforeGameStart(bufferSeconds: 3600)
|
|
|
|
let result = ItineraryBuilder.build(
|
|
stops: [stop1, stop2],
|
|
constraints: constraints,
|
|
segmentValidator: validator
|
|
)
|
|
|
|
#expect(result != nil)
|
|
}
|
|
|
|
@Test("arrivalBeforeGameStart: sufficient time passes")
|
|
func arrivalBeforeGameStart_sufficientTime_passes() {
|
|
let now = TestClock.now
|
|
let tomorrow = calendar.date(byAdding: .day, value: 1, to: now)!
|
|
let gameTime = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: tomorrow)!
|
|
|
|
let stop1 = makeStop(
|
|
city: "New York",
|
|
coordinate: nycCoord,
|
|
departureDate: now
|
|
)
|
|
let stop2 = makeStop(
|
|
city: "Boston",
|
|
coordinate: bostonCoord,
|
|
firstGameStart: gameTime
|
|
)
|
|
|
|
// NYC to Boston is ~4 hours, game is tomorrow at 7pm, plenty of time
|
|
let validator = ItineraryBuilder.arrivalBeforeGameStart(bufferSeconds: 3600)
|
|
|
|
let result = ItineraryBuilder.build(
|
|
stops: [stop1, stop2],
|
|
constraints: constraints,
|
|
segmentValidator: validator
|
|
)
|
|
|
|
#expect(result != nil)
|
|
}
|
|
|
|
@Test("arrivalBeforeGameStart: insufficient time fails")
|
|
func arrivalBeforeGameStart_insufficientTime_fails() {
|
|
let now = TestClock.now
|
|
let gameTime = now.addingTimeInterval(2 * 3600) // Game in 2 hours
|
|
|
|
let stop1 = makeStop(
|
|
city: "New York",
|
|
coordinate: nycCoord,
|
|
departureDate: now
|
|
)
|
|
let stop2 = makeStop(
|
|
city: "Chicago", // ~13 hours away
|
|
coordinate: chicagoCoord,
|
|
firstGameStart: gameTime
|
|
)
|
|
|
|
let validator = ItineraryBuilder.arrivalBeforeGameStart(bufferSeconds: 3600)
|
|
|
|
let result = ItineraryBuilder.build(
|
|
stops: [stop1, stop2],
|
|
constraints: constraints,
|
|
segmentValidator: validator
|
|
)
|
|
|
|
// Should fail because we can't get to Chicago in 2 hours
|
|
// (assuming the segment is even feasible, which it isn't for 1 driver)
|
|
// Either the segment is infeasible OR the validator rejects it
|
|
#expect(result == nil)
|
|
}
|
|
|
|
// MARK: - Property Tests
|
|
|
|
@Test("Property: segments count equals stops count minus one")
|
|
func property_segmentsCountEqualsStopsMinusOne() {
|
|
for count in [2, 3, 4, 5] {
|
|
let stops = (0..<count).map { i in
|
|
makeStop(city: "City\(i)", coordinate: nycCoord) // Same coord for all = always feasible
|
|
}
|
|
|
|
if let result = ItineraryBuilder.build(stops: stops, constraints: constraints) {
|
|
#expect(result.travelSegments.count == stops.count - 1,
|
|
"For \(count) stops, should have \(count - 1) segments")
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test("Property: totals are non-negative")
|
|
func property_totalsNonNegative() {
|
|
let stops = [
|
|
makeStop(city: "New York", coordinate: nycCoord),
|
|
makeStop(city: "Philadelphia", coordinate: phillyCoord)
|
|
]
|
|
|
|
if let result = ItineraryBuilder.build(stops: stops, constraints: constraints) {
|
|
#expect(result.totalDrivingHours >= 0)
|
|
#expect(result.totalDistanceMiles >= 0)
|
|
}
|
|
}
|
|
|
|
@Test("Property: empty/single stop always succeeds")
|
|
func property_emptyOrSingleStopAlwaysSucceeds() {
|
|
let emptyResult = ItineraryBuilder.build(stops: [], constraints: constraints)
|
|
#expect(emptyResult != nil)
|
|
|
|
let singleResult = ItineraryBuilder.build(
|
|
stops: [makeStop(city: "NYC", coordinate: nycCoord)],
|
|
constraints: constraints
|
|
)
|
|
#expect(singleResult != nil)
|
|
}
|
|
|
|
// MARK: - Edge Case Tests
|
|
|
|
@Test("Edge: stops with nil coordinates are infeasible")
|
|
func edge_nilCoordinates_infeasible() {
|
|
let stop1 = makeStop(city: "City1", coordinate: nil)
|
|
let stop2 = makeStop(city: "City2", coordinate: nil)
|
|
|
|
let result = ItineraryBuilder.build(stops: [stop1, stop2], constraints: constraints)
|
|
|
|
// Missing coordinates = infeasible (safer to skip than show wrong drive time)
|
|
#expect(result == nil, "Stops with missing coordinates should be infeasible")
|
|
}
|
|
|
|
@Test("Edge: same city stops have zero distance")
|
|
func edge_sameCityStops_zeroDistance() {
|
|
let stop1 = makeStop(city: "New York", coordinate: nycCoord)
|
|
let stop2 = makeStop(city: "New York", coordinate: nycCoord) // Same location
|
|
|
|
let result = ItineraryBuilder.build(stops: [stop1, stop2], constraints: constraints)
|
|
|
|
#expect(result != nil)
|
|
// Same coordinates should result in ~0 distance
|
|
#expect(result?.totalDistanceMiles ?? 1000 < 1)
|
|
}
|
|
|
|
@Test("Edge: very long trip is still feasible with multiple drivers")
|
|
func edge_veryLongTrip_feasibleWithMultipleDrivers() {
|
|
// NYC -> Chicago -> Seattle
|
|
let stops = [
|
|
makeStop(city: "New York", coordinate: nycCoord),
|
|
makeStop(city: "Chicago", coordinate: chicagoCoord),
|
|
makeStop(city: "Seattle", coordinate: seattleCoord)
|
|
]
|
|
|
|
// With 3 drivers, max is 120 hours (3*8*5)
|
|
let threeDrivers = DrivingConstraints(numberOfDrivers: 3, maxHoursPerDriverPerDay: 8.0)
|
|
|
|
let result = ItineraryBuilder.build(stops: stops, constraints: threeDrivers)
|
|
|
|
#expect(result != nil)
|
|
}
|
|
|
|
// MARK: - Helper Methods
|
|
|
|
private func makeStop(
|
|
city: String,
|
|
coordinate: CLLocationCoordinate2D?,
|
|
departureDate: Date = TestClock.now,
|
|
firstGameStart: Date? = nil
|
|
) -> ItineraryStop {
|
|
ItineraryStop(
|
|
city: city,
|
|
state: "XX",
|
|
coordinate: coordinate,
|
|
games: [],
|
|
arrivalDate: departureDate,
|
|
departureDate: departureDate,
|
|
location: LocationInput(
|
|
name: city,
|
|
coordinate: coordinate
|
|
),
|
|
firstGameStart: firstGameStart
|
|
)
|
|
}
|
|
}
|