source: OpenWorkouts-current/ow/tests/test_fit.py @ 737eb6c

currentfeature/docs
Last change on this file since 737eb6c was b73ae09, checked in by borja <borja@…>, 5 years ago

Added missing tests covering ow.fit, ow.models.workout and ow.utilities

  • Property mode set to 100644
File size: 9.7 KB
Line 
1import os
2from datetime import datetime
3from unittest.mock import Mock
4
5import pytest
6from fitparse import FitFile
7from fitparse.utils import FitHeaderError
8
9from ow.fit import Fit
10
11
12class TestFit(object):
13
14    def get_fixture_path(self, filename):
15        here = os.path.abspath(os.path.dirname(__file__))
16        path = os.path.join(here, 'fixtures', filename)
17        return path
18
19    @pytest.fixture
20    def fit(self):
21        # fit file used in most of the tests below
22        fit_path = self.get_fixture_path('20181230_101115.fit')
23        fit = Fit(fit_path)
24        return fit
25
26    @pytest.fixture
27    def values(self):
28        return {
29            'sport': 'cycling',
30            'unknown_110': 'OpenWorkouts-testing',
31            'start_time': datetime.now(),
32            'total_timer_time': 3600,
33            'total_elapsed_time': 3700,
34            'total_distance': 60000,
35            'total_ascent': 1200,
36            'total_descent': 1100,
37            'total_calories': 2000,
38            'max_heart_rate': 180,
39            'avg_heart_rate': 137,
40            'max_cadence': 111,
41            'avg_cadence': 90,
42            'enhanced_max_speed': 16.666,
43            'max_speed': 18666,
44            'enhanced_avg_speed': 7.638,
45            'avg_speed': 7838
46        }
47
48    @pytest.fixture
49    def expected(self, values):
50        return {
51            'sport': 'cycling',
52            'profile': 'OpenWorkouts-testing',
53            'start': values['start_time'],
54            'duration': 3600,
55            'elapsed': 3700,
56            'distance': 60000,
57            'uphill': 1200,
58            'downhill': 1100,
59            'calories': 2000,
60            'hr': [],
61            'min_hr': None,
62            'max_hr': 180,
63            'avg_hr': 137,
64            'cad': [],
65            'min_cad': None,
66            'max_cad': 111,
67            'avg_cad': 90,
68            'max_speed': 16.666,
69            'avg_speed': 7.638,
70            'atemp': [],
71            'min_atemp': None,
72            'max_atemp': 0,
73            'avg_atemp': 0,
74        }
75
76    fit_files = [
77        ('non-existant.fit', FileNotFoundError),
78        ('20131013.gpx', FitHeaderError),  # GPX file
79        ('20181230_101115.fit', None),  # FIT file
80    ]
81
82    @pytest.mark.parametrize(('filename', 'expected'), fit_files)
83    def test__init__(self, filename, expected):
84        fit_path = self.get_fixture_path(filename)
85        if expected is None:
86            fit = Fit(fit_path)
87            assert isinstance(fit, Fit)
88            assert fit.path == fit_path
89            assert isinstance(fit.obj, FitFile)
90            assert fit.data == {}
91        else:
92            with pytest.raises(expected):
93                Fit(fit_path)
94
95    def test_load_real_fit_file(self, fit):
96        """
97        Test loading data from a real fit file from a Garmin device
98        """
99        assert fit.data == {}
100        fit.load()
101        assert len(fit.data.keys()) == 23
102
103    def test_load_extra_cases(self, fit, values, expected):
104        """
105        Test loading data from some mocked file, so we can be sure all the
106        loading code is executed and covered by tests
107        """
108        # first mock the FitFile object in the fit object
109        session = Mock()
110        session.get_values.return_value = values
111        fit.obj = Mock()
112        # mock get_messages, no matter what we ask for, return a single
113        # session list, which fits for the purpose of this test
114        fit.obj.get_messages.return_value = [session]
115
116        assert fit.data == {}
117
118        # first load, we have all data we are supposed to have in the fit
119        # file, so results are more or less like in the real test
120        fit.load()
121        assert len(fit.data.keys()) == 23
122        for k in expected.keys():
123            assert fit.data[k] == expected[k]
124
125        # now, remove enhanced_max_speed, so the max_speed parameter is used
126        values['enhanced_max_speed'] = None
127        fit.load()
128        assert len(fit.data.keys()) == 23
129        for k in expected.keys():
130            if k == 'max_speed':
131                assert fit.data[k] != expected[k]
132                assert fit.data[k] == 18.666
133            else:
134                assert fit.data[k] == expected[k]
135
136        # now, do the same for the avg speed
137        values['enhanced_max_speed'] = 16.666
138        values['enhanced_avg_speed'] = None
139        fit.load()
140        assert len(fit.data.keys()) == 23
141        for k in expected.keys():
142            if k == 'avg_speed':
143                assert fit.data[k] != expected[k]
144                assert fit.data[k] == 7.838
145            else:
146                assert fit.data[k] == expected[k]
147
148    def test_name(self, fit):
149        # without loading first, no data, so it blows up
150        with pytest.raises(KeyError):
151            fit.name
152        fit.load()
153        # the default fit file has both profile and sport
154        assert fit.name == 'Synapse cycling'
155        # remove profile
156        fit.data['profile'] = None
157        assert fit.name == 'cycling'
158        # change sport
159        fit.data['sport'] = 'running'
160        assert fit.name == 'running'
161
162    def test__calculate_avg_atemp(self, fit):
163        # before loading data, we don't have any info about the average temp
164        assert 'avg_atemp' not in fit.data.keys()
165        # loaded data, fit file didn't have that info pre-calculated
166        fit.load()
167        assert fit.data['avg_atemp'] == 0
168        # this loads the needed temperature data into fit.data + calls
169        # _calculate_avg_atemp
170        fit.gpx
171        # now we have the needed info
172        assert fit.data['avg_atemp'] == 2
173
174    def test_gpx_without_load(self, fit):
175        # without loading first, no data, so it blows up
176        with pytest.raises(KeyError):
177            fit.gpx
178
179    def test_gpx_real_fit_file(self, fit):
180        """
181        Test the gpx generation code with a real fit file from a garmin device
182        """
183        fit.load()
184        # open a pre-saved gpx file, the generated gpx file has to be like
185        # this one
186        gpx_path = self.get_fixture_path('20181230_101115.gpx')
187        with open(gpx_path, 'r') as gpx_file:
188            expected = gpx_file.read()
189        assert fit.gpx == expected
190
191    def test_gpx_extra_cases(self, fit, values):
192        """
193        Test the gpx generation code with some mocked data, so we can be sure
194        all the code is executed and covered by tests
195        """
196        # first mock the FitFile object in the fit object
197        session = Mock()
198        session.get_values.return_value = values
199        fit.obj = Mock()
200        # mock get_messages, no matter what we ask for, return a single
201        # session list, which fits for the purpose of this test
202        fit.obj.get_messages.return_value = [session]
203        fit.load()
204        # after loading data, mock again get_messages, this time return the
205        # list of messages we will loop over to generate the gpx object
206        first_values = {
207            'temperature': 18,
208            'heart_rate': 90,
209            'cadence': 40,
210            'position_long': -104549248,
211            'position_lat': 508158836,
212            'enhanced_altitude': 196.79999999999995,
213            'enhanced_speed': 5.432,
214            'timestamp': datetime.now()
215        }
216        first_record = Mock()
217        first_record.get_values.return_value = first_values
218        second_values = {
219            'temperature': 16,
220            'heart_rate': 110,
221            'cadence': 70,
222            'position_long': -104532648,
223            'position_lat': 508987836,
224            'enhanced_altitude': 210.79999999999995,
225            'enhanced_speed': 10.432,
226            'timestamp': datetime.now()
227        }
228        second_record = Mock()
229        second_record.get_values.return_value = second_values
230
231        third_values = {
232            'temperature': 7,
233            'heart_rate': 140,
234            'cadence': 90,
235            'position_long': -104532876,
236            'position_lat': 508987987,
237            'enhanced_altitude': 250.79999999999995,
238            'enhanced_speed': 9.432,
239            'timestamp': datetime.now()
240        }
241        third_record = Mock()
242        third_record.get_values.return_value = third_values
243
244        # set an hr value of 0, which will trigger the code that sets
245        # the minimum heart rate value from the last known value
246        fourth_values = {
247            'temperature': -2,
248            'heart_rate': 0,
249            'cadence': 0,
250            'position_long': -104532876,
251            'position_lat': 508987987,
252            'enhanced_altitude': 250.79999999999995,
253            'enhanced_speed': 9.432,
254            'timestamp': datetime.now()
255        }
256        fourth_record = Mock()
257        fourth_record.get_values.return_value = fourth_values
258
259        records = [
260            first_record,
261            second_record,
262            third_record,
263            fourth_record,
264        ]
265
266        fit.obj.get_messages.return_value = records
267        xml = fit.gpx
268
269        # now, ensure the proper data is in the xml file
270        #
271        # no 0 hr value is in there
272        assert '<gpxtpx:hr>0</gpxtpx:hr>' not in xml
273        # but the previous value is twice
274        assert xml.count('<gpxtpx:hr>140</gpxtpx:hr>') == 2
275
276        # the other values appear once
277        assert xml.count('<gpxtpx:hr>90</gpxtpx:hr>') == 1
278        assert xml.count('<gpxtpx:hr>110</gpxtpx:hr>') == 1
279        for v in [18, 16, 7, -2]:
280            assert '<gpxtpx:atemp>' + str(v) + '</gpxtpx:atemp>' in xml
281        for v in [40, 70, 90, 0]:
282            assert '<gpxtpx:cad>' + str(v) + '</gpxtpx:cad>' in xml
283
284        # the name provided by the fit object is there
285        assert '<name>' + fit.name + '</name>' in xml
286        # and we have 4 track points, no need to check the latitude/longitude
287        # conversion, as that is covered in the previous test with a real fit
288        # and gpx files test
289        assert xml.count('<trkpt lat') == 4
Note: See TracBrowser for help on using the repository browser.