File: test_time.py

package info (click to toggle)
celery 5.5.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 8,008 kB
  • sloc: python: 64,346; sh: 795; makefile: 378
file content (401 lines) | stat: -rw-r--r-- 13,615 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
import sys
from datetime import datetime, timedelta
from datetime import timezone as _timezone
from datetime import tzinfo
from unittest.mock import Mock, patch

import pytest

if sys.version_info >= (3, 9):
    from zoneinfo import ZoneInfo
else:
    from backports.zoneinfo import ZoneInfo

from celery.utils.iso8601 import parse_iso8601
from celery.utils.time import (LocalTimezone, delta_resolution, ffwd, get_exponential_backoff_interval,
                               humanize_seconds, localize, make_aware, maybe_iso8601, maybe_make_aware,
                               maybe_timedelta, rate, remaining, timezone, utcoffset)


class test_LocalTimezone:

    def test_daylight(self, patching):
        time = patching('celery.utils.time._time')
        time.timezone = 3600
        time.daylight = False
        x = LocalTimezone()
        assert x.STDOFFSET == timedelta(seconds=-3600)
        assert x.DSTOFFSET == x.STDOFFSET
        time.daylight = True
        time.altzone = 3600
        y = LocalTimezone()
        assert y.STDOFFSET == timedelta(seconds=-3600)
        assert y.DSTOFFSET == timedelta(seconds=-3600)

        assert repr(y)

        y._isdst = Mock()
        y._isdst.return_value = True
        assert y.utcoffset(datetime.now())
        assert not y.dst(datetime.now())
        y._isdst.return_value = False
        assert y.utcoffset(datetime.now())
        assert not y.dst(datetime.now())

        assert y.tzname(datetime.now())


class test_iso8601:

    def test_parse_with_timezone(self):
        d = datetime.now(_timezone.utc).replace(tzinfo=ZoneInfo("UTC"))
        assert parse_iso8601(d.isoformat()) == d
        # 2013-06-07T20:12:51.775877+00:00
        iso = d.isoformat()
        iso1 = iso.replace('+00:00', '-01:00')
        d1 = parse_iso8601(iso1)
        d1_offset_in_minutes = d1.utcoffset().total_seconds() / 60
        assert d1_offset_in_minutes == -60
        iso2 = iso.replace('+00:00', '+01:00')
        d2 = parse_iso8601(iso2)
        d2_offset_in_minutes = d2.utcoffset().total_seconds() / 60
        assert d2_offset_in_minutes == +60
        iso3 = iso.replace('+00:00', 'Z')
        d3 = parse_iso8601(iso3)
        assert d3.tzinfo == _timezone.utc


@pytest.mark.parametrize('delta,expected', [
    (timedelta(days=2), datetime(2010, 3, 30, 0, 0)),
    (timedelta(hours=2), datetime(2010, 3, 30, 11, 0)),
    (timedelta(minutes=2), datetime(2010, 3, 30, 11, 50)),
    (timedelta(seconds=2), None),
])
def test_delta_resolution(delta, expected):
    dt = datetime(2010, 3, 30, 11, 50, 58, 41065)
    assert delta_resolution(dt, delta) == expected or dt


@pytest.mark.parametrize('seconds,expected', [
    (4 * 60 * 60 * 24, '4.00 days'),
    (1 * 60 * 60 * 24, '1.00 day'),
    (4 * 60 * 60, '4.00 hours'),
    (1 * 60 * 60, '1.00 hour'),
    (4 * 60, '4.00 minutes'),
    (1 * 60, '1.00 minute'),
    (4, '4.00 seconds'),
    (1, '1.00 second'),
    (4.3567631221, '4.36 seconds'),
    (0, 'now'),
])
def test_humanize_seconds(seconds, expected):
    assert humanize_seconds(seconds) == expected


def test_humanize_seconds__prefix():
    assert humanize_seconds(4, prefix='about ') == 'about 4.00 seconds'


def test_maybe_iso8601_datetime():
    now = datetime.now()
    assert maybe_iso8601(now) is now


@pytest.mark.parametrize('date_str,expected', [
    ('2011-11-04T00:05:23', datetime(2011, 11, 4, 0, 5, 23)),
    ('2011-11-04T00:05:23Z', datetime(2011, 11, 4, 0, 5, 23, tzinfo=_timezone.utc)),
    ('2011-11-04 00:05:23.283+00:00',
     datetime(2011, 11, 4, 0, 5, 23, 283000, tzinfo=_timezone.utc)),
    ('2011-11-04T00:05:23+04:00',
     datetime(2011, 11, 4, 0, 5, 23,  tzinfo=_timezone(timedelta(seconds=14400)))),
])
def test_iso8601_string_datetime(date_str, expected):
    assert maybe_iso8601(date_str) == expected


@pytest.mark.parametrize('arg,expected', [
    (30, timedelta(seconds=30)),
    (30.6, timedelta(seconds=30.6)),
    (timedelta(days=2), timedelta(days=2)),
])
def test_maybe_timedelta(arg, expected):
    assert maybe_timedelta(arg) == expected


