From 2634b058f01bdf52dc6946333fb5470d87a718f8 Mon Sep 17 00:00:00 2001 From: David Sullivan Date: Sun, 19 Apr 2026 14:35:00 +0100 Subject: [PATCH] feat: add unit tests and github actions workflow --- .github/workflows/test.yml | 28 +++++++++++++++++ README.md | 3 ++ pyproject.toml | 9 ++++++ tests/test_data_manager.py | 63 ++++++++++++++++++++++++++++++++++++++ tests/test_recorder.py | 34 ++++++++++++++++++++ uv.lock | 60 ++++++++++++++++++++++++++++++++++++ 6 files changed, 197 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 tests/test_data_manager.py create mode 100644 tests/test_recorder.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b1d508b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Set up Python + run: uv python install + + - name: Install dependencies + run: uv sync --dev + + - name: Run tests + run: uv run pytest diff --git a/README.md b/README.md index f751aef..fbe8e65 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # F1 25 Real-Time Telemetry Plotter +[![Tests](https://github.com/sully90/telemetry/actions/workflows/test.yml/badge.svg)](https://github.com/sully90/telemetry/actions) +![Python Version](https://img.shields.io/badge/python-3.12+-blue.svg) + A Python-based real-time telemetry plotter for F1 25. It provides high-frequency visualization of speed, pedal inputs, and racing lines across dual windows, optimized for multi-monitor setups. ![Example Screenshot](screenshot_20260406_133551.png) diff --git a/pyproject.toml b/pyproject.toml index 39e29ca..d6332f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,15 @@ dependencies = [ telemetry = "telemetry.__main__:main" telemetry-playback = "telemetry.playback:main" +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" + +[dependency-groups] +dev = [ + "pytest>=9.0.3", +] diff --git a/tests/test_data_manager.py b/tests/test_data_manager.py new file mode 100644 index 0000000..9d441ed --- /dev/null +++ b/tests/test_data_manager.py @@ -0,0 +1,63 @@ +import pytest +from unittest.mock import MagicMock +from telemetry.data_manager import TelemetryData + +@pytest.fixture +def telemetry_data(): + # Mock TelemetryRecorder to avoid file IO + data = TelemetryData() + data.recorder = MagicMock() + return data + +def test_initialization(telemetry_data): + assert telemetry_data.player_idx == 0 + assert telemetry_data.track_name == "F1 25 Session" + assert len(telemetry_data.all_cars_data) == 22 + assert telemetry_data.current_lap_num == -1 + +def test_update_session(telemetry_data): + telemetry_data.update_session(0, 15, 1) # Melbourne, Race, Player 1 + assert telemetry_data.track_name == "Melbourne" + assert telemetry_data.session_type == 15 + assert telemetry_data.player_idx == 1 + assert telemetry_data.current_lap_data == telemetry_data.all_cars_data[1] + +def test_update_motion_speed_calculation(telemetry_data): + car_idx = 0 + # First update to set initial position + telemetry_data.update_motion(car_idx, 0, 0, 0, 0, 0, 0, 10.0, 1) + + # Second update 1 second later, 10 meters away + # speed = 10 m / 1 s = 10 m/s = 22.3694 mph + telemetry_data.update_motion(car_idx, 10, 0, 0, 0, 0, 0, 11.0, 2) + + latch = telemetry_data.car_latches[car_idx] + assert pytest.approx(latch["speed_mph"], 0.0001) == 22.3694 + +def test_lap_completion(telemetry_data): + telemetry_data.update_session(0, 10, 0) # Melbourne, TT, Player 0 + car_idx = 0 + + # Simulate some data points for a lap + for i in range(1, 103): + telemetry_data.update_lap(car_idx, 1, float(i), i * 1000, float(i), i) + telemetry_data.update_motion(car_idx, i, 0, 0, 0, 0, 0, float(i) + 0.1, i+1) + + # Complete the lap + telemetry_data.update_lap(car_idx, 2, 0.0, 103000, 103.0, 103) + + # Check if history is updated + assert len(telemetry_data.car_histories[car_idx]) == 1 + assert telemetry_data.best_lap_time != float('inf') + assert telemetry_data.current_lap_num == 2 + +def test_flashback_reset(telemetry_data): + car_idx = 0 + telemetry_data.update_lap(car_idx, 2, 1000.0, 50000, 50.0, 50) + telemetry_data.update_motion(car_idx, 100, 0, 0, 0, 0, 0, 50.0, 50) + assert len(telemetry_data.all_cars_data[car_idx]["distance"]) > 0 + + # Simulate flashback (lap number decreases or distance jumps back) + telemetry_data.update_lap(car_idx, 1, 500.0, 25000, 60.0, 60) + + assert len(telemetry_data.all_cars_data[car_idx]["distance"]) == 0 diff --git a/tests/test_recorder.py b/tests/test_recorder.py new file mode 100644 index 0000000..6325f81 --- /dev/null +++ b/tests/test_recorder.py @@ -0,0 +1,34 @@ +import os +import pytest +import pandas as pd +from telemetry.recorder import TelemetryRecorder + +def test_recorder_flow(tmp_path): + # Use a temporary directory for recordings + recorder = TelemetryRecorder(output_dir=str(tmp_path)) + + # Mock data + track_name = "Melbourne" + units = {"speed": "mph"} + + recorder.start_recording(track_name, units) + assert recorder.is_recording + + sample = { + "car_idx": 0, + "distance": 10.5, + "speed": 150.0 + } + recorder.add_sample(sample) + assert len(recorder.recording_log) == 1 + + filepath = recorder.stop_recording() + assert not recorder.is_recording + assert os.path.exists(filepath) + assert filepath.endswith(".parquet") + + # Verify data can be read back + df = recorder.read_recording(filepath) + assert len(df) == 1 + assert df.iloc[0]["car_idx"] == 0 + assert df.iloc[0]["distance"] == 10.5 diff --git a/uv.lock b/uv.lock index 5b4b647..d48481f 100644 --- a/uv.lock +++ b/uv.lock @@ -19,6 +19,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "numpy" version = "2.4.4" @@ -80,6 +89,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, ] +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + [[package]] name = "pandas" version = "3.0.2" @@ -132,6 +150,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pyarrow" version = "23.0.1" @@ -175,6 +202,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/f2/c0e76a0b451ffdf0cf788932e182758eb7558953f4f27f1aff8e2518b653/pyarrow-23.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", size = 28365807, upload-time = "2026-02-16T10:14:03.892Z" }, ] +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + [[package]] name = "pyqt5" version = "5.15.11" @@ -238,6 +274,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/36/4c242f81fdcbfa4fb62a5645f6af79191f4097a0577bd5460c24f19cc4ef/pyqtgraph-0.14.0-py3-none-any.whl", hash = "sha256:7abb7c3e17362add64f8711b474dffac5e7b0e9245abdf992e9a44119b7aa4f5", size = 1924755, upload-time = "2025-11-16T19:43:22.251Z" }, ] +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -273,6 +325,11 @@ dependencies = [ { name = "websockets" }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + [package.metadata] requires-dist = [ { name = "numpy", specifier = ">=2.4.4" }, @@ -284,6 +341,9 @@ requires-dist = [ { name = "websockets", specifier = ">=16.0" }, ] +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=9.0.3" }] + [[package]] name = "tzdata" version = "2026.1"