From fba165037943fda90039ec9cadf0649cfae0e781 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Mon, 23 Sep 2019 21:13:47 +0200 Subject: Fix failing duration conversion https://github.com/python-discord/bot/issues/446 The current ExpirationDate converter does not convert duration strings to `datetime.datetime` objects correctly. To remedy the problem, I've written a new Duration converter that uses regex matching to extract the relevant duration units and `dateutil.relativedelta.relativedelta` to compute a `datetime.datetime` that's the given duration in the future. I've left the old `ExpirationDate` converter in place for now, since the new Duration converter may not be the most optimal method. However, given the importance of being able to convert durations for moderation tasks, I think it's better to implement Duration now and rethink the approach later. This commit closes #446 --- tests/test_converters.py | 82 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 4 deletions(-) (limited to 'tests/test_converters.py') diff --git a/tests/test_converters.py b/tests/test_converters.py index 3cf774c80..3cf00035f 100644 --- a/tests/test_converters.py +++ b/tests/test_converters.py @@ -1,11 +1,12 @@ import asyncio -from datetime import datetime -from unittest.mock import MagicMock +import datetime +from unittest.mock import MagicMock, patch import pytest from discord.ext.commands import BadArgument from bot.converters import ( + Duration, ExpirationDate, TagContentConverter, TagNameConverter, @@ -17,10 +18,10 @@ from bot.converters import ( ('value', 'expected'), ( # sorry aliens - ('2199-01-01T00:00:00', datetime(2199, 1, 1)), + ('2199-01-01T00:00:00', datetime.datetime(2199, 1, 1)), ) ) -def test_expiration_date_converter_for_valid(value: str, expected: datetime): +def test_expiration_date_converter_for_valid(value: str, expected: datetime.datetime): converter = ExpirationDate() assert asyncio.run(converter.convert(None, value)) == expected @@ -91,3 +92,76 @@ def test_valid_python_identifier_for_valid(value: str): def test_valid_python_identifier_for_invalid(value: str): with pytest.raises(BadArgument, match=f'`{value}` is not a valid Python identifier'): asyncio.run(ValidPythonIdentifier.convert(None, value)) + + +FIXED_UTC_NOW = datetime.datetime.fromisoformat('2019-01-01T00:00:00') + + +@pytest.mark.parametrize( + ('duration', 'expected'), + ( + # Simple duration strings + ('1Y', datetime.datetime.fromisoformat('2020-01-01T00:00:00')), + ('1y', datetime.datetime.fromisoformat('2020-01-01T00:00:00')), + ('1year', datetime.datetime.fromisoformat('2020-01-01T00:00:00')), + ('1years', datetime.datetime.fromisoformat('2020-01-01T00:00:00')), + ('1m', datetime.datetime.fromisoformat('2019-02-01T00:00:00')), + ('1month', datetime.datetime.fromisoformat('2019-02-01T00:00:00')), + ('1months', datetime.datetime.fromisoformat('2019-02-01T00:00:00')), + ('1w', datetime.datetime.fromisoformat('2019-01-08T00:00:00')), + ('1W', datetime.datetime.fromisoformat('2019-01-08T00:00:00')), + ('1week', datetime.datetime.fromisoformat('2019-01-08T00:00:00')), + ('1weeks', datetime.datetime.fromisoformat('2019-01-08T00:00:00')), + ('1d', datetime.datetime.fromisoformat('2019-01-02T00:00:00')), + ('1D', datetime.datetime.fromisoformat('2019-01-02T00:00:00')), + ('1day', datetime.datetime.fromisoformat('2019-01-02T00:00:00')), + ('1days', datetime.datetime.fromisoformat('2019-01-02T00:00:00')), + ('1h', datetime.datetime.fromisoformat('2019-01-01T01:00:00')), + ('1H', datetime.datetime.fromisoformat('2019-01-01T01:00:00')), + ('1hour', datetime.datetime.fromisoformat('2019-01-01T01:00:00')), + ('1hours', datetime.datetime.fromisoformat('2019-01-01T01:00:00')), + ('1M', datetime.datetime.fromisoformat('2019-01-01T00:01:00')), + ('1minute', datetime.datetime.fromisoformat('2019-01-01T00:01:00')), + ('1minutes', datetime.datetime.fromisoformat('2019-01-01T00:01:00')), + ('1s', datetime.datetime.fromisoformat('2019-01-01T00:00:01')), + ('1S', datetime.datetime.fromisoformat('2019-01-01T00:00:01')), + ('1second', datetime.datetime.fromisoformat('2019-01-01T00:00:01')), + ('1seconds', datetime.datetime.fromisoformat('2019-01-01T00:00:01')), + + # Complex duration strings + ('1y1m1w1d1H1M1S', datetime.datetime.fromisoformat('2020-02-09T01:01:01')), + ('5y100S', datetime.datetime.fromisoformat('2024-01-01T00:01:40')), + ('2w28H', datetime.datetime.fromisoformat('2019-01-16T04:00:00')), + ) +) +def test_duration_converter_for_valid(duration: str, expected: datetime): + converter = Duration() + + with patch('bot.converters.datetime') as mock_datetime: + mock_datetime.utcnow.return_value = FIXED_UTC_NOW + assert asyncio.run(converter.convert(None, duration)) == expected + + +@pytest.mark.parametrize( + ('duration'), + ( + # Units in wrong order + ('1d1w'), + ('1s1y'), + + # Unknown substrings + ('1MVes'), + ('1y3breads'), + + # Missing amount + ('ym'), + + # Garbage + ('Guido van Rossum'), + ('lemon lemon lemon lemon lemon lemon lemon'), + ) +) +def test_duration_converter_for_invalid(duration: str): + converter = Duration() + with pytest.raises(BadArgument, match=f'`{duration}` is not a valid duration string.'): + asyncio.run(converter.convert(None, duration)) -- cgit v1.2.3