def test_remaining():
    # Relative
    remaining(datetime.now(_timezone.utc), timedelta(hours=1), relative=True)

    """
    The upcoming cases check whether the next run is calculated correctly
    """
    eastern_tz = ZoneInfo("US/Eastern")
    tokyo_tz = ZoneInfo("Asia/Tokyo")

    # Case 1: `start` in UTC and `now` in other timezone
    start = datetime.now(ZoneInfo("UTC"))
    now = datetime.now(eastern_tz)
    delta = timedelta(hours=1)
    assert str(start.tzinfo) == str(ZoneInfo("UTC"))
    assert str(now.tzinfo) == str(eastern_tz)
    rem_secs = remaining(start, delta, now).total_seconds()
    # assert remaining time is approximately equal to delta
    assert rem_secs == pytest.approx(delta.total_seconds(), abs=1)

    # Case 2: `start` and `now` in different timezones (other than UTC)
    start = datetime.now(eastern_tz)
    now = datetime.now(tokyo_tz)
    delta = timedelta(hours=1)
    assert str(start.tzinfo) == str(eastern_tz)
    assert str(now.tzinfo) == str(tokyo_tz)
    rem_secs = remaining(start, delta, now).total_seconds()
    assert rem_secs == pytest.approx(delta.total_seconds(), abs=1)

    """
    Case 3: DST check
    Suppose start (which is last_run_time) is in EST while next_run is in EDT,
    then check whether the `next_run` is actually the time specified in the
    start (i.e. there is not an hour diff due to DST).
    In 2019, DST starts on March 10
    """
    start = datetime(
        month=3, day=9, year=2019, hour=10,
        minute=0, tzinfo=eastern_tz)  # EST

    now = datetime(
        day=11, month=3, year=2019, hour=1,
        minute=0, tzinfo=eastern_tz)  # EDT
    delta = ffwd(hour=10, year=2019, microsecond=0, minute=0,
                 second=0, day=11, weeks=0, month=3)
    # `next_actual_time` is the next time to run (derived from delta)
    next_actual_time = datetime(
        day=11, month=3, year=2019, hour=10, minute=0, tzinfo=eastern_tz)  # EDT
    assert start.tzname() == "EST"
    assert now.tzname() == "EDT"
    assert next_actual_time.tzname() == "EDT"
    rem_time = remaining(start, delta, now)
    next_run = now + rem_time
    assert next_run == next_actual_time

    """
    Case 4: DST check between now and next_run
    Suppose start (which is last_run_time) and now are in EST while next_run
    is in EDT, then check that the remaining time returned is the exact real
    time difference (not wall time).
    For example, between
    2019-03-10 01:30:00-05:00 and
    2019-03-10 03:30:00-04:00
    There is only 1 hour difference in real time, but 2 on wall time.
    Python by default uses wall time in arithmetic between datetimes with
    equal non-UTC timezones.
    In 2019, DST starts on March 10
    """
    start = datetime(
        day=10, month=3, year=2019, hour=1,
        minute=30, tzinfo=eastern_tz)  # EST

    now = datetime(
        day=10, month=3, year=2019, hour=1,
        minute=30, tzinfo=eastern_tz)  # EST
    delta = ffwd(hour=3, year=2019, microsecond=0, minute=30,
                 second=0, day=10, weeks=0, month=3)
    # `next_actual_time` is the next time to run (derived from delta)
    next_actual_time = datetime(
        day=10, month=3, year=2019, hour=3, minute=30, tzinfo=eastern_tz)  # EDT
    assert start.tzname() == "EST"
    assert now.tzname() == "EST"
    assert next_actual_time.tzname() == "EDT"
    rem_time = remaining(start, delta, now)
    assert rem_time.total_seconds() == 3600
    next_run_utc = now.astimezone(ZoneInfo("UTC")) + rem_time
    next_run_edt = next_run_utc.astimezone(eastern_tz)
    assert next_run_utc == next_actual_time
    assert next_run_edt == next_actual_time


class test_timezone:

    def test_get_timezone_with_zoneinfo(self):
        assert timezone.get_timezone('UTC')

    def test_tz_or_local(self):
        assert timezone.tz_or_local() == timezone.local
        assert timezone.tz_or_local(timezone.utc)

    def test_to_local(self):
        assert timezone.to_local(make_aware(datetime.now(_timezone.utc), timezone.utc))
        assert timezone.to_local(datetime.now(_timezone.utc))

    def test_to_local_fallback(self):
        assert timezone.to_local_fallback(
            make_aware(datetime.now(_timezone.utc), timezone.utc))
        assert timezone.to_local_fallback(datetime.now(_timezone.utc))


class test_make_aware:

    def test_standard_tz(self):
        tz = tzinfo()
        wtz = make_aware(datetime.now(_timezone.utc), tz)
        assert wtz.tzinfo == tz

    def test_tz_when_zoneinfo(self):
        tz = ZoneInfo('US/Eastern')
        wtz = make_aware(datetime.now(_timezone.utc), tz)
        assert wtz.tzinfo == tz

    def test_maybe_make_aware(self):
        aware = datetime.now(_timezone.utc).replace(tzinfo=timezone.utc)
        assert maybe_make_aware(aware)
        naive = datetime.now()
        assert maybe_make_aware(naive)
        assert maybe_make_aware(naive).tzinfo is ZoneInfo("UTC")

        tz = ZoneInfo('US/Eastern')
        eastern = datetime.now(_timezone.utc).replace(tzinfo=tz)
        assert maybe_make_aware(eastern).tzinfo is tz
        utcnow = datetime.now()
        assert maybe_make_aware(utcnow, 'UTC').tzinfo is ZoneInfo("UTC")


