File size: 7,417 Bytes
dec0e0c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | """Processing in fetch_data.py: no network, synthetic inputs with known answers."""
from datetime import datetime, timedelta, timezone
import numpy as np
import pytest
import fetch_data as fd
T0 = datetime(2025, 3, 1, 15, 0, tzinfo=timezone.utc)
CIRCUIT = "Testring"
def iso(seconds: float) -> str:
return (T0 + timedelta(seconds=seconds)).isoformat()
# ββ names and colours βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_race_filename_is_a_plain_name():
assert fd.race_filename(2024, "SΓ£o Paulo Grand Prix", "R") == "2024_sΓ£o_paulo_grand_prix_r.json"
# Separators from a crafted name must never reach the file system
for name in ("../../etc/passwd", "..\\..\\serve", "a/b\\c"):
f = fd.race_filename(2024, name, "R")
assert "/" not in f and "\\" not in f
@pytest.mark.parametrize("raw, expected", [
("3671c6", "#3671c6"), ("#ff8000", "#ff8000"), (None, "#ffffff"), ("nan", "#ffffff"), ("", "#ffffff"),
])
def test_hex_color(raw, expected):
assert fd.hex_color(raw) == expected
# ββ track geometry: race distance comes from snapping onto the outline ββββββ
def circle(radius=1000.0, n=2000):
a = np.linspace(0, 2 * np.pi, n, endpoint=False)
return np.c_[radius * np.cos(a), radius * np.sin(a)]
def test_snap_gives_distance_along_the_lap():
loop = fd._Loop(circle())
assert loop.length == pytest.approx(2 * np.pi * 1000, rel=1e-3)
# A quarter of the way round, 5 units off the line
s, off = loop.snap(np.array([0.0]), np.array([1005.0]))
assert s[0] == pytest.approx(loop.length / 4, rel=2e-3)
assert off[0] == pytest.approx(5.0, abs=1.0)
def test_unwrap_counts_laps():
loop = fd._Loop(circle())
a = np.linspace(0, 3 * 2 * np.pi, 600) # three laps, sampled densely
s, _ = loop.snap(1000 * np.cos(a), 1000 * np.sin(a))
cum = fd._unwrap(s, loop.length)
assert cum[-1] - cum[0] == pytest.approx(3 * loop.length, rel=0.01)
assert np.all(np.diff(cum) > -1) # never jumps back a lap
# ββ race control -> FastF1-style track status codes βββββββββββββββββββββββββ
def test_track_status_codes():
rc = [
{"date": iso(10), "category": "SafetyCar", "message": "SAFETY CAR DEPLOYED"},
{"date": iso(20), "category": "SafetyCar", "message": "SAFETY CAR IN THIS LAP"},
{"date": iso(30), "category": "Flag", "flag": "CLEAR", "scope": "Track", "message": "TRACK CLEAR"},
{"date": iso(40), "category": "SafetyCar", "message": "VIRTUAL SAFETY CAR DEPLOYED"},
{"date": iso(50), "category": "SafetyCar", "message": "VIRTUAL SAFETY CAR ENDING"},
{"date": iso(60), "category": "Flag", "flag": "CLEAR", "scope": "Sector", "message": "CLEAR IN TRACK SECTOR 4"},
{"date": iso(70), "category": "Flag", "flag": "RED", "scope": "Track", "message": "RED FLAG"},
{"date": iso(80), "category": "Flag", "flag": "DOUBLE YELLOW", "scope": "Track", "message": "DOUBLE YELLOW"},
]
status = fd._track_status(rc, T0.timestamp())
assert [(t, code) for t, code, _ in status] == [
(10, "4"), (30, "1"), (40, "6"), (50, "1"), (70, "5"), (80, "2")]
# ββ lap windows, including a retirement mid-lap βββββββββββββββββββββββββββββ
def test_lap_windows_end_a_retirement_when_the_car_stops():
laps = [{"lap_number": 1, "date_start": iso(0), "lap_duration": 90.0},
{"lap_number": 2, "date_start": iso(90), "lap_duration": None}]
t = T0.timestamp() + np.arange(0, 200, 1.0)
x = np.where(t - T0.timestamp() < 130, (t - T0.timestamp()) * 50, 130 * 50) # stops at 130 s
y = np.zeros_like(x)
w = fd._lap_windows(laps, t, x, y)
assert [n for n, _, _ in w] == [1, 2]
assert w[0][2] - T0.timestamp() == pytest.approx(90)
assert w[1][2] - T0.timestamp() == pytest.approx(130, abs=1.5)
# ββ pit stops: OpenF1's pit time is the exit, not the entry βββββββββββββββββ
def test_pit_window_ends_at_the_pit_time():
stops = [{"driver_number": 1, "date": iso(1000), "lane_duration": 24.2},
{"driver_number": 1, "date": iso(3000), "pit_duration": 30.0}, # older field name
{"driver_number": 1, "date": iso(5000)}, # no duration: 25 s
{"driver_number": 2, "date": iso(1500), "lane_duration": 20.0}]
assert fd.pit_windows(stops, 1, T0.timestamp()) == [[975.8, 1000.0], [2970.0, 3000.0], [4975.0, 5000.0]]
# ββ pit lane traced from a real stop ββββββββββββββββββββββββββββββββββββββββ
def test_pit_lane_is_traced_during_the_stop_window():
t = T0.timestamp() + np.arange(0, 100, 0.25)
raw = {44: (t, (t - T0.timestamp()) * 10, np.zeros_like(t))}
stops = [{"driver_number": 44, "date": iso(60), "lane_duration": 20.0}] # pit exit at 60 s
lane = fd._pit_lane_from_stops(stops, raw)
xs = [p[0] for p in lane]
assert min(xs) >= 400 - 1e-6 and max(xs) <= 600 + 1e-6 # x = 10 * t for t in [40, 60]
assert len(lane) >= 70
assert fd._max_jump(lane) < 5
def test_no_pit_lane_without_timed_stops():
raw = {1: (np.arange(10.0), np.arange(10.0), np.arange(10.0))}
assert fd._pit_lane_from_stops([{"driver_number": 1, "date": iso(5)}], raw) is None
# ββ per-lap table used by the strategy model ββββββββββββββββββββββββββββββββ
def test_lap_rows():
session = {"session_key": 7, "year": 2025, "date_start": iso(0), "circuit_short_name": CIRCUIT}
laps = [{"driver_number": 1, "lap_number": n, "date_start": iso(90 * (n - 1)), "lap_duration": 90.0,
"is_pit_out_lap": n == 3} for n in (1, 2, 3)]
stints = [{"driver_number": 1, "stint_number": 1, "lap_start": 1, "lap_end": 2, "compound": "SOFT", "tyre_age_at_start": 3},
{"driver_number": 1, "stint_number": 2, "lap_start": 3, "lap_end": 3, "compound": "HARD", "tyre_age_at_start": 0}]
pits = [{"driver_number": 1, "lap_number": 2, "date": iso(185), "lane_duration": 22.0}]
weather = [{"date": iso(0), "track_temperature": 31.0, "air_temperature": 22.0, "rainfall": 0},
{"date": iso(150), "track_temperature": 33.0, "air_temperature": 22.0, "rainfall": 0}]
rc = [{"date": iso(200), "category": "SafetyCar", "message": "SAFETY CAR DEPLOYED"},
{"date": iso(400), "category": "Flag", "flag": "CLEAR", "scope": "Track", "message": "TRACK CLEAR"}]
rows = fd.lap_rows(session, laps, stints, pits, weather, rc, {1: {"team_name": "Team"}})
by = {r["lap"]: r for r in rows}
assert [by[n]["compound"] for n in (1, 2, 3)] == ["SOFT", "SOFT", "HARD"]
assert [by[n]["tyre_age"] for n in (1, 2, 3)] == [3, 4, 0]
assert [by[n]["pit_in_lap"] for n in (1, 2, 3)] == [0, 1, 0]
assert [by[n]["pit_out_lap"] for n in (1, 2, 3)] == [0, 0, 1]
assert [by[n]["neutralised"] for n in (1, 2, 3)] == [0, 0, 1] # SC from 200 s, lap 3 = 180-270 s
assert [by[n]["track_temp"] for n in (1, 2, 3)] == [31.0, 31.0, 33.0] # weather at each lap start
assert by[2]["start"] == 90 and by[1]["circuit"] == CIRCUIT
|