source: OpenWorkouts-current/ow/tests/models/test_workout.py @ 5ec3a0b

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

Imported sources from the old python2-only repository:

  • Modified the code so it is python 3.6 compatible
  • Fixed deprecation warnings, pyramid 1.10.x supported now
  • Fixed deprecation warnings about some libraries, like pyramid-simpleform
  • Added pytest-pycodestyle and pytest-flakes for automatic checks on the source code files when running tests.
  • Added default pytest.ini setup to enforce some default parameters when running tests.
  • Cleaned up the code a bit, catched up with tests coverage.
  • Property mode set to 100644
File size: 14.0 KB
Line 
1import os
2from io import BytesIO
3from datetime import datetime, timedelta, timezone
4from decimal import Decimal
5from unittest.mock import patch
6
7import pytest
8from pyramid.security import Allow, Everyone
9
10from ow.models.workout import Workout
11from ow.models.user import User
12from ow.models.root import OpenWorkouts
13
14
15class TestWorkoutModels(object):
16
17    @pytest.fixture
18    def root(self):
19        root = OpenWorkouts()
20        root['john'] = User(firstname='John', lastname='Doe',
21                            email='john.doe@example.net')
22        root['john'].password = 's3cr3t'
23        root['john']['1'] = Workout(
24            start=datetime(2015, 6, 28, 12, 55, tzinfo=timezone.utc),
25            duration=timedelta(minutes=60),
26            distance=30
27        )
28        return root
29
30    def test__acl__(self, root):
31        # First check permissions for a workout without parent
32        permissions = [(Allow, Everyone, 'view'),
33                       (Allow, 'group:admins', 'edit')]
34        workout = Workout()
35        assert workout.__acl__() == permissions
36
37        # Now permissions on a workout that has been added to a user
38        uid = str(root['john'].uid)
39        permissions = [(Allow, uid, 'view'), (Allow, uid, 'edit')]
40        assert root['john']['1'].__acl__() == permissions
41
42    def test_runthrough(self, root):
43        """
44        Just a simple run through to see if those objects click
45        """
46        root['joe'] = User(firstname='Joe', lastname='Di Maggio')
47        assert(u'Joe Di Maggio' == root['joe'].fullname)
48        joe = root['joe']
49        start = datetime(2015, 6, 5, 19, 1, tzinfo=timezone.utc)
50        duration = timedelta(minutes=20)
51        distance = Decimal('0.25')  # 250m
52        w = Workout(start=start, duration=duration, sport='swimming',
53                    notes=u'Yay, I swam!', distance=distance)
54        joe['1'] = w
55        expected = datetime(2015, 6, 5, 19, 21, tzinfo=timezone.utc)
56        assert expected == joe['1'].end
57        assert 250 == joe['1'].distance * 1000
58
59    def test_add_user(self, root):
60        root.add_user('fred', firstname=u'Fred', lastname=u'Flintstone')
61        assert u'Fred Flintstone' == root['fred'].fullname
62
63    def test_workout_id(self, root):
64        assert root['john']['1'].workout_id == '1'
65
66    def test_end(self, root):
67        # workout without duration, it has no end
68        workout = Workout()
69        assert workout.end is None
70        # workout with duration, end is start_time + duration
71        expected = datetime(2015, 6, 28, 13, 55, tzinfo=timezone.utc)
72        assert root['john']['1'].end == expected
73
74    def test_start_date(self):
75        start_date = datetime.now()
76        workout = Workout(start=start_date)
77        assert workout.start_date == start_date.strftime('%d/%m/%Y')
78
79    def test_start_time(self):
80        start_date = datetime.now()
81        workout = Workout(start=start_date)
82        assert workout.start_time == start_date.strftime('%H:%M')
83
84    def test_split_duration(self):
85        # return the same hours, minutes, seconds we provided when creating
86        # the workout instance
87        duration = timedelta(hours=1, minutes=30, seconds=15)
88        workout = Workout(duration=duration)
89        assert workout.split_duration() == (1, 30, 15)
90        # If the duration is longer than a day, return the calculation of
91        # in hours, minutes, seconds too
92        duration = timedelta(days=1, hours=1, minutes=30, seconds=15)
93        workout = Workout(duration=duration)
94        assert workout.split_duration() == (25, 30, 15)
95
96    def test_duration_hours_minutes_seconds(self):
97        duration = timedelta(hours=1, minutes=30, seconds=15)
98        workout = Workout(duration=duration)
99        assert workout.duration_hours == '01'
100        assert workout.duration_minutes == '30'
101        assert workout.duration_seconds == '15'
102
103    def test_rounded_distance_no_value(self):
104        workout = Workout()
105        assert workout.rounded_distance == '-'
106
107    def test_rounded_distance(self):
108        workout = Workout()
109        workout.distance = 44.44444444
110        assert workout.rounded_distance == 44.4
111
112    def test_has_hr(self):
113        workout = Workout()
114        assert not workout.has_hr
115        workout.hr_min = 90
116        assert not workout.has_hr
117        workout.hr_max = 180
118        assert not workout.has_hr
119        workout.hr_avg = 120
120        assert workout.has_hr
121
122    def test_hr(self):
123        workout = Workout()
124        assert workout.hr is None
125        workout.hr_min = 90
126        assert workout.hr is None
127        workout.hr_max = 180
128        assert workout.hr is None
129        workout.hr_avg = 120
130        assert workout.hr['min'] == 90
131        assert workout.hr['max'] == 180
132        assert workout.hr['avg'] == 120
133
134    def test_has_cad(self):
135        workout = Workout()
136        assert not workout.has_cad
137        workout.cad_min = 0
138        assert not workout.has_cad
139        workout.cad_max = 110
140        assert not workout.has_cad
141        workout.cad_avg = 50
142        assert workout.has_cad
143
144    def test_cad(self):
145        workout = Workout()
146        assert workout.cad is None
147        workout.cad_min = 0
148        assert workout.cad is None
149        workout.cad_max = 110
150        assert workout.cad is None
151        workout.cad_avg = 50
152        assert workout.cad['min'] == 0
153        assert workout.cad['max'] == 110
154        assert workout.cad['avg'] == 50
155
156    def test_has_atemp(self):
157        workout = Workout()
158        assert not workout.has_atemp
159        workout.atemp_min = 0
160        assert not workout.has_atemp
161        workout.atemp_max = 12
162        assert not workout.has_atemp
163        workout.atemp_avg = 5
164        assert workout.has_atemp
165
166    def test_atemp(self):
167        workout = Workout()
168        assert workout.atemp is None
169        workout.atemp_min = 0
170        assert workout.atemp is None
171        workout.atemp_max = 12
172        assert workout.atemp is None
173        workout.atemp_avg = 5
174        assert workout.atemp['min'] == 0
175        assert workout.atemp['max'] == 12
176        assert workout.atemp['avg'] == 5
177
178    def test_load_from_file_invalid(self):
179        workout = Workout()
180        workout.tracking_filetype = 'alf'
181        with patch.object(workout, 'load_from_gpx') as lfg:
182            workout.load_from_file()
183            assert not lfg.called
184
185    def test_load_from_file_gpx(self):
186        workout = Workout()
187        workout.tracking_filetype = 'gpx'
188        with patch.object(workout, 'load_from_gpx') as lfg:
189            workout.load_from_file()
190            assert lfg.called
191
192    gpx_params = (
193        # GPX 1.0 file, no extensions
194        ('fixtures/20131013.gpx', {
195            'start': datetime(2013, 10, 13, 5, 28, 26, tzinfo=timezone.utc),
196            'duration': timedelta(seconds=27652),
197            'distance': Decimal(98.12598431852807),
198            'title': 'A ride I will never forget',
199            'blob': 'path',
200            'hr': {'min': None, 'max': None, 'avg': None},
201            'cad': {'min': None, 'max': None, 'avg': None},
202            'atemp': {'min': None, 'max': None, 'avg': None}}),
203        # GPX 1.0 file, no extensions, missing elevation
204        ('fixtures/20131013-without-elevation.gpx', {
205            'start': datetime(2013, 10, 13, 5, 28, 26, tzinfo=timezone.utc),
206            'duration': timedelta(seconds=27652),
207            'distance': Decimal(98.12598431852807),
208            'title': 'A ride I will never forget',
209            'blob': None,
210            'hr': {'min': None, 'max': None, 'avg': None},
211            'cad': {'min': None, 'max': None, 'avg': None},
212            'atemp': {'min': None, 'max': None, 'avg': None}}),
213        # GPX 1.1 file with extensions
214        ('fixtures/20160129-with-extensions.gpx', {
215            'start': datetime(2016, 1, 29, 8, 12, 9, tzinfo=timezone.utc),
216            'duration': timedelta(seconds=7028),
217            'distance': Decimal(48.37448557752049237024039030),
218            'title': 'Cota counterclockwise + end bonus',
219            'blob': 'path',
220            'hr': {'min': Decimal(100), 'max': Decimal(175),
221                   'avg': Decimal(148.365864144454008055618032813072)},
222            'cad': {'min': Decimal(0), 'max': Decimal(110),
223                    'avg': Decimal(67.41745485812553740326741187)},
224            'atemp': {'min': Decimal(-4), 'max': Decimal(14),
225                      'avg': Decimal(-0.3869303525365434221840068788)}}),
226        )
227
228    @pytest.mark.parametrize(('filename', 'expected'), gpx_params)
229    def test_load_from_gpx(self, filename, expected):
230        """
231        Load a gpx file located in tests/fixtures using the load_from_gpx()
232        method of the Workout model, then check that certain attrs on the
233        workout are updated correctly
234        """
235        # expected values
236        start = expected['start']
237        duration = expected['duration']
238        distance = expected['distance']
239        title = expected['title']
240        blob = expected['blob']
241        hr = expected['hr']
242        cad = expected['cad']
243        atemp = expected['atemp']
244
245        workout = Workout()
246
247        # Check the values are different by default
248        assert workout.start != start
249        assert workout.duration != duration
250        assert workout.distance != distance
251
252        gpx_file_path = os.path.join(
253            os.path.dirname(os.path.dirname(__file__)), filename)
254        with patch.object(workout, 'tracking_file') as tf:
255            with open(gpx_file_path, 'r') as gpx_file:
256                tf.open.return_value = BytesIO(gpx_file.read().encode('utf-8'))
257                # Set the path to the blob object containing the gpx file.
258                # more info in models.workout.Workout.parse_gpx()
259                tf._p_blob_uncommitted = gpx_file_path
260                if blob is None:
261                    # set the uncommited blob to None, mimicing what happens
262                    # with a workout saved into the db (transaction.commit())
263                    tf._p_blob_uncommitted = None
264                    tf._p_blob_committed = gpx_file_path
265                # Without this, has_gpx() will return False
266                workout.tracking_filetype = 'gpx'
267                res = workout.load_from_gpx()
268                assert res is True
269                assert workout.start == start
270                assert workout.duration == duration
271                assert isinstance(workout.distance, Decimal)
272                assert round(workout.distance) == round(distance)
273                # The title of the workout is taken from the gpx file
274                assert workout.title == title
275                for k in hr.keys():
276                    # We use 'fail' as the fallback in the getattr call because
277                    # None is one of the posible values, and we want to be sure
278                    # those attrs are there
279                    #
280                    # The keys are the same for the hr, cad and atemp dicts, so
281                    # we can do all tests in one loop
282                    #
283                    # If the expected value is not None, use round() to avoid
284                    # problems when comparing long Decimal objects
285                    value = getattr(workout, 'hr_'+k, 'fail')
286                    if hr[k] is None:
287                        assert hr[k] == value
288                    else:
289                        assert round(hr[k]) == round(value)
290
291                    value = getattr(workout, 'cad_'+k, 'fail')
292                    if cad[k] is None:
293                        assert cad[k] == value
294                    else:
295                        assert round(cad[k]) == round(value)
296
297                    value = getattr(workout, 'atemp_'+k, 'fail')
298                    if atemp[k] is None:
299                        assert atemp[k] == value
300                    else:
301                        assert round(atemp[k]) == round(value)
302
303    def test_load_from_gpx_no_tracks(self):
304        """
305        If we load an empty (but valid) gpx file (i.e., no tracks information)
306        the attrs on the workout are not updated, the call to load_from_gpx()
307        returns False
308        """
309        workout = Workout()
310
311        # We do not check the start time, it would need some mocking on the
312        # datetime module and there is no need really
313        assert workout.duration is None
314        assert workout.distance is None
315
316        gpx_file_path = os.path.join(
317            os.path.dirname(os.path.dirname(__file__)),
318            'fixtures/empty.gpx')
319        with patch.object(workout, 'tracking_file') as tf:
320            with open(gpx_file_path, 'r') as gpx_file:
321                tf.open.return_value = BytesIO(gpx_file.read().encode('utf-8'))
322                res = workout.load_from_gpx()
323                assert res is False
324                assert workout.duration is None
325                assert workout.distance is None
326                assert workout.title == ''
327                for k in ['max', 'min', 'avg']:
328                    for a in ['hr_', 'cad_', 'atemp_']:
329                        assert getattr(workout, a+k, 'fail') is None
330
331    def test_parse_gpx_no_gpx_file(self):
332        """
333        Test the behaviour of parse_gpx() when we call it on a workout without
334        a gpx tracking file. The behaviour of such method when the workout has
335        a gpx tracking file is covered by the test_load_from_gpx() test above
336        """
337        workout = Workout()
338        res = workout.parse_gpx()
339        assert res == {}
340
341    def test_has_tracking_file(self, root):
342        workout = root['john']['1']
343        # without tracking file
344        assert workout.has_tracking_file is False
345        # with tracking file
346        workout.tracking_file = 'faked tracking file'
347        assert workout.has_tracking_file is True
348
349    def test_has_gpx(self, root):
350        workout = root['john']['1']
351        # without tracking file
352        assert workout.has_gpx is False
353        workout.tracking_filetype = 'fit'
354        assert workout.has_gpx is False
355        # with non-gpx tracking file
356        workout.tracking_file = 'faked tracking file'
357        workout.tracking_filetype = 'fit'
358        assert workout.has_gpx is False
359        # with gpx tracking file
360        workout.tracking_file = 'faked tracking file'
361        workout.tracking_filetype = 'gpx'
362        assert workout.has_gpx is True
Note: See TracBrowser for help on using the repository browser.