class test_localize:

    def test_standard_tz(self):
        class tzz(tzinfo):

            def utcoffset(self, dt):
                return None  # Mock no utcoffset specified

        tz = tzz()
        assert localize(make_aware(datetime.now(_timezone.utc), tz), tz)

    @patch('dateutil.tz.datetime_ambiguous')
    def test_when_zoneinfo(self, datetime_ambiguous_mock):
        datetime_ambiguous_mock.return_value = False
        tz = ZoneInfo("US/Eastern")
        assert localize(make_aware(datetime.now(_timezone.utc), tz), tz)

        datetime_ambiguous_mock.return_value = True
        tz2 = ZoneInfo("US/Eastern")
        assert localize(make_aware(datetime.now(_timezone.utc), tz2), tz2)

    @patch('dateutil.tz.datetime_ambiguous')
    def test_when_is_ambiguous(self, datetime_ambiguous_mock):
        class tzz(tzinfo):

            def utcoffset(self, dt):
                return None  # Mock no utcoffset specified

            def is_ambiguous(self, dt):
                return True

        datetime_ambiguous_mock.return_value = False
        tz = tzz()
        assert localize(make_aware(datetime.now(_timezone.utc), tz), tz)

        datetime_ambiguous_mock.return_value = True
        tz2 = tzz()
        assert localize(make_aware(datetime.now(_timezone.utc), tz2), tz2)

    def test_localize_changes_utc_dt(self):
        now_utc_time = datetime.now(tz=ZoneInfo("UTC"))
        local_tz = ZoneInfo('US/Eastern')
        localized_time = localize(now_utc_time, local_tz)
        assert localized_time == now_utc_time

    def test_localize_aware_dt_idempotent(self):
        t = (2017, 4, 23, 21, 36, 59, 0)
        local_zone = ZoneInfo('America/New_York')
        local_time = datetime(*t)
        local_time_aware = datetime(*t, tzinfo=local_zone)
        alternate_zone = ZoneInfo('America/Detroit')
        localized_time = localize(local_time_aware, alternate_zone)
        assert localized_time == local_time_aware
        assert local_zone.utcoffset(
            local_time) == alternate_zone.utcoffset(local_time)
        localized_utc_offset = localized_time.tzinfo.utcoffset(local_time)
        assert localized_utc_offset == alternate_zone.utcoffset(local_time)
        assert localized_utc_offset == local_zone.utcoffset(local_time)


@pytest.mark.parametrize('s,expected', [
    (999, 999),
    (7.5, 7.5),
    ('2.5/s', 2.5),
    ('1456/s', 1456),
    ('100/m', 100 / 60.0),
    ('10/h', 10 / 60.0 / 60.0),
    (0, 0),
    (None, 0),
    ('0/m', 0),
    ('0/h', 0),
    ('0/s', 0),
    ('0.0/s', 0),
])
def test_rate_limit_string(s, expected):
    assert rate(s) == expected


class test_ffwd:

    def test_repr(self):
        x = ffwd(year=2012)
        assert repr(x)

    def test_radd_with_unknown_gives_NotImplemented(self):
        x = ffwd(year=2012)
        assert x.__radd__(object()) == NotImplemented


class test_utcoffset:

    def test_utcoffset(self, patching):
        _time = patching('celery.utils.time._time')
        _time.daylight = True
        assert utcoffset(time=_time) is not None
        _time.daylight = False
        assert utcoffset(time=_time) is not None


class test_get_exponential_backoff_interval:

    @patch('random.randrange', lambda n: n - 2)
    def test_with_jitter(self):
        assert get_exponential_backoff_interval(
            factor=4,
            retries=3,
            maximum=100,
            full_jitter=True
        ) == 4 * (2 ** 3) - 1

    def test_without_jitter(self):
        assert get_exponential_backoff_interval(
            factor=4,
            retries=3,
            maximum=100,
            full_jitter=False
        ) == 4 * (2 ** 3)

    def test_bound_by_maximum(self):
        maximum_boundary = 100
        assert get_exponential_backoff_interval(
            factor=40,
            retries=3,
            maximum=maximum_boundary
        ) == maximum_boundary

    @patch('random.randrange', lambda n: n - 1)
    def test_negative_values(self):
        assert get_exponential_backoff_interval(
            factor=-40,
            retries=3,
            maximum=100
        ) == 0

    @patch('random.randrange')
    def test_valid_random_range(self, rr):
        rr.return_value = 0
        maximum = 100
        get_exponential_backoff_interval(
            factor=40, retries=10, maximum=maximum, full_jitter=True)
        rr.assert_called_once_with(maximum + 1)