| """Unit tests for the Commentary tab handler (Issue #10).""" |
|
|
| import sys |
| import types |
| import tempfile |
| from pathlib import Path |
| from unittest.mock import patch, MagicMock |
|
|
| import pandas as pd |
| import numpy as np |
| import pytest |
|
|
| sys.path.insert(0, str(Path(__file__).parent.parent.parent)) |
|
|
|
|
| |
| |
| |
|
|
| @pytest.fixture() |
| def lap_df(): |
| rows = [] |
| for lap in range(15, 26): |
| for driver, pos, team, gap in [ |
| ("VER", 1, "Red Bull Racing", 0.0), |
| ("NOR", 2, "McLaren", 4.5), |
| ]: |
| rows.append({ |
| "lap_number": lap, |
| "driver_code": driver, |
| "team": team, |
| "position": pos, |
| "gap_to_leader_s": gap, |
| "compound": "MEDIUM", |
| "tyre_life": lap - 10, |
| "lap_time_s": 90.1, |
| "sc_active": False, |
| }) |
| return pd.DataFrame(rows) |
|
|
|
|
| @pytest.fixture() |
| def race(): |
| return {"season": 2024, "round": 21, "name": "Brazilian GP", "circuit": "Interlagos", "modes": ["commentary"]} |
|
|
|
|
| |
| |
| |
|
|
| class TestTopTwoDrivers: |
| def test_returns_p1_p2_at_pivot_lap(self, lap_df): |
| with patch("app._CACHE_DIR") as mock_cache, \ |
| patch("app._race_key", return_value="2024_Brazilian_GP"), \ |
| patch("pandas.read_parquet", return_value=lap_df): |
| from app import _top_two_drivers |
| driver_a, driver_b, team_name = _top_two_drivers(2024, 21, 20) |
| assert driver_a == "VER" |
| assert driver_b == "NOR" |
| assert team_name == "Red Bull Racing" |
|
|
| def test_falls_back_to_last_lap_when_pivot_missing(self, lap_df): |
| |
| with patch("app._race_key", return_value="2024_Brazilian_GP"), \ |
| patch("pandas.read_parquet", return_value=lap_df): |
| from app import _top_two_drivers |
| driver_a, driver_b, team_name = _top_two_drivers(2024, 21, 99) |
| assert driver_a == "VER" |
| assert driver_b == "NOR" |
|
|
|
|
| |
| |
| |
|
|
| class TestGenerateCommentary: |
| def _make_wav_bytes(self): |
| """Minimal valid 16-bit WAV bytes (44-byte header + 2 bytes of silence).""" |
| import struct |
| data = b"\x00\x00" |
| header = struct.pack( |
| "<4sI4s4sIHHIIHH4sI", |
| b"RIFF", 36 + len(data), b"WAVE", |
| b"fmt ", 16, 1, 1, 24000, 48000, 2, 16, |
| b"data", len(data), |
| ) |
| return header + data |
|
|
| def test_broadcast_returns_audio_path_and_text(self, race, lap_df): |
| audio_bytes = self._make_wav_bytes() |
| modal_result = {"text": "Verstappen extends his lead.", "audio_wav": audio_bytes} |
|
|
| with patch("app._top_two_drivers", return_value=("VER", "NOR", "Red Bull Racing")), \ |
| patch("app.get_lap_window", return_value=lap_df), \ |
| patch("app.build_commentary_prompt", return_value="[prompt]"), \ |
| patch("app.call_generate_commentary", return_value=modal_result): |
| from app import _generate_commentary |
| audio_path, text = _generate_commentary(race, 20, "Broadcast") |
|
|
| assert text == "Verstappen extends his lead." |
| assert audio_path is not None |
| assert audio_path.endswith(".wav") |
|
|
| def test_radio_passes_mode_radio_to_prompt_builder(self, race, lap_df): |
| modal_result = {"text": "Box this lap.", "audio_wav": b""} |
|
|
| with patch("app._top_two_drivers", return_value=("VER", "NOR", "Red Bull Racing")), \ |
| patch("app.get_lap_window", return_value=lap_df), \ |
| patch("app.build_commentary_prompt", return_value="[prompt]") as mock_build, \ |
| patch("app.call_generate_commentary", return_value=modal_result): |
| from app import _generate_commentary |
| _generate_commentary(race, 20, "Radio") |
|
|
| mock_build.assert_called_once_with(lap_df, "Red Bull Racing", "radio") |
|
|
| def test_broadcast_passes_mode_broadcast_to_prompt_builder(self, race, lap_df): |
| modal_result = {"text": "Hamilton closes in.", "audio_wav": b""} |
|
|
| with patch("app._top_two_drivers", return_value=("VER", "HAM", "Red Bull Racing")), \ |
| patch("app.get_lap_window", return_value=lap_df), \ |
| patch("app.build_commentary_prompt", return_value="[prompt]") as mock_build, \ |
| patch("app.call_generate_commentary", return_value=modal_result): |
| from app import _generate_commentary |
| _generate_commentary(race, 20, "Broadcast") |
|
|
| mock_build.assert_called_once_with(lap_df, "Red Bull Racing", "broadcast") |
|
|
| def test_no_audio_bytes_returns_none_path(self, race, lap_df): |
| modal_result = {"text": "VER leads.", "audio_wav": b""} |
|
|
| with patch("app._top_two_drivers", return_value=("VER", "NOR", "Red Bull Racing")), \ |
| patch("app.get_lap_window", return_value=lap_df), \ |
| patch("app.build_commentary_prompt", return_value="[prompt]"), \ |
| patch("app.call_generate_commentary", return_value=modal_result): |
| from app import _generate_commentary |
| audio_path, text = _generate_commentary(race, 20, "Broadcast") |
|
|
| assert audio_path is None |
| assert text == "VER leads." |
|
|
|
|
| |
| |
| |
|
|
| class TestLoadingMessages: |
| def test_first_call_returns_pit_wall_message(self): |
| from modal_backend.client import get_commentary_loading_message |
| import modal_backend.client as client_mod |
| original = client_mod._commentary_call_count |
| try: |
| client_mod._commentary_call_count = 0 |
| msg = get_commentary_loading_message() |
| assert "pit wall" in msg.lower() |
| finally: |
| client_mod._commentary_call_count = original |
|
|
| def test_subsequent_call_returns_shorter_message(self): |
| from modal_backend.client import get_commentary_loading_message |
| import modal_backend.client as client_mod |
| original = client_mod._commentary_call_count |
| try: |
| client_mod._commentary_call_count = 1 |
| msg = get_commentary_loading_message() |
| assert msg != "Connecting to the pit wall… (~20s first call)" |
| finally: |
| client_mod._commentary_call_count = original |
|
|