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

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

(#52) - screenshots of the workouts maps were corrupted randomly.

Replaced our screenshot_map shell script that was calling chrome headless
directly with some python code running splinter + selenium webdriver.

Using chromedriver in such environment we can first visit the map site
for the given workout, then wait a bit for it to completely load, then
take the screenshot.

I've also removed all traces of screenshot_map from the code.

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