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>
575 lines
22 KiB
Swift
575 lines
22 KiB
Swift
//
|
|
// TestFixtures.swift
|
|
// SportsTimeTests
|
|
//
|
|
// Factory methods for creating test data. These fixtures create realistic
|
|
// domain objects with sensible defaults that can be customized per test.
|
|
//
|
|
// Usage:
|
|
// let game = TestFixtures.game() // Default game
|
|
// let game = TestFixtures.game(sport: .nba, city: "Boston") // Customized
|
|
//
|
|
|
|
import Foundation
|
|
import CoreLocation
|
|
@testable import SportsTime
|
|
|
|
// MARK: - Test Fixtures
|
|
|
|
enum TestFixtures {
|
|
|
|
// MARK: - Reference Data
|
|
|
|
/// Real stadium coordinates for realistic distance calculations
|
|
static let coordinates: [String: CLLocationCoordinate2D] = [
|
|
"New York": CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855), // NYC (Midtown)
|
|
"Boston": CLLocationCoordinate2D(latitude: 42.3467, longitude: -71.0972), // Fenway Park
|
|
"Chicago": CLLocationCoordinate2D(latitude: 41.9484, longitude: -87.6553), // Wrigley Field
|
|
"Los Angeles": CLLocationCoordinate2D(latitude: 34.0739, longitude: -118.2400), // Dodger Stadium
|
|
"San Francisco": CLLocationCoordinate2D(latitude: 37.7786, longitude: -122.3893), // Oracle Park
|
|
"Seattle": CLLocationCoordinate2D(latitude: 47.5914, longitude: -122.3325), // T-Mobile Park
|
|
"Denver": CLLocationCoordinate2D(latitude: 39.7559, longitude: -104.9942), // Coors Field
|
|
"Houston": CLLocationCoordinate2D(latitude: 29.7573, longitude: -95.3555), // Minute Maid
|
|
"Miami": CLLocationCoordinate2D(latitude: 25.7781, longitude: -80.2197), // LoanDepot Park
|
|
"Atlanta": CLLocationCoordinate2D(latitude: 33.7553, longitude: -84.4006), // Truist Park
|
|
"Phoenix": CLLocationCoordinate2D(latitude: 33.4455, longitude: -112.0667), // Chase Field
|
|
"Dallas": CLLocationCoordinate2D(latitude: 32.7473, longitude: -97.0945), // Globe Life Field
|
|
"Philadelphia": CLLocationCoordinate2D(latitude: 39.9061, longitude: -75.1665), // Citizens Bank
|
|
"Detroit": CLLocationCoordinate2D(latitude: 42.3390, longitude: -83.0485), // Comerica Park
|
|
"Minneapolis": CLLocationCoordinate2D(latitude: 44.9817, longitude: -93.2776), // Target Field
|
|
]
|
|
|
|
/// Time zones for realistic local time testing
|
|
static let timeZones: [String: String] = [
|
|
"New York": "America/New_York",
|
|
"Boston": "America/New_York",
|
|
"Chicago": "America/Chicago",
|
|
"Los Angeles": "America/Los_Angeles",
|
|
"San Francisco": "America/Los_Angeles",
|
|
"Seattle": "America/Los_Angeles",
|
|
"Denver": "America/Denver",
|
|
"Houston": "America/Chicago",
|
|
"Miami": "America/New_York",
|
|
"Atlanta": "America/New_York",
|
|
"Phoenix": "America/Phoenix",
|
|
"Dallas": "America/Chicago",
|
|
"Philadelphia": "America/New_York",
|
|
"Detroit": "America/Detroit",
|
|
"Minneapolis": "America/Chicago",
|
|
]
|
|
|
|
/// State abbreviations
|
|
static let states: [String: String] = [
|
|
"New York": "NY", "Boston": "MA", "Chicago": "IL",
|
|
"Los Angeles": "CA", "San Francisco": "CA", "Seattle": "WA",
|
|
"Denver": "CO", "Houston": "TX", "Miami": "FL", "Atlanta": "GA",
|
|
"Phoenix": "AZ", "Dallas": "TX", "Philadelphia": "PA",
|
|
"Detroit": "MI", "Minneapolis": "MN",
|
|
]
|
|
|
|
// MARK: - Game Factory
|
|
|
|
/// Creates a Game with realistic defaults.
|
|
///
|
|
/// - Expected Behavior:
|
|
/// - Returns a valid Game with all required fields populated
|
|
/// - ID follows canonical format: "game_{sport}_{season}_{away}_{home}_{mmdd}"
|
|
/// - DateTime defaults to noon tomorrow in specified city's timezone
|
|
static func game(
|
|
id: String? = nil,
|
|
sport: Sport = .mlb,
|
|
city: String = "New York",
|
|
dateTime: Date? = nil,
|
|
homeTeamId: String? = nil,
|
|
awayTeamId: String? = nil,
|
|
stadiumId: String? = nil,
|
|
season: String = "2026",
|
|
isPlayoff: Bool = false
|
|
) -> Game {
|
|
let actualDateTime = dateTime ?? TestClock.calendar.date(byAdding: .day, value: 1, to: TestClock.now)!
|
|
let homeId = homeTeamId ?? "team_\(sport.rawValue.lowercased())_\(city.lowercased().replacingOccurrences(of: " ", with: "_"))"
|
|
let awayId = awayTeamId ?? "team_\(sport.rawValue.lowercased())_visitor"
|
|
let stadId = stadiumId ?? "stadium_\(sport.rawValue.lowercased())_\(city.lowercased().replacingOccurrences(of: " ", with: "_"))"
|
|
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "MMdd"
|
|
formatter.timeZone = TestClock.timeZone
|
|
formatter.locale = TestClock.locale
|
|
let dateStr = formatter.string(from: actualDateTime)
|
|
|
|
let actualId = id ?? "game_\(sport.rawValue.lowercased())_\(season)_\(awayId.split(separator: "_").last ?? "vis")_\(homeId.split(separator: "_").last ?? "home")_\(dateStr)"
|
|
|
|
return Game(
|
|
id: actualId,
|
|
homeTeamId: homeId,
|
|
awayTeamId: awayId,
|
|
stadiumId: stadId,
|
|
dateTime: actualDateTime,
|
|
sport: sport,
|
|
season: season,
|
|
isPlayoff: isPlayoff
|
|
)
|
|
}
|
|
|
|
/// Creates multiple games spread across time and cities.
|
|
///
|
|
/// - Parameter count: Number of games to create
|
|
/// - Parameter cities: Cities to distribute games across (cycles through list)
|
|
/// - Parameter startDate: First game date (subsequent games spread by daySpread)
|
|
/// - Parameter daySpread: Days between games
|
|
static func games(
|
|
count: Int,
|
|
sport: Sport = .mlb,
|
|
cities: [String] = ["New York", "Boston", "Chicago", "Los Angeles"],
|
|
startDate: Date = TestClock.now,
|
|
daySpread: Int = 1
|
|
) -> [Game] {
|
|
(0..<count).map { i in
|
|
let city = cities[i % cities.count]
|
|
let gameDate = TestClock.calendar.date(byAdding: .day, value: i * daySpread, to: startDate)!
|
|
return game(sport: sport, city: city, dateTime: gameDate)
|
|
}
|
|
}
|
|
|
|
/// Creates games for same-day conflict testing.
|
|
static func sameDayGames(
|
|
cities: [String],
|
|
date: Date = TestClock.now,
|
|
sport: Sport = .mlb
|
|
) -> [Game] {
|
|
cities.enumerated().map { index, city in
|
|
// Stagger times by 3 hours
|
|
let time = TestClock.calendar.date(byAdding: .hour, value: 13 + (index * 3), to: TestClock.calendar.startOfDay(for: date))!
|
|
return game(sport: sport, city: city, dateTime: time)
|
|
}
|
|
}
|
|
|
|
// MARK: - Stadium Factory
|
|
|
|
/// Creates a Stadium with realistic defaults.
|
|
///
|
|
/// - Expected Behavior:
|
|
/// - Uses real coordinates for known cities
|
|
/// - ID follows canonical format: "stadium_{sport}_{city}"
|
|
/// - TimeZone populated for known cities
|
|
static func stadium(
|
|
id: String? = nil,
|
|
name: String? = nil,
|
|
city: String = "New York",
|
|
state: String? = nil,
|
|
sport: Sport = .mlb,
|
|
capacity: Int = 40000,
|
|
yearOpened: Int? = nil
|
|
) -> Stadium {
|
|
let coordinate = coordinates[city] ?? CLLocationCoordinate2D(latitude: 40.0, longitude: -74.0)
|
|
let actualState = state ?? states[city] ?? "NY"
|
|
let actualName = name ?? "\(city) \(sport.rawValue) Stadium"
|
|
let actualId = id ?? "stadium_\(sport.rawValue.lowercased())_\(city.lowercased().replacingOccurrences(of: " ", with: "_"))"
|
|
|
|
return Stadium(
|
|
id: actualId,
|
|
name: actualName,
|
|
city: city,
|
|
state: actualState,
|
|
latitude: coordinate.latitude,
|
|
longitude: coordinate.longitude,
|
|
capacity: capacity,
|
|
sport: sport,
|
|
yearOpened: yearOpened,
|
|
timeZoneIdentifier: timeZones[city]
|
|
)
|
|
}
|
|
|
|
/// Creates a stadium map for a set of games.
|
|
static func stadiumMap(for games: [Game]) -> [String: Stadium] {
|
|
var map: [String: Stadium] = [:]
|
|
for game in games {
|
|
if map[game.stadiumId] == nil {
|
|
// Extract city from stadium ID (assumes format stadium_sport_city)
|
|
let parts = game.stadiumId.split(separator: "_")
|
|
let city = parts.count > 2 ? parts[2...].joined(separator: " ").capitalized : "Unknown"
|
|
map[game.stadiumId] = stadium(id: game.stadiumId, city: city, sport: game.sport)
|
|
}
|
|
}
|
|
return map
|
|
}
|
|
|
|
/// Creates stadiums at specific coordinates for distance testing.
|
|
static func stadiumsForDistanceTest() -> [Stadium] {
|
|
[
|
|
stadium(city: "New York"), // East
|
|
stadium(city: "Chicago"), // Central
|
|
stadium(city: "Denver"), // Mountain
|
|
stadium(city: "Los Angeles"), // West
|
|
]
|
|
}
|
|
|
|
// MARK: - Team Factory
|
|
|
|
/// Creates a Team with realistic defaults.
|
|
static func team(
|
|
id: String? = nil,
|
|
name: String = "Test Team",
|
|
abbreviation: String? = nil,
|
|
sport: Sport = .mlb,
|
|
city: String = "New York",
|
|
stadiumId: String? = nil
|
|
) -> Team {
|
|
let actualId = id ?? "team_\(sport.rawValue.lowercased())_\(city.lowercased().replacingOccurrences(of: " ", with: "_"))"
|
|
let actualAbbr = abbreviation ?? String(city.prefix(3)).uppercased()
|
|
let actualStadiumId = stadiumId ?? "stadium_\(sport.rawValue.lowercased())_\(city.lowercased().replacingOccurrences(of: " ", with: "_"))"
|
|
|
|
return Team(
|
|
id: actualId,
|
|
name: name,
|
|
abbreviation: actualAbbr,
|
|
sport: sport,
|
|
city: city,
|
|
stadiumId: actualStadiumId
|
|
)
|
|
}
|
|
|
|
// MARK: - TripStop Factory
|
|
|
|
/// Creates a TripStop with realistic defaults.
|
|
static func tripStop(
|
|
stopNumber: Int = 1,
|
|
city: String = "New York",
|
|
state: String? = nil,
|
|
arrivalDate: Date? = nil,
|
|
departureDate: Date? = nil,
|
|
games: [String] = [],
|
|
isRestDay: Bool = false
|
|
) -> TripStop {
|
|
let coordinate = coordinates[city]
|
|
let actualState = state ?? states[city] ?? "NY"
|
|
let arrival = arrivalDate ?? TestClock.now
|
|
let departure = departureDate ?? TestClock.calendar.date(byAdding: .day, value: 1, to: arrival)!
|
|
|
|
return TripStop(
|
|
stopNumber: stopNumber,
|
|
city: city,
|
|
state: actualState,
|
|
coordinate: coordinate,
|
|
arrivalDate: arrival,
|
|
departureDate: departure,
|
|
games: games,
|
|
isRestDay: isRestDay
|
|
)
|
|
}
|
|
|
|
/// Creates a sequence of trip stops for a multi-city trip.
|
|
static func tripStops(
|
|
cities: [String],
|
|
startDate: Date = TestClock.now,
|
|
daysPerStop: Int = 1
|
|
) -> [TripStop] {
|
|
var stops: [TripStop] = []
|
|
var currentDate = startDate
|
|
|
|
for (index, city) in cities.enumerated() {
|
|
let departure = TestClock.calendar.date(byAdding: .day, value: daysPerStop, to: currentDate)!
|
|
stops.append(tripStop(
|
|
stopNumber: index + 1,
|
|
city: city,
|
|
arrivalDate: currentDate,
|
|
departureDate: departure
|
|
))
|
|
currentDate = departure
|
|
}
|
|
return stops
|
|
}
|
|
|
|
// MARK: - TravelSegment Factory
|
|
|
|
/// Creates a TravelSegment between two cities.
|
|
static func travelSegment(
|
|
from: String = "New York",
|
|
to: String = "Boston",
|
|
travelMode: TravelMode = .drive
|
|
) -> TravelSegment {
|
|
let fromCoord = coordinates[from] ?? CLLocationCoordinate2D(latitude: 40.0, longitude: -74.0)
|
|
let toCoord = coordinates[to] ?? CLLocationCoordinate2D(latitude: 42.0, longitude: -71.0)
|
|
|
|
// Calculate approximate distance (haversine)
|
|
let distance = haversineDistance(from: fromCoord, to: toCoord) * 1.3
|
|
// Estimate driving time at 60 mph average
|
|
let duration = distance / 60.0 * 3600.0
|
|
|
|
return TravelSegment(
|
|
fromLocation: LocationInput(name: from, coordinate: fromCoord),
|
|
toLocation: LocationInput(name: to, coordinate: toCoord),
|
|
travelMode: travelMode,
|
|
distanceMeters: distance * 1609.34, // miles to meters
|
|
durationSeconds: duration
|
|
)
|
|
}
|
|
|
|
// MARK: - TripPreferences Factory
|
|
|
|
/// Creates TripPreferences with common defaults.
|
|
static func preferences(
|
|
mode: PlanningMode = .dateRange,
|
|
sports: Set<Sport> = [.mlb],
|
|
startDate: Date? = nil,
|
|
endDate: Date? = nil,
|
|
regions: Set<Region> = [.east, .central, .west],
|
|
leisureLevel: LeisureLevel = .moderate,
|
|
travelMode: TravelMode = .drive,
|
|
needsEVCharging: Bool = false,
|
|
maxDrivingHoursPerDriver: Double? = nil
|
|
) -> TripPreferences {
|
|
let start = startDate ?? TestClock.now
|
|
let end = endDate ?? TestClock.calendar.date(byAdding: .day, value: 7, to: start)!
|
|
|
|
return TripPreferences(
|
|
planningMode: mode,
|
|
sports: sports,
|
|
travelMode: travelMode,
|
|
startDate: start,
|
|
endDate: end,
|
|
leisureLevel: leisureLevel,
|
|
routePreference: .balanced,
|
|
needsEVCharging: needsEVCharging,
|
|
maxDrivingHoursPerDriver: maxDrivingHoursPerDriver,
|
|
selectedRegions: regions
|
|
)
|
|
}
|
|
|
|
// MARK: - Trip Factory
|
|
|
|
/// Creates a complete Trip with stops and segments.
|
|
static func trip(
|
|
name: String = "Test Trip",
|
|
stops: [TripStop]? = nil,
|
|
preferences: TripPreferences? = nil,
|
|
status: TripStatus = .planned
|
|
) -> Trip {
|
|
let actualStops = stops ?? tripStops(cities: ["New York", "Boston"])
|
|
let actualPrefs = preferences ?? TestFixtures.preferences()
|
|
|
|
// Calculate totals from stops
|
|
let totalGames = actualStops.reduce(0) { $0 + $1.games.count }
|
|
|
|
return Trip(
|
|
name: name,
|
|
preferences: actualPrefs,
|
|
stops: actualStops,
|
|
totalGames: totalGames,
|
|
status: status
|
|
)
|
|
}
|
|
|
|
// MARK: - RichGame Factory
|
|
|
|
/// Creates a RichGame with resolved team and stadium references.
|
|
static func richGame(
|
|
game: Game? = nil,
|
|
homeCity: String = "New York",
|
|
awayCity: String = "Boston",
|
|
sport: Sport = .mlb
|
|
) -> RichGame {
|
|
let actualGame = game ?? TestFixtures.game(sport: sport, city: homeCity)
|
|
let homeTeam = team(sport: sport, city: homeCity)
|
|
let awayTeam = team(sport: sport, city: awayCity)
|
|
let gameStadium = stadium(city: homeCity, sport: sport)
|
|
|
|
return RichGame(
|
|
game: actualGame,
|
|
homeTeam: homeTeam,
|
|
awayTeam: awayTeam,
|
|
stadium: gameStadium
|
|
)
|
|
}
|
|
|
|
// MARK: - TripScore Factory
|
|
|
|
/// Creates a TripScore with customizable component scores.
|
|
static func tripScore(
|
|
overall: Double = 85.0,
|
|
gameQuality: Double = 90.0,
|
|
routeEfficiency: Double = 80.0,
|
|
leisureBalance: Double = 85.0,
|
|
preferenceAlignment: Double = 85.0
|
|
) -> TripScore {
|
|
TripScore(
|
|
overallScore: overall,
|
|
gameQualityScore: gameQuality,
|
|
routeEfficiencyScore: routeEfficiency,
|
|
leisureBalanceScore: leisureBalance,
|
|
preferenceAlignmentScore: preferenceAlignment
|
|
)
|
|
}
|
|
|
|
// MARK: - Date Helpers
|
|
|
|
/// Creates a date at a specific time (for testing time-sensitive logic).
|
|
static func date(
|
|
year: Int = 2026,
|
|
month: Int = 6,
|
|
day: Int = 15,
|
|
hour: Int = 19,
|
|
minute: Int = 5
|
|
) -> Date {
|
|
var components = DateComponents()
|
|
components.year = year
|
|
components.month = month
|
|
components.day = day
|
|
components.hour = hour
|
|
components.minute = minute
|
|
components.timeZone = TimeZone(identifier: "America/New_York")
|
|
return TestClock.calendar.date(from: components)!
|
|
}
|
|
|
|
/// Creates dates for a range of days.
|
|
static func dateRange(start: Date = TestClock.now, days: Int) -> (start: Date, end: Date) {
|
|
let end = TestClock.calendar.date(byAdding: .day, value: days, to: start)!
|
|
return (start, end)
|
|
}
|
|
|
|
// MARK: - Private Helpers
|
|
|
|
/// Haversine distance calculation (returns miles).
|
|
private static func haversineDistance(
|
|
from: CLLocationCoordinate2D,
|
|
to: CLLocationCoordinate2D
|
|
) -> Double {
|
|
let R = 3958.8 // Earth radius in miles
|
|
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 R * c
|
|
}
|
|
}
|
|
|
|
// MARK: - Coordinate Constants for Testing
|
|
|
|
extension TestFixtures {
|
|
|
|
/// Known distances between cities (in miles) for validation.
|
|
static let knownDistances: [(from: String, to: String, miles: Double)] = [
|
|
("New York", "Boston", 215),
|
|
("New York", "Chicago", 790),
|
|
("New York", "Los Angeles", 2790),
|
|
("Chicago", "Denver", 1000),
|
|
("Los Angeles", "San Francisco", 380),
|
|
("Seattle", "Los Angeles", 1135),
|
|
]
|
|
|
|
/// Cities clearly in each region for boundary testing.
|
|
static let eastCoastCities = ["New York", "Boston", "Miami", "Atlanta", "Philadelphia"]
|
|
static let centralCities = ["Chicago", "Houston", "Dallas", "Minneapolis", "Detroit"]
|
|
static let westCoastCities = ["Los Angeles", "San Francisco", "Seattle", "Phoenix"]
|
|
}
|
|
|
|
// MARK: - Messy / Realistic Data Factories
|
|
|
|
extension TestFixtures {
|
|
|
|
/// Creates games that are all in the past relative to a reference date.
|
|
static func pastGames(
|
|
count: Int,
|
|
sport: Sport = .mlb,
|
|
cities: [String] = ["New York", "Boston", "Chicago"],
|
|
referenceDate: Date = TestClock.now
|
|
) -> [Game] {
|
|
(0..<count).map { i in
|
|
let city = cities[i % cities.count]
|
|
let daysAgo = 30 + (i * 2) // 30-60 days in the past
|
|
let gameDate = TestClock.calendar.date(byAdding: .day, value: -daysAgo, to: referenceDate)!
|
|
return game(sport: sport, city: city, dateTime: gameDate)
|
|
}
|
|
}
|
|
|
|
/// Creates a mix of past and future games, returning them categorized.
|
|
static func mixedPastFutureGames(
|
|
pastCount: Int = 5,
|
|
futureCount: Int = 5,
|
|
sport: Sport = .mlb,
|
|
cities: [String] = ["New York", "Boston", "Chicago", "Philadelphia"],
|
|
referenceDate: Date = TestClock.now
|
|
) -> (past: [Game], future: [Game], all: [Game]) {
|
|
let past = (0..<pastCount).map { i in
|
|
let city = cities[i % cities.count]
|
|
let gameDate = TestClock.calendar.date(byAdding: .day, value: -(i + 1) * 5, to: referenceDate)!
|
|
return game(id: "past_\(i)", sport: sport, city: city, dateTime: gameDate)
|
|
}
|
|
let future = (0..<futureCount).map { i in
|
|
let city = cities[i % cities.count]
|
|
let gameDate = TestClock.calendar.date(byAdding: .day, value: (i + 1) * 2, to: referenceDate)!
|
|
return game(id: "future_\(i)", sport: sport, city: city, dateTime: gameDate)
|
|
}
|
|
return (past, future, past + future)
|
|
}
|
|
|
|
/// Creates games from sports the user didn't select (for testing sport filtering).
|
|
static func gamesWithWrongSport(
|
|
selectedSport: Sport = .mlb,
|
|
wrongSport: Sport = .nba,
|
|
correctCount: Int = 3,
|
|
wrongCount: Int = 3,
|
|
cities: [String] = ["New York", "Boston", "Chicago"]
|
|
) -> (correct: [Game], wrong: [Game], all: [Game]) {
|
|
let start = TestClock.addingDays(1)
|
|
let correct = (0..<correctCount).map { i in
|
|
let city = cities[i % cities.count]
|
|
let dt = TestClock.calendar.date(byAdding: .day, value: i, to: start)!
|
|
return game(id: "correct_\(i)", sport: selectedSport, city: city, dateTime: dt)
|
|
}
|
|
let wrong = (0..<wrongCount).map { i in
|
|
let city = cities[i % cities.count]
|
|
let dt = TestClock.calendar.date(byAdding: .day, value: i, to: start)!
|
|
return game(id: "wrong_\(i)", sport: wrongSport, city: city, dateTime: dt)
|
|
}
|
|
return (correct, wrong, correct + wrong)
|
|
}
|
|
|
|
/// Creates games referencing stadium IDs that don't exist in any stadium map.
|
|
static func gamesWithMissingStadium(
|
|
count: Int = 3,
|
|
sport: Sport = .mlb
|
|
) -> [Game] {
|
|
let start = TestClock.addingDays(1)
|
|
return (0..<count).map { i in
|
|
let dt = TestClock.calendar.date(byAdding: .day, value: i, to: start)!
|
|
return game(
|
|
id: "orphan_\(i)",
|
|
sport: sport,
|
|
city: "Atlantis",
|
|
dateTime: dt,
|
|
stadiumId: "stadium_nonexistent_\(i)"
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Creates two different games sharing the same ID (simulates rescheduled games).
|
|
static func duplicateIdGames(sport: Sport = .mlb) -> [Game] {
|
|
let dt1 = TestClock.addingDays(2)
|
|
let dt2 = TestClock.addingDays(3)
|
|
return [
|
|
game(id: "dup_game_001", sport: sport, city: "New York", dateTime: dt1),
|
|
game(id: "dup_game_001", sport: sport, city: "Boston", dateTime: dt2),
|
|
]
|
|
}
|
|
|
|
/// Creates games spread over many days for long-trip duration testing.
|
|
static func longTripGames(
|
|
days: Int = 30,
|
|
sport: Sport = .mlb,
|
|
cities: [String] = ["New York", "Boston", "Chicago", "Philadelphia", "Atlanta"]
|
|
) -> [Game] {
|
|
let start = TestClock.addingDays(1)
|
|
return (0..<days).map { i in
|
|
let city = cities[i % cities.count]
|
|
let dt = TestClock.calendar.date(byAdding: .day, value: i, to: start)!
|
|
return game(id: "long_\(i)", sport: sport, city: city, dateTime: dt)
|
|
}
|
|
}
|
|
}
|