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

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

(#58) Set a title automatically when adding manually a workout without
providing one.

The title is generated based on the only required data we have (starting
date and time) + sport (if provided).

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