mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
API changes: - Cogs must now inherit from `commands.Cog` (see #2151 for discussion and more details) - All functions which are not decorators in the `redbot.core.checks` module are now deprecated in favour of their counterparts in `redbot.core.utils.mod`. This is to make this module more consistent and end the confusing naming convention. - `redbot.core.checks.check_overrides` function is now gone, overrideable checks can now be created with the `@commands.permissions_check` decorator - Command, Group, Cog and Context have some new attributes and methods, but they are for internal use so shouldn't concern cog creators (unless they're making a permissions cog!). - `__permissions_check_before` and `__permissions_check_after` have been replaced: A cog method named `__permissions_hook` will be evaluated as permissions hooks in the same way `__permissions_check_before` previously was. Permissions hooks can also be added/removed/verified through the new `*_permissions_hook()` methods on the bot object, and they will be verified even when permissions is unloaded. - New utility method `redbot.core.utils.chat_formatting.humanize_list` - New dependency [`schema`](https://github.com/keleshev/schema) User-facing changes: - When a `@bot_has_permissions` check fails, the bot will respond saying what permissions were actually missing. - All YAML-related `[p]permissions` subcommands now reside under the `[p]permissions acl` sub-group (tbh I still think the whole cog has too many top-level commands) - The YAML schema for these commands has been changed - A rule cannot be set as allow and deny at the same time (previously this would just default to allow) Documentation: - New documentation for `redbot.core.commands.requires` and `redbot.core.checks` modules - Renewed documentation for the permissions cog - `sphinx.ext.doctest` is now enabled Note: standard discord.py checks will still behave exactly the same way, in fact they are checked before `Requires` is looked at, so they are not overrideable. Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
194 lines
6.0 KiB
Python
194 lines
6.0 KiB
Python
import asyncio
|
|
import pytest
|
|
import random
|
|
import textwrap
|
|
import warnings
|
|
from redbot.core.utils import (
|
|
chat_formatting,
|
|
bounded_gather,
|
|
bounded_gather_iter,
|
|
deduplicate_iterables,
|
|
)
|
|
|
|
|
|
def test_bordered_symmetrical():
|
|
expected = textwrap.dedent(
|
|
"""\
|
|
┌──────────────┐ ┌─────────────┐
|
|
│one │ │four │
|
|
│two │ │five │
|
|
│three │ │six │
|
|
└──────────────┘ └─────────────┘"""
|
|
)
|
|
col1, col2 = ["one", "two", "three"], ["four", "five", "six"]
|
|
assert chat_formatting.bordered(col1, col2) == expected
|
|
|
|
|
|
def test_bordered_asymmetrical():
|
|
expected = textwrap.dedent(
|
|
"""\
|
|
┌──────────────┐ ┌──────────────┐
|
|
│one │ │four │
|
|
│two │ │five │
|
|
│three │ │six │
|
|
└──────────────┘ │seven │
|
|
└──────────────┘"""
|
|
)
|
|
col1, col2 = ["one", "two", "three"], ["four", "five", "six", "seven"]
|
|
assert chat_formatting.bordered(col1, col2) == expected
|
|
|
|
|
|
def test_bordered_asymmetrical_2():
|
|
expected = textwrap.dedent(
|
|
"""\
|
|
┌──────────────┐ ┌─────────────┐
|
|
│one │ │five │
|
|
│two │ │six │
|
|
│three │ └─────────────┘
|
|
│four │
|
|
└──────────────┘ """
|
|
)
|
|
col1, col2 = ["one", "two", "three", "four"], ["five", "six"]
|
|
assert chat_formatting.bordered(col1, col2) == expected
|
|
|
|
|
|
def test_bordered_ascii():
|
|
expected = textwrap.dedent(
|
|
"""\
|
|
---------------- ---------------
|
|
|one | |four |
|
|
|two | |five |
|
|
|three | |six |
|
|
---------------- ---------------"""
|
|
)
|
|
col1, col2 = ["one", "two", "three"], ["four", "five", "six"]
|
|
assert chat_formatting.bordered(col1, col2, ascii_border=True) == expected
|
|
|
|
|
|
def test_deduplicate_iterables():
|
|
expected = [1, 2, 3, 4, 5]
|
|
inputs = [[1, 2, 1], [3, 1, 2, 4], [5, 1, 2]]
|
|
assert deduplicate_iterables(*inputs) == expected
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bounded_gather():
|
|
status = [0, 0] # num_running, max_running
|
|
|
|
async def wait_task(i, delay, status, fail=False):
|
|
status[0] += 1
|
|
await asyncio.sleep(delay)
|
|
status[1] = max(status)
|
|
status[0] -= 1
|
|
|
|
if fail:
|
|
raise RuntimeError
|
|
|
|
return i
|
|
|
|
num_concurrent = random.randint(2, 8)
|
|
num_tasks = random.randint(4 * num_concurrent, 5 * num_concurrent)
|
|
num_fail = random.randint(num_concurrent, num_tasks)
|
|
|
|
tasks = [wait_task(i, random.random() / 1000, status) for i in range(num_tasks)]
|
|
tasks += [wait_task(i, random.random() / 1000, status, fail=True) for i in range(num_fail)]
|
|
|
|
num_failed = 0
|
|
|
|
results = await bounded_gather(*tasks, limit=num_concurrent, return_exceptions=True)
|
|
|
|
for i, result in enumerate(results):
|
|
if isinstance(result, RuntimeError):
|
|
num_failed += 1
|
|
else:
|
|
assert result == i # verify_permissions original orde
|
|
assert 0 <= result < num_tasks
|
|
|
|
assert 0 < status[1] <= num_concurrent
|
|
assert num_fail == num_failed
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bounded_gather_iter():
|
|
status = [0, 0] # num_running, max_running
|
|
|
|
async def wait_task(i, delay, status, fail=False):
|
|
status[0] += 1
|
|
await asyncio.sleep(delay)
|
|
status[1] = max(status)
|
|
status[0] -= 1
|
|
|
|
if fail:
|
|
raise RuntimeError
|
|
|
|
return i
|
|
|
|
num_concurrent = random.randint(2, 8)
|
|
num_tasks = random.randint(4 * num_concurrent, 16 * num_concurrent)
|
|
num_fail = random.randint(num_concurrent, num_tasks)
|
|
|
|
tasks = [wait_task(i, random.random() / 1000, status) for i in range(num_tasks)]
|
|
tasks += [wait_task(i, random.random() / 1000, status, fail=True) for i in range(num_fail)]
|
|
random.shuffle(tasks)
|
|
|
|
num_failed = 0
|
|
|
|
for result in bounded_gather_iter(*tasks, limit=num_concurrent):
|
|
try:
|
|
result = await result
|
|
except RuntimeError:
|
|
num_failed += 1
|
|
continue
|
|
|
|
assert 0 <= result < num_tasks
|
|
|
|
assert 0 < status[1] <= num_concurrent
|
|
assert num_fail == num_failed
|
|
|
|
|
|
@pytest.mark.skip(reason="spams logs with pending task warnings")
|
|
@pytest.mark.asyncio
|
|
async def test_bounded_gather_iter_cancel():
|
|
status = [0, 0, 0] # num_running, max_running, num_ran
|
|
|
|
async def wait_task(i, delay, status, fail=False):
|
|
status[0] += 1
|
|
await asyncio.sleep(delay)
|
|
status[1] = max(status[:2])
|
|
status[0] -= 1
|
|
|
|
if fail:
|
|
raise RuntimeError
|
|
|
|
status[2] += 1
|
|
return i
|
|
|
|
num_concurrent = random.randint(2, 8)
|
|
num_tasks = random.randint(4 * num_concurrent, 16 * num_concurrent)
|
|
quit_on = random.randint(0, num_tasks)
|
|
num_fail = random.randint(num_concurrent, num_tasks)
|
|
|
|
tasks = [wait_task(i, random.random() / 1000, status) for i in range(num_tasks)]
|
|
tasks += [wait_task(i, random.random() / 1000, status, fail=True) for i in range(num_fail)]
|
|
random.shuffle(tasks)
|
|
|
|
num_failed = 0
|
|
i = 0
|
|
|
|
for result in bounded_gather_iter(*tasks, limit=num_concurrent):
|
|
try:
|
|
result = await result
|
|
except RuntimeError:
|
|
num_failed += 1
|
|
continue
|
|
|
|
if i == quit_on:
|
|
break
|
|
|
|
assert 0 <= result < num_tasks
|
|
i += 1
|
|
|
|
assert 0 < status[1] <= num_concurrent
|
|
assert quit_on <= status[2] <= quit_on + num_concurrent
|
|
assert num_failed <= num_fail
|