import os from datetime import datetime from unittest.mock import Mock import pytest from fitparse import FitFile from fitparse.utils import FitHeaderError from ow.fit import Fit class TestFit(object): def get_fixture_path(self, filename): here = os.path.abspath(os.path.dirname(__file__)) path = os.path.join(here, 'fixtures', filename) return path @pytest.fixture def fit(self): # fit file used in most of the tests below fit_path = self.get_fixture_path('20181230_101115.fit') fit = Fit(fit_path) return fit @pytest.fixture def values(self): return { 'sport': 'cycling', 'unknown_110': 'OpenWorkouts-testing', 'start_time': datetime.now(), 'total_timer_time': 3600, 'total_elapsed_time': 3700, 'total_distance': 60000, 'total_ascent': 1200, 'total_descent': 1100, 'total_calories': 2000, 'max_heart_rate': 180, 'avg_heart_rate': 137, 'max_cadence': 111, 'avg_cadence': 90, 'enhanced_max_speed': 16.666, 'max_speed': 18666, 'enhanced_avg_speed': 7.638, 'avg_speed': 7838 } @pytest.fixture def expected(self, values): return { 'sport': 'cycling', 'profile': 'OpenWorkouts-testing', 'start': values['start_time'], 'duration': 3600, 'elapsed': 3700, 'distance': 60000, 'uphill': 1200, 'downhill': 1100, 'calories': 2000, 'hr': [], 'min_hr': None, 'max_hr': 180, 'avg_hr': 137, 'cad': [], 'min_cad': None, 'max_cad': 111, 'avg_cad': 90, 'max_speed': 16.666, 'avg_speed': 7.638, 'atemp': [], 'min_atemp': None, 'max_atemp': 0, 'avg_atemp': 0, } fit_files = [ ('non-existant.fit', FileNotFoundError), ('20131013.gpx', FitHeaderError), # GPX file ('20181230_101115.fit', None), # FIT file ] @pytest.mark.parametrize(('filename', 'expected'), fit_files) def test__init__(self, filename, expected): fit_path = self.get_fixture_path(filename) if expected is None: fit = Fit(fit_path) assert isinstance(fit, Fit) assert fit.path == fit_path assert isinstance(fit.obj, FitFile) assert fit.data == {} else: with pytest.raises(expected): Fit(fit_path) def test_load_real_fit_file(self, fit): """ Test loading data from a real fit file from a Garmin device """ assert fit.data == {} fit.load() assert len(fit.data.keys()) == 23 def test_load_extra_cases(self, fit, values, expected): """ Test loading data from some mocked file, so we can be sure all the loading code is executed and covered by tests """ # first mock the FitFile object in the fit object session = Mock() session.get_values.return_value = values fit.obj = Mock() # mock get_messages, no matter what we ask for, return a single # session list, which fits for the purpose of this test fit.obj.get_messages.return_value = [session] assert fit.data == {} # first load, we have all data we are supposed to have in the fit # file, so results are more or less like in the real test fit.load() assert len(fit.data.keys()) == 23 for k in expected.keys(): assert fit.data[k] == expected[k] # now, remove enhanced_max_speed, so the max_speed parameter is used values['enhanced_max_speed'] = None fit.load() assert len(fit.data.keys()) == 23 for k in expected.keys(): if k == 'max_speed': assert fit.data[k] != expected[k] assert fit.data[k] == 18.666 else: assert fit.data[k] == expected[k] # now, do the same for the avg speed values['enhanced_max_speed'] = 16.666 values['enhanced_avg_speed'] = None fit.load() assert len(fit.data.keys()) == 23 for k in expected.keys(): if k == 'avg_speed': assert fit.data[k] != expected[k] assert fit.data[k] == 7.838 else: assert fit.data[k] == expected[k] def test_name(self, fit): # without loading first, no data, so it blows up with pytest.raises(KeyError): fit.name fit.load() # the default fit file has both profile and sport assert fit.name == 'Synapse cycling' # remove profile fit.data['profile'] = None assert fit.name == 'cycling' # change sport fit.data['sport'] = 'running' assert fit.name == 'running' def test__calculate_avg_atemp(self, fit): # before loading data, we don't have any info about the average temp assert 'avg_atemp' not in fit.data.keys() # loaded data, fit file didn't have that info pre-calculated fit.load() assert fit.data['avg_atemp'] == 0 # this loads the needed temperature data into fit.data + calls # _calculate_avg_atemp fit.gpx # now we have the needed info assert fit.data['avg_atemp'] == 2 def test_gpx_without_load(self, fit): # without loading first, no data, so it blows up with pytest.raises(KeyError): fit.gpx def test_gpx_real_fit_file(self, fit): """ Test the gpx generation code with a real fit file from a garmin device """ fit.load() # open a pre-saved gpx file, the generated gpx file has to be like # this one gpx_path = self.get_fixture_path('20181230_101115.gpx') with open(gpx_path, 'r') as gpx_file: expected = gpx_file.read() assert fit.gpx == expected def test_gpx_extra_cases(self, fit, values): """ Test the gpx generation code with some mocked data, so we can be sure all the code is executed and covered by tests """ # first mock the FitFile object in the fit object session = Mock() session.get_values.return_value = values fit.obj = Mock() # mock get_messages, no matter what we ask for, return a single # session list, which fits for the purpose of this test fit.obj.get_messages.return_value = [session] fit.load() # after loading data, mock again get_messages, this time return the # list of messages we will loop over to generate the gpx object first_values = { 'temperature': 18, 'heart_rate': 90, 'cadence': 40, 'position_long': -104549248, 'position_lat': 508158836, 'enhanced_altitude': 196.79999999999995, 'enhanced_speed': 5.432, 'timestamp': datetime.now() } first_record = Mock() first_record.get_values.return_value = first_values second_values = { 'temperature': 16, 'heart_rate': 110, 'cadence': 70, 'position_long': -104532648, 'position_lat': 508987836, 'enhanced_altitude': 210.79999999999995, 'enhanced_speed': 10.432, 'timestamp': datetime.now() } second_record = Mock() second_record.get_values.return_value = second_values third_values = { 'temperature': 7, 'heart_rate': 140, 'cadence': 90, 'position_long': -104532876, 'position_lat': 508987987, 'enhanced_altitude': 250.79999999999995, 'enhanced_speed': 9.432, 'timestamp': datetime.now() } third_record = Mock() third_record.get_values.return_value = third_values # set an hr value of 0, which will trigger the code that sets # the minimum heart rate value from the last known value fourth_values = { 'temperature': -2, 'heart_rate': 0, 'cadence': 0, 'position_long': -104532876, 'position_lat': 508987987, 'enhanced_altitude': 250.79999999999995, 'enhanced_speed': 9.432, 'timestamp': datetime.now() } fourth_record = Mock() fourth_record.get_values.return_value = fourth_values records = [ first_record, second_record, third_record, fourth_record, ] fit.obj.get_messages.return_value = records xml = fit.gpx # now, ensure the proper data is in the xml file # # no 0 hr value is in there assert '0' not in xml # but the previous value is twice assert xml.count('140') == 2 # the other values appear once assert xml.count('90') == 1 assert xml.count('110') == 1 for v in [18, 16, 7, -2]: assert '' + str(v) + '' in xml for v in [40, 70, 90, 0]: assert '' + str(v) + '' in xml # the name provided by the fit object is there assert '' + fit.name + '' in xml # and we have 4 track points, no need to check the latitude/longitude # conversion, as that is covered in the previous test with a real fit # and gpx files test assert xml.count('