committed by
GitHub
5 changed files with 320 additions and 0 deletions
@ -0,0 +1,35 @@ |
|||||
|
name: test |
||||
|
|
||||
|
on: |
||||
|
push: |
||||
|
pull_request: |
||||
|
types: [ opened, edited ] |
||||
|
|
||||
|
jobs: |
||||
|
pytest: |
||||
|
runs-on: ubuntu-latest |
||||
|
strategy: |
||||
|
fail-fast: false |
||||
|
matrix: |
||||
|
python-version: [ '3.8', '3.x' ] |
||||
|
|
||||
|
name: pytest ${{ matrix.python-version }} |
||||
|
steps: |
||||
|
- uses: actions/checkout@v2 |
||||
|
with: |
||||
|
fetch-depth: 0 |
||||
|
|
||||
|
- name: Set up CPython ${{ matrix.python-version }} |
||||
|
uses: actions/setup-python@v2 |
||||
|
with: |
||||
|
python-version: ${{ matrix.python-version }} |
||||
|
|
||||
|
- name: Install dependencies |
||||
|
run: | |
||||
|
python -m pip install --upgrade pip setuptools wheel "coverage[toml]" pytest pytest-asyncio pytest-cov pytest-mock |
||||
|
pip install -U -r requirements.txt |
||||
|
|
||||
|
- name: Run tests |
||||
|
shell: bash |
||||
|
run: | |
||||
|
PYTHONPATH="$(pwd)" pytest -vs --cov=discord --cov-report term-missing:skip-covered |
@ -0,0 +1,261 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
|
||||
|
""" |
||||
|
|
||||
|
Tests for discord.utils |
||||
|
|
||||
|
""" |
||||
|
|
||||
|
import datetime |
||||
|
import random |
||||
|
import collections |
||||
|
import secrets |
||||
|
import sys |
||||
|
import time |
||||
|
import typing |
||||
|
|
||||
|
import pytest |
||||
|
|
||||
|
from discord import utils |
||||
|
|
||||
|
|
||||
|
# Async generator for async support |
||||
|
async def async_iterate(array): |
||||
|
for item in array: |
||||
|
yield item |
||||
|
|
||||
|
|
||||
|
def test_cached_properties(): |
||||
|
# cached_property |
||||
|
class Test: |
||||
|
@utils.cached_property |
||||
|
def time(self) -> float: |
||||
|
return time.perf_counter() |
||||
|
|
||||
|
instance = Test() |
||||
|
|
||||
|
assert instance.time == instance.time |
||||
|
|
||||
|
# cached_slot_property |
||||
|
class TestSlotted: |
||||
|
__slots__ = ( |
||||
|
'_cs_time' |
||||
|
) |
||||
|
|
||||
|
@utils.cached_slot_property('_cs_time') |
||||
|
def time(self) -> float: |
||||
|
return time.perf_counter() |
||||
|
|
||||
|
instance = TestSlotted() |
||||
|
|
||||
|
assert instance.time == instance.time |
||||
|
assert not hasattr(instance, '__dict__') |
||||
|
|
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
('snowflake', 'time_tuple'), |
||||
|
[ |
||||
|
(10000000000000000, (2015, 1, 28, 14, 16, 25)), |
||||
|
(12345678901234567, (2015, 2, 4, 1, 37, 19)), |
||||
|
(100000000000000000, (2015, 10, 3, 22, 44, 17)), |
||||
|
(123456789012345678, (2015, 12, 7, 16, 13, 12)), |
||||
|
(661720302316814366, (2020, 1, 1, 0, 0, 14)), |
||||
|
(1000000000000000000, (2022, 7, 22, 11, 22, 59)), |
||||
|
] |
||||
|
) |
||||
|
def test_snowflake_time(snowflake: int, time_tuple: typing.Tuple[int, int, int, int, int, int]): |
||||
|
dt = utils.snowflake_time(snowflake) |
||||
|
|
||||
|
assert (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second) == time_tuple |
||||
|
|
||||
|
assert utils.time_snowflake(dt, high=False) <= snowflake <= utils.time_snowflake(dt, high=True) |
||||
|
|
||||
|
|
||||
|
@pytest.mark.asyncio |
||||
|
async def test_get_find(): |
||||
|
# Generate a dictionary of random keys to values |
||||
|
mapping = { |
||||
|
secrets.token_bytes(32): secrets.token_bytes(32) |
||||
|
for _ in range(100) |
||||
|
} |
||||
|
|
||||
|
# Turn it into a shuffled iterable of pairs |
||||
|
pair = collections.namedtuple('pair', 'key value') |
||||
|
array = [pair(key=k, value=v) for k, v in mapping.items()] |
||||
|
random.shuffle(array) |
||||
|
|
||||
|
# Confirm all values can be found |
||||
|
for key, value in mapping.items(): |
||||
|
# Sync get |
||||
|
item = utils.get(array, key=key) |
||||
|
assert item is not None |
||||
|
assert item.value == value |
||||
|
|
||||
|
# Async get |
||||
|
item = await utils.get(async_iterate(array), key=key) |
||||
|
assert item is not None |
||||
|
assert item.value == value |
||||
|
|
||||
|
# Sync find |
||||
|
item = utils.find(lambda i: i.key == key, array) |
||||
|
assert item is not None |
||||
|
assert item.value == value |
||||
|
|
||||
|
# Async find |
||||
|
item = await utils.find(lambda i: i.key == key, async_iterate(array)) |
||||
|
assert item is not None |
||||
|
assert item.value == value |
||||
|
|
||||
|
|
||||
|
def test_get_slots(): |
||||
|
class A: |
||||
|
__slots__ = ('one', 'two') |
||||
|
|
||||
|
class B(A): |
||||
|
__slots__ = ('three', 'four') |
||||
|
|
||||
|
class C(B): |
||||
|
__slots__ = ('five', 'six') |
||||
|
|
||||
|
assert set(utils.get_slots(C)) == {'one', 'two', 'three', 'four', 'five', 'six'} |
||||
|
|
||||
|
|
||||
|
def test_valid_icon_size(): |
||||
|
# Valid icon sizes |
||||
|
for size in [16, 32, 64, 128, 256, 512, 1024, 2048, 4096]: |
||||
|
assert utils.valid_icon_size(size) |
||||
|
|
||||
|
# Some not valid icon sizes |
||||
|
for size in [-1, 0, 20, 103, 500, 8192]: |
||||
|
assert not utils.valid_icon_size(size) |
||||
|
|
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
('url', 'code'), |
||||
|
[ |
||||
|
('https://discordapp.com/invite/dpy', 'dpy'), |
||||
|
('https://discord.com/invite/dpy', 'dpy'), |
||||
|
('https://discord.gg/dpy', 'dpy'), |
||||
|
] |
||||
|
) |
||||
|
def test_resolve_invite(url, code): |
||||
|
assert utils.resolve_invite(url) == code |
||||
|
|
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
('url', 'code'), |
||||
|
[ |
||||
|
('https://discordapp.com/template/foobar', 'foobar'), |
||||
|
('https://discord.com/template/foobar', 'foobar'), |
||||
|
('https://discord.new/foobar', 'foobar'), |
||||
|
] |
||||
|
) |
||||
|
def test_resolve_template(url, code): |
||||
|
assert utils.resolve_template(url) == code |
||||
|
|
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
'mention', |
||||
|
['@everyone', '@here'] |
||||
|
) |
||||
|
def test_escape_mentions(mention): |
||||
|
assert mention not in utils.escape_mentions(mention) |
||||
|
assert mention not in utils.escape_mentions(f"one {mention} two") |
||||
|
|
||||
|
|
||||
|
@pytest.mark.asyncio |
||||
|
@pytest.mark.parametrize( |
||||
|
('source', 'chunk_size', 'chunked'), |
||||
|
[ |
||||
|
([1, 2, 3, 4, 5, 6], 2, [[1, 2], [3, 4], [5, 6]]), |
||||
|
([1, 2, 3, 4, 5, 6], 3, [[1, 2, 3], [4, 5, 6]]), |
||||
|
([1, 2, 3, 4, 5, 6], 4, [[1, 2, 3, 4], [5, 6]]), |
||||
|
([1, 2, 3, 4, 5, 6], 5, [[1, 2, 3, 4, 5], [6]]), |
||||
|
] |
||||
|
) |
||||
|
async def test_as_chunks(source, chunk_size, chunked): |
||||
|
assert [x for x in utils.as_chunks(source, chunk_size)] == chunked |
||||
|
assert [x async for x in utils.as_chunks(async_iterate(source), chunk_size)] == chunked |
||||
|
|
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
('annotation', 'resolved'), |
||||
|
[ |
||||
|
(datetime.datetime, datetime.datetime), |
||||
|
('datetime.datetime', datetime.datetime), |
||||
|
('typing.Union[typing.Literal["a"], typing.Literal["b"]]', typing.Union[typing.Literal["a"], typing.Literal["b"]]), |
||||
|
('typing.Union[typing.Union[int, str], typing.Union[bool, dict]]', typing.Union[int, str, bool, dict]), |
||||
|
] |
||||
|
) |
||||
|
def test_resolve_annotation(annotation, resolved): |
||||
|
assert resolved == utils.resolve_annotation(annotation, globals(), locals(), None) |
||||
|
|
||||
|
|
||||
|
@pytest.mark.skipif(sys.version_info < (3, 10), reason="3.10 union syntax") |
||||
|
@pytest.mark.parametrize( |
||||
|
('annotation', 'resolved'), |
||||
|
[ |
||||
|
('int | None', typing.Optional[int]), |
||||
|
('str | int', typing.Union[str, int]), |
||||
|
('str | int | None', typing.Optional[typing.Union[str, int]]), |
||||
|
] |
||||
|
) |
||||
|
def test_resolve_annotation_310(annotation, resolved): |
||||
|
assert resolved == utils.resolve_annotation(annotation, globals(), locals(), None) |
||||
|
|
||||
|
|
||||
|
# is_inside_class tests |
||||
|
|
||||
|
def not_a_class(): |
||||
|
def not_a_class_either(): |
||||
|
pass |
||||
|
return not_a_class_either |
||||
|
|
||||
|
class ThisIsAClass: |
||||
|
def in_a_class(self): |
||||
|
def not_directly_in_a_class(): |
||||
|
pass |
||||
|
return not_directly_in_a_class |
||||
|
|
||||
|
@classmethod |
||||
|
def a_class_method(cls): |
||||
|
def not_directly_in_a_class(): |
||||
|
pass |
||||
|
return not_directly_in_a_class |
||||
|
|
||||
|
@staticmethod |
||||
|
def a_static_method(): |
||||
|
def not_directly_in_a_class(): |
||||
|
pass |
||||
|
return not_directly_in_a_class |
||||
|
|
||||
|
class SubClass: |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
def test_is_inside_class(): |
||||
|
assert not utils.is_inside_class(not_a_class) |
||||
|
assert not utils.is_inside_class(not_a_class()) |
||||
|
assert not utils.is_inside_class(ThisIsAClass) |
||||
|
assert utils.is_inside_class(ThisIsAClass.in_a_class) |
||||
|
assert utils.is_inside_class(ThisIsAClass.a_class_method) |
||||
|
assert utils.is_inside_class(ThisIsAClass.a_static_method) |
||||
|
assert not utils.is_inside_class(ThisIsAClass().in_a_class()) |
||||
|
assert not utils.is_inside_class(ThisIsAClass.a_class_method()) |
||||
|
assert not utils.is_inside_class(ThisIsAClass().a_static_method()) |
||||
|
assert not utils.is_inside_class(ThisIsAClass.a_static_method()) |
||||
|
# Only really designed for callables, although I guess it is callable due to the constructor |
||||
|
assert utils.is_inside_class(ThisIsAClass.SubClass) |
||||
|
|
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
('dt', 'style', 'formatted'), |
||||
|
[ |
||||
|
(datetime.datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=datetime.timezone.utc), None, '<t:0>'), |
||||
|
(datetime.datetime(2020, 1, 1, 0, 0, 0, 0, tzinfo=datetime.timezone.utc), None, '<t:1577836800>'), |
||||
|
(datetime.datetime(2020, 1, 1, 0, 0, 0, 0, tzinfo=datetime.timezone.utc), 'F', '<t:1577836800:F>'), |
||||
|
(datetime.datetime(2033, 5, 18, 3, 33, 20, 0, tzinfo=datetime.timezone.utc), 'D', '<t:2000000000:D>'), |
||||
|
] |
||||
|
) |
||||
|
def test_format_dt(dt: datetime.datetime, style: typing.Optional[utils.TimestampStyle], formatted: str): |
||||
|
assert utils.format_dt(dt, style=style) == formatted |
Loading…
Reference in new issue