mirror of
https://github.com/sstent/FitTrack_ReportGenerator.git
synced 2026-01-28 01:51:39 +00:00
feat: Initial implementation of FitTrack Report Generator
This commit introduces the initial version of the FitTrack Report Generator, a FastAPI application for analyzing workout files. Key features include: - Parsing of FIT, TCX, and GPX workout files. - Analysis of power, heart rate, speed, and elevation data. - Generation of summary reports and charts. - REST API for single and batch workout analysis. The project structure has been set up with a `src` directory for core logic, an `api` directory for the FastAPI application, and a `tests` directory for unit, integration, and contract tests. The development workflow is configured to use Docker and modern Python tooling.
This commit is contained in:
139
tests/unit/test_batch_processor.py
Normal file
139
tests/unit/test_batch_processor.py
Normal file
@@ -0,0 +1,139 @@
|
||||
import pytest
|
||||
import zipfile
|
||||
import io
|
||||
from unittest.mock import MagicMock, patch
|
||||
from src.core.batch_processor import BatchProcessor
|
||||
from src.core.workout_data import WorkoutData, WorkoutMetadata
|
||||
from datetime import datetime, timedelta
|
||||
import pandas as pd
|
||||
|
||||
@pytest.fixture
|
||||
def mock_workout_data():
|
||||
metadata = WorkoutMetadata(
|
||||
start_time=datetime(2025, 1, 1, 10, 0, 0),
|
||||
duration=timedelta(minutes=10),
|
||||
device="Garmin",
|
||||
file_type="FIT"
|
||||
)
|
||||
time_series_data = pd.DataFrame({
|
||||
"power": [100, 110, 120],
|
||||
"heart_rate": [150, 155, 160]
|
||||
})
|
||||
return WorkoutData(metadata=metadata, time_series_data=time_series_data)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_file_parser():
|
||||
parser = MagicMock()
|
||||
parser.parse.return_value = MagicMock(spec=WorkoutData)
|
||||
return parser
|
||||
|
||||
@pytest.fixture
|
||||
def mock_workout_analyzer():
|
||||
analyzer = MagicMock()
|
||||
analyzer.calculate_summary_metrics.return_value = {"avg_power": 100}
|
||||
return analyzer
|
||||
|
||||
@pytest.fixture
|
||||
def mock_report_generator():
|
||||
generator = MagicMock()
|
||||
generator.generate_html_report.return_value = "<html>report</html>"
|
||||
return generator
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_session():
|
||||
session = MagicMock()
|
||||
return session
|
||||
|
||||
def create_zip_file(file_names_and_content):
|
||||
zip_buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
for name, content in file_names_and_content.items():
|
||||
zf.writestr(name, content)
|
||||
zip_buffer.seek(0)
|
||||
return zip_buffer
|
||||
|
||||
def test_batch_processor_initialization(mock_db_session):
|
||||
processor = BatchProcessor(db_session=mock_db_session)
|
||||
assert processor.db_session == mock_db_session
|
||||
|
||||
@patch('src.core.file_parser.FitParser')
|
||||
@patch('src.core.file_parser.TcxParser')
|
||||
@patch('src.core.file_parser.GpxParser')
|
||||
@patch('src.core.workout_analyzer.WorkoutAnalyzer')
|
||||
@patch('src.core.report_generator.ReportGenerator')
|
||||
def test_process_zip_file_single_fit(mock_report_generator_cls, mock_workout_analyzer_cls, mock_gpx_parser_cls, mock_tcx_parser_cls, mock_fit_parser_cls, mock_db_session, mock_workout_data):
|
||||
# Mock parsers to return mock_workout_data
|
||||
mock_fit_parser_cls.return_value.parse.return_value = mock_workout_data
|
||||
mock_workout_analyzer_cls.return_value.calculate_summary_metrics.return_value = {"avg_power": 100}
|
||||
mock_report_generator_cls.return_value.generate_html_report.return_value = "<html>report</html>"
|
||||
|
||||
zip_content = create_zip_file({"workout.fit": b"dummy_fit_content"})
|
||||
processor = BatchProcessor(db_session=mock_db_session)
|
||||
results = processor.process_zip_file(zip_content, user_id=None, ftp_value=None)
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0]["file_name"] == "workout.fit"
|
||||
assert results[0]["status"] == "completed"
|
||||
mock_fit_parser_cls.return_value.parse.assert_called_once()
|
||||
mock_workout_analyzer_cls.assert_called_once()
|
||||
mock_db_session.add.assert_called_once()
|
||||
mock_db_session.commit.assert_called_once()
|
||||
|
||||
@patch('src.core.file_parser.FitParser')
|
||||
@patch('src.core.file_parser.TcxParser')
|
||||
@patch('src.core.file_parser.GpxParser')
|
||||
@patch('src.core.workout_analyzer.WorkoutAnalyzer')
|
||||
@patch('src.core.report_generator.ReportGenerator')
|
||||
def test_process_zip_file_multiple_files(mock_report_generator_cls, mock_workout_analyzer_cls, mock_gpx_parser_cls, mock_tcx_parser_cls, mock_fit_parser_cls, mock_db_session, mock_workout_data):
|
||||
mock_fit_parser_cls.return_value.parse.return_value = mock_workout_data
|
||||
mock_tcx_parser_cls.return_value.parse.return_value = mock_workout_data
|
||||
mock_workout_analyzer_cls.return_value.calculate_summary_metrics.return_value = {"avg_power": 100}
|
||||
mock_report_generator_cls.return_value.generate_html_report.return_value = "<html>report</html>"
|
||||
|
||||
zip_content = create_zip_file({"workout1.fit": b"dummy_fit_content", "workout2.tcx": b"dummy_tcx_content"})
|
||||
processor = BatchProcessor(db_session=mock_db_session)
|
||||
results = processor.process_zip_file(zip_content, user_id=None, ftp_value=None)
|
||||
|
||||
assert len(results) == 2
|
||||
assert any(r["file_name"] == "workout1.fit" for r in results)
|
||||
assert any(r["file_name"] == "workout2.tcx" for r in results)
|
||||
assert all(r["status"] == "completed" for r in results)
|
||||
assert mock_fit_parser_cls.return_value.parse.call_count == 1
|
||||
assert mock_tcx_parser_cls.return_value.parse.call_count == 1
|
||||
assert mock_workout_analyzer_cls.call_count == 2
|
||||
assert mock_db_session.add.call_count == 2
|
||||
assert mock_db_session.commit.call_count == 2
|
||||
|
||||
@patch('src.core.file_parser.FitParser')
|
||||
@patch('src.core.workout_analyzer.WorkoutAnalyzer')
|
||||
def test_process_zip_file_unsupported_file_type(mock_workout_analyzer_cls, mock_fit_parser_cls, mock_db_session):
|
||||
zip_content = create_zip_file({"document.txt": b"some text"})
|
||||
processor = BatchProcessor(db_session=mock_db_session)
|
||||
results = processor.process_zip_file(zip_content, user_id=None, ftp_value=None)
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0]["file_name"] == "document.txt"
|
||||
assert results[0]["status"] == "failed"
|
||||
assert "Unsupported file type" in results[0]["error_message"]
|
||||
mock_fit_parser_cls.return_value.parse.assert_not_called()
|
||||
mock_workout_analyzer_cls.assert_not_called()
|
||||
mock_db_session.add.assert_not_called()
|
||||
mock_db_session.commit.assert_not_called()
|
||||
|
||||
@patch('src.core.file_parser.FitParser')
|
||||
@patch('src.core.workout_analyzer.WorkoutAnalyzer')
|
||||
def test_process_zip_file_parsing_error(mock_workout_analyzer_cls, mock_fit_parser_cls, mock_db_session):
|
||||
mock_fit_parser_cls.return_value.parse.side_effect = Exception("Corrupted file")
|
||||
|
||||
zip_content = create_zip_file({"corrupted.fit": b"bad content"})
|
||||
processor = BatchProcessor(db_session=mock_db_session)
|
||||
results = processor.process_zip_file(zip_content, user_id=None, ftp_value=None)
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0]["file_name"] == "corrupted.fit"
|
||||
assert results[0]["status"] == "failed"
|
||||
assert "Corrupted file" in results[0]["error_message"]
|
||||
mock_fit_parser_cls.return_value.parse.assert_called_once()
|
||||
mock_workout_analyzer_cls.assert_not_called()
|
||||
mock_db_session.add.assert_not_called()
|
||||
mock_db_session.commit.assert_not_called()
|
||||
Reference in New Issue
Block a user