Files
FitTrack_ReportGenerator/tests/unit/test_batch_processor.py
sstent 9e0bd322d3 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.
2025-10-11 09:54:13 -07:00

139 lines
6.1 KiB
Python

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()