diff options
Diffstat (limited to '')
| -rw-r--r-- | bot/utils/time.py | 19 | ||||
| -rw-r--r-- | tests/helpers.py | 4 | ||||
| -rw-r--r-- | tests/utils/test_time.py | 62 | 
3 files changed, 80 insertions, 5 deletions
| diff --git a/bot/utils/time.py b/bot/utils/time.py index da28f2c76..2aea2c099 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -1,5 +1,6 @@  import asyncio  import datetime +from typing import Optional  import dateutil.parser  from dateutil.relativedelta import relativedelta @@ -34,6 +35,9 @@ def humanize_delta(delta: relativedelta, precision: str = "seconds", max_units:      precision specifies the smallest unit of time to include (e.g. "seconds", "minutes").      max_units specifies the maximum number of units of time to include (e.g. 1 may include days but not hours).      """ +    if max_units <= 0: +        raise ValueError("max_units must be positive") +      units = (          ("years", delta.years),          ("months", delta.months), @@ -83,15 +87,20 @@ def time_since(past_datetime: datetime.datetime, precision: str = "seconds", max      return f"{humanized} ago" -def parse_rfc1123(time_str: str) -> datetime.datetime: +def parse_rfc1123(stamp: str) -> datetime.datetime:      """Parse RFC1123 time string into datetime.""" -    return datetime.datetime.strptime(time_str, RFC1123_FORMAT).replace(tzinfo=datetime.timezone.utc) +    return datetime.datetime.strptime(stamp, RFC1123_FORMAT).replace(tzinfo=datetime.timezone.utc)  # Hey, this could actually be used in the off_topic_names and reddit cogs :) -async def wait_until(time: datetime.datetime) -> None: -    """Wait until a given time.""" -    delay = time - datetime.datetime.utcnow() +async def wait_until(time: datetime.datetime, start: Optional[datetime.datetime] = None) -> None: +    """ +    Wait until a given time. + +    :param time: A datetime.datetime object to wait until. +    :param start: The start from which to calculate the waiting duration. Defaults to UTC time. +    """ +    delay = time - (start or datetime.datetime.utcnow())      delay_seconds = delay.total_seconds()      # Incorporate a small delay so we don't rapid-fire the event due to time precision errors diff --git a/tests/helpers.py b/tests/helpers.py index 2908294f7..25059fa3a 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -7,6 +7,10 @@ __all__ = ('AsyncMock', 'async_test')  # TODO: Remove me on 3.8 +# Allows you to mock a coroutine. Since the default `__call__` of `MagicMock` +# is not a coroutine, trying to mock a coroutine with it will result in errors +# as the default `__call__` is not awaitable. Use this class for monkeypatching +# coroutines instead.  class AsyncMock(MagicMock):      async def __call__(self, *args, **kwargs):          return super(AsyncMock, self).__call__(*args, **kwargs) diff --git a/tests/utils/test_time.py b/tests/utils/test_time.py new file mode 100644 index 000000000..4baa6395c --- /dev/null +++ b/tests/utils/test_time.py @@ -0,0 +1,62 @@ +import asyncio +from datetime import datetime, timezone +from unittest.mock import patch + +import pytest +from dateutil.relativedelta import relativedelta + +from bot.utils import time +from tests.helpers import AsyncMock + + +    ('delta', 'precision', 'max_units', 'expected'), +    ( +        (relativedelta(days=2), 'seconds', 1, '2 days'), +        (relativedelta(days=2, hours=2), 'seconds', 2, '2 days and 2 hours'), +        (relativedelta(days=2, hours=2), 'seconds', 1, '2 days'), +        (relativedelta(days=2, hours=2), 'days', 2, '2 days'), + +        # Does not abort for unknown units, as the unit name is checked +        # against the attribute of the relativedelta instance. +        (relativedelta(days=2, hours=2), 'elephants', 2, '2 days and 2 hours'), + +        # Very high maximum units, but it only ever iterates over +        # each value the relativedelta might have. +        (relativedelta(days=2, hours=2), 'hours', 20, '2 days and 2 hours'), +    ) +) +def test_humanize_delta( +        delta: relativedelta, +        precision: str, +        max_units: int, +        expected: str +): +    assert time.humanize_delta(delta, precision, max_units) == expected + + [email protected]('max_units', (-1, 0)) +def test_humanize_delta_raises_for_invalid_max_units(max_units: int): +    with pytest.raises(ValueError, match='max_units must be positive'): +        time.humanize_delta(relativedelta(days=2, hours=2), 'hours', max_units) + + +    ('stamp', 'expected'), +    ( +        ('Sun, 15 Sep 2019 12:00:00 GMT', datetime(2019, 9, 15, 12, 0, 0, tzinfo=timezone.utc)), +    ) +) +def test_parse_rfc1123(stamp: str, expected: str): +    assert time.parse_rfc1123(stamp) == expected + + +@patch('asyncio.sleep', new_callable=AsyncMock) +def test_wait_until(sleep_patch): +    start = datetime(2019, 1, 1, 0, 0) +    then = datetime(2019, 1, 1, 0, 10) + +    # No return value +    assert asyncio.run(time.wait_until(then, start)) is None + +    sleep_patch.assert_called_once_with(10 * 60) | 
