source: OpenWorkouts-current/ow/tests/test_utilities.py @ 2f8a48f

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

(#7) Added several methods to the User model to gather some stats (yearly,

monthly, weekly).

Added two new utilities:

  • timedelta_to_hms (so we can print timedelta objects properly in template code)
  • get_week_days (returns a list of datetime objects for the days in the same week as a given day)

Added a template_helpers module, containing code that affects template
rendering.

Added timedelta_to_hms as a global to the default template rendering context

Refactored some code in the Workout model so it uses timedelta_to_hms instead
of running the same code twice.

  • Property mode set to 100644
File size: 9.9 KB
Line 
1import os
2from datetime import timedelta, datetime
3from unittest.mock import patch
4from pyexpat import ExpatError
5from xml.dom.minidom import Element
6
7import pytest
8
9from ow.models.root import OpenWorkouts
10from ow.models.user import User
11from ow.models.workout import Workout
12
13from ow.utilities import (
14    slugify,
15    GPXMinidomParser,
16    semicircles_to_degrees,
17    degrees_to_semicircles,
18    miles_to_kms,
19    kms_to_miles,
20    meters_to_kms,
21    kms_to_meters,
22    mps_to_kmph,
23    kmph_to_mps,
24    save_map_screenshot,
25    timedelta_to_hms,
26    get_week_days
27)
28
29from ow.tests.helpers import join
30
31
32class TestUtilities(object):
33
34    @pytest.fixture
35    def john(self):
36        john = User(firstname='John', lastname='Doe',
37                    email='john.doe@example.net')
38        john.password = 's3cr3t'
39        return john
40
41    @pytest.fixture
42    def root(self, john):
43        root = OpenWorkouts()
44        root.add_user(john)
45        john['1'] = Workout(
46            duration=timedelta(minutes=60),
47            distance=30
48        )
49        return root
50
51    def test_slugify(self):
52        res = slugify(u'long story SHORT      ')
53        assert res == u'long-story-short'
54        res = slugify(u'bla \u03ba\u03b1\u03bb\u03ac \u03c0\u03b1\u03c2')
55        assert res == u'bla-kala-pas'
56
57    def test_slugify_special_chars(self):
58        res = slugify(u'(r)-[i]\u00AE')
59        assert res == u'r-i-r'
60
61    def test_semicircles_to_degrees(self):
62        assert semicircles_to_degrees(10) == 10 * (180 / pow(2, 31))
63
64    def test_degrees_to_semicircles(self):
65        assert degrees_to_semicircles(10) == 10 * (pow(2, 31) / 180)
66
67    def test_miles_to_kms(self):
68        assert miles_to_kms(100) == 100 / 0.62137119
69
70    def test_kms_to_miles(self):
71        assert kms_to_miles(100) == 100 * 0.62137119
72
73    def test_meters_to_kms(self):
74        assert meters_to_kms(1000) == 1
75
76    def test_kms_to_meters(self):
77        assert kms_to_meters(1) == 1000
78
79    def test_mps_to_kmph(self):
80        assert mps_to_kmph(5) == 5 * 3.6
81
82    def test_kmph_to_mps(self):
83        assert kmph_to_mps(30) == 30 * 0.277778
84
85    @patch('ow.utilities.os')
86    @patch('ow.utilities.subprocess')
87    def test_save_map_screenshot_no_gpx(self, subprocess, os, root, john):
88        saved = save_map_screenshot(john['1'])
89        assert not saved
90        assert not os.path.abspath.called
91        assert not os.path.dirname.called
92        assert not os.path.join.called
93        assert not os.path.exists.called
94        assert not os.makedirs.called
95        assert not subprocess.run.called
96        # even having a fit tracking file, nothing is done
97        john['1'].tracking_file = 'faked fit file'
98        john['1'].tracking_filetype = 'fit'
99        saved = save_map_screenshot(john['1'])
100        assert not saved
101        assert not os.path.abspath.called
102        assert not os.path.dirname.called
103        assert not os.path.join.called
104        assert not os.path.exists.called
105        assert not os.makedirs.called
106        assert not subprocess.run.called
107
108    @patch('ow.utilities.os')
109    @patch('ow.utilities.subprocess')
110    def test_save_map_screenshot_with_gpx(self, subprocess, os, root, john):
111        os.path.abspath.return_value = 'current_dir'
112        os.path.join.side_effect = join
113        # This mimics what happens when the directory for this user map
114        # screenshots does not exist, which means we don'have to create one
115        # (calling os.makedirs)
116        os.path.exists.return_value = False
117
118        john['1'].tracking_file = 'faked gpx content'
119        john['1'].tracking_filetype = 'gpx'
120        saved = save_map_screenshot(john['1'])
121        assert saved
122        os.path.abspath.assert_called_once
123        assert os.path.dirname.called
124        assert os.path.join.call_count == 3
125        assert os.path.exists.called
126        assert os.makedirs.called
127        subprocess.run.assert_called_once
128
129    @patch('ow.utilities.os')
130    @patch('ow.utilities.subprocess')
131    def test_save_map_screenshot_with_gpx_makedirs(
132            self, subprocess, os, root, john):
133        os.path.abspath.return_value = 'current_dir'
134        os.path.join.side_effect = join
135        # If os.path.exists returns True, makedirs is not called
136        os.path.exists.return_value = True
137
138        john['1'].tracking_file = 'faked gpx content'
139        john['1'].tracking_filetype = 'gpx'
140        saved = save_map_screenshot(john['1'])
141        assert saved
142        os.path.abspath.assert_called_once
143        assert os.path.dirname.called
144        assert os.path.join.call_count == 3
145        assert os.path.exists.called
146        assert not os.makedirs.called
147        subprocess.run.assert_called_once
148
149    def test_timedelta_to_hms(self):
150        value = timedelta(seconds=0)
151        assert timedelta_to_hms(value) == (0, 0, 0)
152        value = timedelta(seconds=3600)
153        assert timedelta_to_hms(value) == (1, 0, 0)
154        value = timedelta(seconds=3900)
155        assert timedelta_to_hms(value) == (1, 5, 0)
156        value = timedelta(seconds=3940)
157        assert timedelta_to_hms(value) == (1, 5, 40)
158        value = timedelta(seconds=4)
159        assert timedelta_to_hms(value) == (0, 0, 4)
160        value = timedelta(seconds=150)
161        assert timedelta_to_hms(value) == (0, 2, 30)
162        # try now something that is not a timedelta
163        with pytest.raises(AttributeError):
164            timedelta_to_hms('not a timedelta')
165
166    def test_week_days(self):
167        # get days from a monday, week starting on monday
168        days = get_week_days(datetime(2019, 1, 21))
169        assert len(days) == 7
170        matches = [
171            [days[0], datetime(2019, 1, 21)],
172            [days[1], datetime(2019, 1, 22)],
173            [days[2], datetime(2019, 1, 23)],
174            [days[3], datetime(2019, 1, 24)],
175            [days[4], datetime(2019, 1, 25)],
176            [days[5], datetime(2019, 1, 26)],
177            [days[6], datetime(2019, 1, 27)]
178        ]
179        for m in matches:
180            assert m[0] == m[1]
181        # get days from a wednesday, week starting on monday
182        days = get_week_days(datetime(2019, 1, 23))
183        assert len(days) == 7
184        matches = [
185            [days[0], datetime(2019, 1, 21)],
186            [days[1], datetime(2019, 1, 22)],
187            [days[2], datetime(2019, 1, 23)],
188            [days[3], datetime(2019, 1, 24)],
189            [days[4], datetime(2019, 1, 25)],
190            [days[5], datetime(2019, 1, 26)],
191            [days[6], datetime(2019, 1, 27)]
192        ]
193        for m in matches:
194            assert m[0] == m[1]
195        # get days from a monday, but week starting on sunday now
196        days = get_week_days(datetime(2019, 1, 21), start_day=0)
197        assert len(days) == 7
198        matches = [
199            [days[0], datetime(2019, 1, 20)],
200            [days[1], datetime(2019, 1, 21)],
201            [days[2], datetime(2019, 1, 22)],
202            [days[3], datetime(2019, 1, 23)],
203            [days[4], datetime(2019, 1, 24)],
204            [days[5], datetime(2019, 1, 25)],
205            [days[6], datetime(2019, 1, 26)]
206        ]
207        for m in matches:
208            assert m[0] == m[1]
209
210
211class TestGPXParseMinidom(object):
212
213    def gpx_file(self, filename):
214        """
215        Return the full path to the given filename from the available fixtures
216        """
217        here = os.path.abspath(os.path.dirname(__file__))
218        path = os.path.join(here, 'fixtures', filename)
219        return path
220
221    def test_load_gpx_invalid(self):
222        gpx_file = self.gpx_file('invalid.gpx')
223        parser = GPXMinidomParser(gpx_file)
224        with pytest.raises(ExpatError):
225            parser.load_gpx()
226        assert parser.gpx is None
227
228    gpx_files = [
229        ('empty.gpx', Element),
230        ('20131013.gpx', Element),  # GPX 1.0 file
231        ('20160129-with-extensions.gpx', Element),  # GPX 1.1 file with ext.
232    ]
233
234    @pytest.mark.parametrize(('filename', 'expected'), gpx_files)
235    def test_load_gpx(self, filename, expected):
236        """
237        Loading valid gpx files ends in the gpx attribute of the parser
238        being a xml.dom.minidom.Element object, not matter if the gpx file
239        is empty or a 1.0/1.1 gpx file.
240        """
241        gpx_file = self.gpx_file(filename)
242        parser = GPXMinidomParser(gpx_file)
243        parser.load_gpx()
244        assert isinstance(parser.gpx, expected)
245
246    def test_parse_tracks_empty_gpx(self):
247        gpx_file = self.gpx_file('empty.gpx')
248        parser = GPXMinidomParser(gpx_file)
249        parser.load_gpx()
250        parser.parse_tracks()
251        assert parser.tracks == {}
252
253    def test_parse_tracks_1_0_gpx(self):
254        """
255        Parsing a GPX 1.0 file with no extensions. The points in the track
256        contain keys for the well-known extensions (hr, cad, atemp), but their
257        values are None
258        """
259        gpx_file = self.gpx_file('20131013.gpx')
260        parser = GPXMinidomParser(gpx_file)
261        parser.load_gpx()
262        parser.parse_tracks()
263        keys = list(parser.tracks.keys())
264        assert keys == [u'A ride I will never forget']
265        point = parser.tracks[keys[0]][0]
266        data = ['lat', 'lon', 'ele', 'time']
267        for d in data:
268            assert point[d] is not None
269        extensions = ['hr', 'cad', 'atemp']
270        for e in extensions:
271            assert point[e] is None
272
273    def test_parse_tracks_1_1_gpx(self):
274        """
275        Parsing a GPX 1.1 file with extensions. The points in the track contain
276        keys for the well-known extensions (hr, cad, atemp), with the values
277        taken from the gpx file (although we test only that they are not None)
278        """
279        gpx_file = self.gpx_file('20160129-with-extensions.gpx')
280        parser = GPXMinidomParser(gpx_file)
281        parser.load_gpx()
282        parser.parse_tracks()
283        keys = list(parser.tracks.keys())
284        assert keys == [
285            u'Cota counterclockwise + end bonus']
286        point = parser.tracks[keys[0]][0]
287        data = ['lat', 'lon', 'ele', 'time']
288        for d in data:
289            assert point[d] is not None
290        extensions = ['hr', 'cad', 'atemp']
291        for e in extensions:
292            assert point[e] is not None
Note: See TracBrowser for help on using the repository browser.