From 23ed891612f4050f5d19bae3028ae47eb8500239 Mon Sep 17 00:00:00 2001 From: evgfilim1 Date: Sun, 6 Nov 2022 20:01:24 +0500 Subject: [PATCH 01/12] Initial work on tests Add necessary fixtures Add CommandsModule tests --- dev-requirements.txt | 9 ++ tests/__init__.py | 0 tests/conftest.py | 67 +++++++++++++ tests/test_modules/__init__.py | 0 tests/test_modules/test_commands.py | 141 ++++++++++++++++++++++++++++ userbot/modules/commands.py | 1 + 6 files changed, 218 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_modules/__init__.py create mode 100644 tests/test_modules/test_commands.py diff --git a/dev-requirements.txt b/dev-requirements.txt index 12a5115..1490221 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,13 @@ +# i18n Babel~=2.11.0 + +# Code style black~=22.10.0 isort~=5.10.1 pre-commit~=2.20.0 + +# Tests +pytest~=7.2.0 +pytest-asyncio~=0.20.1 +# TODO (2022-11-06): pytest-cov~=4.0.0 +# TODO (2022-11-06): hypothesis~=6.56.4 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..38f3a26 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import asyncio +import os +import random + +import pytest +import pytest_asyncio +from pyrogram import Client + +from userbot import __version__ +from userbot.config import Config, RedisConfig + + +@pytest.fixture(scope="session") +def config() -> Config: + os.environ.setdefault("SESSION", "test") + # https://github.com/telegramdesktop/tdesktop/blob/a4b0443/Telegram/cmake/telegram_options.cmake#L15-L18 + os.environ.setdefault("API_ID", "17349") + os.environ.setdefault("API_HASH", "344583e45741c457fe1862106095a5eb") + + config = Config.from_env() + + config.kwargs.setdefault("test_mode", "1") + config.kwargs.setdefault("in_memory", "1") + if config.kwargs.get("phone_number", None) is None: + dc_n = str(random.randint(1, 3)) + random_n = f"{random.randint(0, 9999):4d}" + # https://docs.pyrogram.org/topics/test-servers#test-numbers + config.kwargs["phone_number"] = f"99966{dc_n}{random_n}" + config.kwargs["phone_code"] = dc_n * 5 + + return config + + +@pytest.fixture(scope="session") +def redis_config() -> RedisConfig: + os.environ.setdefault("REDIS_HOST", "localhost") + os.environ.setdefault("REDIS_DB", "2") + return RedisConfig.from_env() + + +@pytest.fixture(scope="session") +def event_loop() -> asyncio.AbstractEventLoop: + """Overrides pytest default function scoped event loop""" + policy = asyncio.get_event_loop_policy() + loop = policy.new_event_loop() + yield loop + loop.close() + + +@pytest_asyncio.fixture(scope="session") +async def client(config: Config) -> Client: + app = Client( + name=config.session, + api_id=config.api_id, + api_hash=config.api_hash, + app_version=f"evgfilim1/userbot {__version__} TEST", + device_model="Linux", + test_mode=config.kwargs.pop("test_mode") == "1", + in_memory=config.kwargs.pop("in_memory") == "1", + workdir=str(config.data_location), + **config.kwargs, + ) + await app.start() + yield app + await app.stop() diff --git a/tests/test_modules/__init__.py b/tests/test_modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_commands.py b/tests/test_modules/test_commands.py new file mode 100644 index 0000000..aa0c71f --- /dev/null +++ b/tests/test_modules/test_commands.py @@ -0,0 +1,141 @@ +from unittest.mock import patch + +from pyrogram import Client + +from userbot.modules.commands import CommandsHandler, CommandsModule + + +# region Test `CommandsModule` +def test_add_callable() -> None: + """Tests add() can be used as a callable.""" + + commands = CommandsModule() + + async def handler() -> None: + """Test handler""" + pass + + with patch.object( + CommandsModule, + "add_handler", + autospec=True, + side_effect=CommandsModule.add_handler, + ) as mock: + commands.add(handler, "test1", "test2", prefix="?", usage="") + + mock.assert_called_once() + + assert len(commands._handlers) == 1 + + +def test_add_decorator() -> None: + """Tests add() can be used as a decorator.""" + commands = CommandsModule() + + with patch.object( + CommandsModule, + "add_handler", + autospec=True, + side_effect=CommandsModule.add_handler, + ) as mock: + + @commands.add("test1", "test2", prefix="?", usage="") + async def handler() -> None: + """Test handler""" + pass + + mock.assert_called_once() + + assert len(commands._handlers) == 1 + + +def test_add_args() -> None: + """Tests add() arguments are passed to the handler.""" + commands = CommandsModule() + + async def handler() -> None: + """Test handler""" + pass + + with patch.object( + CommandsModule, + "add_handler", + autospec=True, + ) as mock: + commands.add( + handler, + "test1", + "test2", + prefix="?", + usage="", + category="test", + hidden=True, + handle_edits=False, + waiting_message="test123", + timeout=42, + ) + + h: CommandsHandler = mock.call_args.args[1] + + assert all(expected == actual for expected, actual in zip(("test1", "test2"), iter(h.commands))) + assert h.prefix == "?" + assert h.handler is handler + assert h.usage == "" + assert h.doc == handler.__doc__ + assert h.category == "test" + assert h.hidden is True + assert h.handle_edits is False + assert h.waiting_message == "test123" + assert h.timeout == 42 + + +def test_register(client: Client) -> None: + """Tests register() adds all handlers to pyrogram Client""" + + commands = CommandsModule() + + @commands.add("foo") + async def foo() -> None: + """Test handler""" + pass + + @commands.add("bar", handle_edits=False) + async def bar() -> None: + """Test handler""" + pass + + with patch.object( + Client, + "add_handler", + autospec=True, + ) as mock: + commands.register(client) + + # Three handlers will be added: "foo", edited "foo" and "bar". + assert mock.call_count == 3 + + +def test_register_root(client: Client) -> None: + """Tests register() adds all handlers + help handler to pyrogram Client""" + + commands = CommandsModule(root=True) + + @commands.add("foo") + async def foo() -> None: + """Test handler""" + pass + + with patch.object( + Client, + "add_handler", + autospec=True, + ) as mock: + commands.register(client) + + # Two handlers will be added: "foo", edited "foo", "help" and edited "help". + assert mock.call_count == 4 + # Root CommandsModule also registers some necessary middlewares + assert commands._middleware.has_handlers + + +# endregion diff --git a/userbot/modules/commands.py b/userbot/modules/commands.py index 9b8b4cf..ddf7ba3 100644 --- a/userbot/modules/commands.py +++ b/userbot/modules/commands.py @@ -496,6 +496,7 @@ def register(self, client: Client) -> None: ) if self._ensure_middlewares_registered: # These middlewares are expected by the base module to be registered + # TODO (2022-11-06): flag to disable this behavior if icon_middleware not in self._middleware: self.add_middleware(icon_middleware) if translate_middleware not in self._middleware: From add89d5ca73f3fe50904586adabe79fc8a6d39da Mon Sep 17 00:00:00 2001 From: evgfilim1 Date: Sun, 6 Nov 2022 20:07:26 +0500 Subject: [PATCH 02/12] Run "Lint code" Action on `tests` dir --- .github/workflows/lint.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 7dcd064..da608a3 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -7,6 +7,7 @@ on: paths: - ".github/workflows/lint.yaml" - "locales/*.pot" + - "tests/**" - "userbot/**" - ".pre-commit-config.yaml" - "dev-requirements.txt" @@ -18,6 +19,7 @@ on: paths: - ".github/workflows/lint.yaml" - "locales/*.pot" + - "tests/**" - "userbot/**" - ".pre-commit-config.yaml" - "dev-requirements.txt" @@ -56,5 +58,5 @@ jobs: - name: Check code style run: | - isort --check --diff userbot - black --check --diff userbot + isort --check --diff userbot tests + black --check --diff userbot tests From b173a7c196690714cc9161c8f2c93fcc7d52c313 Mon Sep 17 00:00:00 2001 From: evgfilim1 Date: Sun, 6 Nov 2022 20:15:09 +0500 Subject: [PATCH 03/12] Add "Test code" Action --- .github/workflows/test.yaml | 47 +++++++++++++++++++++++++++++++++++++ pyproject.toml | 5 ++++ 2 files changed, 52 insertions(+) create mode 100644 .github/workflows/test.yaml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..ba73e1e --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,47 @@ +name: Test code + +on: + push: + branches: + - master + paths: + - ".github/workflows/test.yaml" + - "tests/**" + - "userbot/**" + - "dev-requirements.txt" + - "pyproject.toml" + - "requirements.txt" + pull_request: + branches: + - master + paths: + - ".github/workflows/test.yaml" + - "tests/**" + - "userbot/**" + - "dev-requirements.txt" + - "pyproject.toml" + - "requirements.txt" + +permissions: + contents: read + +jobs: + test: + name: Test code using pytest + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Install Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + cache: pip + + - name: Install packages + run: pip install -r dev-requirements.txt -r requirements.txt + + - name: Run tests + run: pytest diff --git a/pyproject.toml b/pyproject.toml index bcb9333..5ac55b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,3 +7,8 @@ line_length = 100 py_version = '310' # (2022-10-28) isort 5.10.1 doesn't support 3.11 profile = 'black' known_first_party = ['userbot'] + +[tool.pytest.ini_options] +testpaths = [ + "tests", +] From 6479f24119edb318b7a4bd607df6cf487283b45a Mon Sep 17 00:00:00 2001 From: evgfilim1 Date: Sun, 6 Nov 2022 20:59:56 +0500 Subject: [PATCH 04/12] Register a test user automatically if needed Save credentials for future test runs not to abuse Telegram API --- tests/conftest.py | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 38f3a26..9a1af31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,15 @@ from __future__ import annotations import asyncio +import json import os import random +from typing import AsyncIterable import pytest import pytest_asyncio from pyrogram import Client +from pyrogram.types import TermsOfService, User from userbot import __version__ from userbot.config import Config, RedisConfig @@ -24,11 +27,22 @@ def config() -> Config: config.kwargs.setdefault("test_mode", "1") config.kwargs.setdefault("in_memory", "1") if config.kwargs.get("phone_number", None) is None: - dc_n = str(random.randint(1, 3)) - random_n = f"{random.randint(0, 9999):4d}" - # https://docs.pyrogram.org/topics/test-servers#test-numbers - config.kwargs["phone_number"] = f"99966{dc_n}{random_n}" - config.kwargs["phone_code"] = dc_n * 5 + try: + with open(config.data_location / ".test_phone.json") as f: + phone_number, phone_code = json.load(f) + except FileNotFoundError: + dc_n = str(random.randint(1, 3)) + random_n = f"{random.randint(0, 9999):4d}" + # https://docs.pyrogram.org/topics/test-servers#test-numbers + phone_number = f"99966{dc_n}{random_n}" + phone_code = dc_n * 5 + try: + with open(config.data_location / ".test_phone.json", "w") as f: + json.dump([phone_number, phone_code], f) + except OSError: + pass + config.kwargs["phone_number"] = phone_number + config.kwargs["phone_code"] = phone_code return config @@ -50,7 +64,7 @@ def event_loop() -> asyncio.AbstractEventLoop: @pytest_asyncio.fixture(scope="session") -async def client(config: Config) -> Client: +async def client(config: Config) -> AsyncIterable[Client]: app = Client( name=config.session, api_id=config.api_id, @@ -62,6 +76,23 @@ async def client(config: Config) -> Client: workdir=str(config.data_location), **config.kwargs, ) + # Make sure we are registered, register otherwise + is_authorized = await app.connect() + if not is_authorized: + phone_number = config.kwargs["phone_number"] + code = await app.send_code(phone_number) + signed_in = await app.sign_in( + phone_number, code.phone_code_hash, config.kwargs["phone_code"] + ) + if not isinstance(signed_in, User): + await app.sign_up( + phone_number, + code.phone_code_hash, + f"Test {phone_number[-4:]}", + ) + if isinstance(signed_in, TermsOfService): + await app.accept_terms_of_service(signed_in.id) + await app.disconnect() await app.start() yield app await app.stop() From d72c543403e4222cb1c723d4fb1e62e64391715e Mon Sep 17 00:00:00 2001 From: evgfilim1 Date: Sun, 6 Nov 2022 21:00:33 +0500 Subject: [PATCH 05/12] Use environment variables in "Test code" Action --- .github/workflows/test.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ba73e1e..192c4a0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -44,4 +44,8 @@ jobs: run: pip install -r dev-requirements.txt -r requirements.txt - name: Run tests - run: pytest + env: + ENV_FILE_CONTENT: ${{ secrets.TEST_ENV_FILE_CONTENT }} + run: | + eval "$(echo "$ENV_FILE_CONTENT" | sed '/^#/d;s/^/export /')" + pytest From 6cd52e572cf92f043468d75fcea0519cda4eb64e Mon Sep 17 00:00:00 2001 From: evgfilim1 Date: Sun, 6 Nov 2022 21:05:08 +0500 Subject: [PATCH 06/12] Use `$GITHUB_ENV` to set environment variables --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 192c4a0..8dc615c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -47,5 +47,5 @@ jobs: env: ENV_FILE_CONTENT: ${{ secrets.TEST_ENV_FILE_CONTENT }} run: | - eval "$(echo "$ENV_FILE_CONTENT" | sed '/^#/d;s/^/export /')" + echo "$ENV_FILE_CONTENT" >>"$GITHUB_ENV" pytest From 348c8e2c3cea50e9c1b7267bdd552d6c1dabe1c1 Mon Sep 17 00:00:00 2001 From: evgfilim1 Date: Tue, 8 Nov 2022 11:34:18 +0500 Subject: [PATCH 07/12] Integrate `hypothesis` in tests Add CI="true" env for "Test code" Action --- .github/workflows/test.yaml | 3 + dev-requirements.txt | 2 +- tests/test_modules/test_commands.py | 109 ++++++++++++++++++---------- 3 files changed, 73 insertions(+), 41 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8dc615c..b598ca9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -25,6 +25,9 @@ on: permissions: contents: read +env: + CI: "true" + jobs: test: name: Test code using pytest diff --git a/dev-requirements.txt b/dev-requirements.txt index 1490221..1728b3b 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -7,7 +7,7 @@ isort~=5.10.1 pre-commit~=2.20.0 # Tests +hypothesis~=6.56.4 pytest~=7.2.0 pytest-asyncio~=0.20.1 # TODO (2022-11-06): pytest-cov~=4.0.0 -# TODO (2022-11-06): hypothesis~=6.56.4 diff --git a/tests/test_modules/test_commands.py b/tests/test_modules/test_commands.py index aa0c71f..ee54139 100644 --- a/tests/test_modules/test_commands.py +++ b/tests/test_modules/test_commands.py @@ -1,11 +1,20 @@ +import string from unittest.mock import patch +from hypothesis import assume, given +from hypothesis import strategies as st from pyrogram import Client +from userbot.modules.base import HandlerT from userbot.modules.commands import CommandsHandler, CommandsModule +async def _sample_handler() -> str | None: + pass + + # region Test `CommandsModule` +# TODO (2022-11-07): check expected failure cases def test_add_callable() -> None: """Tests add() can be used as a callable.""" @@ -49,13 +58,19 @@ async def handler() -> None: assert len(commands._handlers) == 1 -def test_add_args() -> None: +@given( + handler=st.builds( + CommandsHandler, + commands=st.lists(st.text(string.ascii_letters), min_size=1), + prefix=st.sampled_from(string.punctuation), + handler=st.functions(like=_sample_handler), + ), +) +def test_add_args(handler: CommandsHandler) -> None: """Tests add() arguments are passed to the handler.""" - commands = CommandsModule() + assume(handler.category != "" and handler.doc != "") # These values are considered `None` - async def handler() -> None: - """Test handler""" - pass + commands = CommandsModule() with patch.object( CommandsModule, @@ -63,46 +78,60 @@ async def handler() -> None: autospec=True, ) as mock: commands.add( - handler, - "test1", - "test2", - prefix="?", - usage="", - category="test", - hidden=True, - handle_edits=False, - waiting_message="test123", - timeout=42, + handler.handler, + *handler.commands, + prefix=handler.prefix, + usage=handler.usage, + doc=handler.doc, + category=handler.category, + hidden=handler.hidden, + handle_edits=handler.handle_edits, + waiting_message=handler.waiting_message, + timeout=handler.timeout, ) h: CommandsHandler = mock.call_args.args[1] - assert all(expected == actual for expected, actual in zip(("test1", "test2"), iter(h.commands))) - assert h.prefix == "?" - assert h.handler is handler - assert h.usage == "" - assert h.doc == handler.__doc__ - assert h.category == "test" - assert h.hidden is True - assert h.handle_edits is False - assert h.waiting_message == "test123" - assert h.timeout == 42 - - -def test_register(client: Client) -> None: + assert all(expected == actual for expected, actual in zip(handler.commands, iter(h.commands))) + assert h.prefix == handler.prefix + assert h.handler is handler.handler + assert h.usage == handler.usage + assert h.doc == handler.doc + assert h.category == handler.category + assert h.hidden == handler.hidden + assert h.handle_edits == handler.handle_edits + assert h.waiting_message == handler.waiting_message + assert h.timeout == handler.timeout + + +@given( + handlers=st.lists( + st.tuples( + st.functions(like=_sample_handler), + st.lists(st.text(string.ascii_letters), min_size=1, unique=True), + st.booleans(), + ), + ) +) +def test_register(handlers: list[tuple[HandlerT, list[str], bool]], client: Client) -> None: """Tests register() adds all handlers to pyrogram Client""" + # Don't repeat commands between handlers + # TODO (2022-11-08): optimize this + assume( + all( + cmd not in other[1] + for handler in handlers + for other in handlers + for cmd in handler[1] + if other != handler + ) + ) commands = CommandsModule() - - @commands.add("foo") - async def foo() -> None: - """Test handler""" - pass - - @commands.add("bar", handle_edits=False) - async def bar() -> None: - """Test handler""" - pass + handler_count = 0 + for handler, command_list, handle_edits in handlers: + commands.add(handler, *command_list, handle_edits=handle_edits) + handler_count += 1 + int(handle_edits) with patch.object( Client, @@ -112,11 +141,11 @@ async def bar() -> None: commands.register(client) # Three handlers will be added: "foo", edited "foo" and "bar". - assert mock.call_count == 3 + assert mock.call_count == handler_count def test_register_root(client: Client) -> None: - """Tests register() adds all handlers + help handler to pyrogram Client""" + """Tests register() adds a handler + help handler to pyrogram Client""" commands = CommandsModule(root=True) From f871b4e81707d5d39900be169c7a4f2027d5b6a8 Mon Sep 17 00:00:00 2001 From: evgfilim1 Date: Tue, 8 Nov 2022 12:03:39 +0500 Subject: [PATCH 08/12] Log out automatically if the Pyrogram session is in-memory one --- tests/conftest.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9a1af31..1efbc90 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -65,6 +65,7 @@ def event_loop() -> asyncio.AbstractEventLoop: @pytest_asyncio.fixture(scope="session") async def client(config: Config) -> AsyncIterable[Client]: + in_memory = config.kwargs.pop("in_memory") == "1" app = Client( name=config.session, api_id=config.api_id, @@ -72,7 +73,7 @@ async def client(config: Config) -> AsyncIterable[Client]: app_version=f"evgfilim1/userbot {__version__} TEST", device_model="Linux", test_mode=config.kwargs.pop("test_mode") == "1", - in_memory=config.kwargs.pop("in_memory") == "1", + in_memory=in_memory, workdir=str(config.data_location), **config.kwargs, ) @@ -95,4 +96,7 @@ async def client(config: Config) -> AsyncIterable[Client]: await app.disconnect() await app.start() yield app - await app.stop() + if in_memory: + await app.log_out() + else: + await app.stop() From 4703dcfcd0b59fb08addc0a6eeaf555817f706e3 Mon Sep 17 00:00:00 2001 From: evgfilim1 Date: Tue, 8 Nov 2022 14:00:04 +0500 Subject: [PATCH 09/12] Update `conftest.py` to use `SecretStr` --- tests/conftest.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1efbc90..1f2b8c7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,7 @@ from userbot import __version__ from userbot.config import Config, RedisConfig +from userbot.utils import SecretStr @pytest.fixture(scope="session") @@ -24,8 +25,8 @@ def config() -> Config: config = Config.from_env() - config.kwargs.setdefault("test_mode", "1") - config.kwargs.setdefault("in_memory", "1") + config.kwargs.setdefault("test_mode", SecretStr("1")) + config.kwargs.setdefault("in_memory", SecretStr("1")) if config.kwargs.get("phone_number", None) is None: try: with open(config.data_location / ".test_phone.json") as f: @@ -41,8 +42,8 @@ def config() -> Config: json.dump([phone_number, phone_code], f) except OSError: pass - config.kwargs["phone_number"] = phone_number - config.kwargs["phone_code"] = phone_code + config.kwargs["phone_number"] = SecretStr(phone_number) + config.kwargs["phone_code"] = SecretStr(phone_code) return config @@ -69,7 +70,7 @@ async def client(config: Config) -> AsyncIterable[Client]: app = Client( name=config.session, api_id=config.api_id, - api_hash=config.api_hash, + api_hash=config.api_hash.value, app_version=f"evgfilim1/userbot {__version__} TEST", device_model="Linux", test_mode=config.kwargs.pop("test_mode") == "1", @@ -80,10 +81,10 @@ async def client(config: Config) -> AsyncIterable[Client]: # Make sure we are registered, register otherwise is_authorized = await app.connect() if not is_authorized: - phone_number = config.kwargs["phone_number"] + phone_number = config.kwargs["phone_number"].value code = await app.send_code(phone_number) signed_in = await app.sign_in( - phone_number, code.phone_code_hash, config.kwargs["phone_code"] + phone_number, code.phone_code_hash, config.kwargs["phone_code"].value ) if not isinstance(signed_in, User): await app.sign_up( From f0b6b6b1dd838f30e56f18efa5926e62725f4328 Mon Sep 17 00:00:00 2001 From: evgfilim1 Date: Sat, 12 Nov 2022 01:13:22 +0500 Subject: [PATCH 10/12] Use fake client instead of a real one Remove asserts not related to `CommandsHandler` Optimize `assume` expression --- tests/test_modules/test_commands.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/tests/test_modules/test_commands.py b/tests/test_modules/test_commands.py index ee54139..4585135 100644 --- a/tests/test_modules/test_commands.py +++ b/tests/test_modules/test_commands.py @@ -34,8 +34,6 @@ async def handler() -> None: mock.assert_called_once() - assert len(commands._handlers) == 1 - def test_add_decorator() -> None: """Tests add() can be used as a decorator.""" @@ -55,8 +53,6 @@ async def handler() -> None: mock.assert_called_once() - assert len(commands._handlers) == 1 - @given( handler=st.builds( @@ -113,21 +109,17 @@ def test_add_args(handler: CommandsHandler) -> None: ), ) ) -def test_register(handlers: list[tuple[HandlerT, list[str], bool]], client: Client) -> None: +def test_register(handlers: list[tuple[HandlerT, list[str], bool]]) -> None: """Tests register() adds all handlers to pyrogram Client""" # Don't repeat commands between handlers - # TODO (2022-11-08): optimize this - assume( - all( - cmd not in other[1] - for handler in handlers - for other in handlers - for cmd in handler[1] - if other != handler - ) - ) + all_commands = set() + for _, commands, _ in handlers: + for command in commands: + assume(command not in all_commands) + all_commands.add(command) commands = CommandsModule() + fake_client = Client("fake", in_memory=True) handler_count = 0 for handler, command_list, handle_edits in handlers: commands.add(handler, *command_list, handle_edits=handle_edits) @@ -138,16 +130,17 @@ def test_register(handlers: list[tuple[HandlerT, list[str], bool]], client: Clie "add_handler", autospec=True, ) as mock: - commands.register(client) + commands.register(fake_client) # Three handlers will be added: "foo", edited "foo" and "bar". assert mock.call_count == handler_count -def test_register_root(client: Client) -> None: +def test_register_root() -> None: """Tests register() adds a handler + help handler to pyrogram Client""" commands = CommandsModule(root=True) + fake_client = Client("fake", in_memory=True) @commands.add("foo") async def foo() -> None: @@ -159,7 +152,7 @@ async def foo() -> None: "add_handler", autospec=True, ) as mock: - commands.register(client) + commands.register(fake_client) # Two handlers will be added: "foo", edited "foo", "help" and edited "help". assert mock.call_count == 4 From c05531a1627a0753084bd672aa9a85015d7a6b81 Mon Sep 17 00:00:00 2001 From: evgfilim1 Date: Sat, 12 Nov 2022 01:21:23 +0500 Subject: [PATCH 11/12] Add more tests --- tests/test_modules/test_commands.py | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/test_modules/test_commands.py b/tests/test_modules/test_commands.py index 4585135..9b1ac27 100644 --- a/tests/test_modules/test_commands.py +++ b/tests/test_modules/test_commands.py @@ -1,6 +1,7 @@ import string from unittest.mock import patch +import pytest from hypothesis import assume, given from hypothesis import strategies as st from pyrogram import Client @@ -100,6 +101,27 @@ def test_add_args(handler: CommandsHandler) -> None: assert h.timeout == handler.timeout +def test_invalid_add() -> None: + """Tests add() raises ValueError when invalid arguments are passed.""" + commands = CommandsModule() + + async def handler() -> None: + """Test handler""" + pass + + with patch.object( + CommandsModule, + "add_handler", + autospec=True, + ) as mock: + with pytest.raises(ValueError): + # This call should fail because no commands are passed. + # Type linting is disabled because it's expected to fail. + commands.add(handler, prefix="?", usage="") # noqa # type: ignore + + mock.assert_not_called() + + @given( handlers=st.lists( st.tuples( @@ -160,4 +182,27 @@ async def foo() -> None: assert commands._middleware.has_handlers +def test_register_duplicates() -> None: + """Tests register() raises ValueError when duplicate commands are added.""" + + commands = CommandsModule() + fake_client = Client("fake", in_memory=True) + + @commands.add("foo") + @commands.add("foo") + async def foo() -> None: + """Test handler""" + pass + + with patch.object( + Client, + "add_handler", + autospec=True, + ) as mock: + with pytest.raises(ValueError): + commands.register(fake_client) + + mock.assert_not_called() + + # endregion From 6566ca5258e56858c13a8079ebac56a7ebb42713 Mon Sep 17 00:00:00 2001 From: evgfilim1 Date: Tue, 15 Nov 2022 03:23:28 +0500 Subject: [PATCH 12/12] Fix tests --- tests/test_modules/test_commands.py | 12 ++++++------ userbot/modules/commands.py | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/test_modules/test_commands.py b/tests/test_modules/test_commands.py index 9b1ac27..2a88e6f 100644 --- a/tests/test_modules/test_commands.py +++ b/tests/test_modules/test_commands.py @@ -132,7 +132,7 @@ async def handler() -> None: ) ) def test_register(handlers: list[tuple[HandlerT, list[str], bool]]) -> None: - """Tests register() adds all handlers to pyrogram Client""" + """Tests register() adds all handlers + help handlers to pyrogram Client""" # Don't repeat commands between handlers all_commands = set() for _, commands, _ in handlers: @@ -154,14 +154,14 @@ def test_register(handlers: list[tuple[HandlerT, list[str], bool]]) -> None: ) as mock: commands.register(fake_client) - # Three handlers will be added: "foo", edited "foo" and "bar". - assert mock.call_count == handler_count + # Four handlers will be added: "foo", edited "foo", "bar", "help" and edited "help". + assert mock.call_count == handler_count + 2 -def test_register_root() -> None: - """Tests register() adds a handler + help handler to pyrogram Client""" +def test_register_with_middlewares() -> None: + """Tests register() adds a handler + help handler + middlewares to pyrogram Client""" - commands = CommandsModule(root=True) + commands = CommandsModule(ensure_middlewares_registered=True) fake_client = Client("fake", in_memory=True) @commands.add("foo") diff --git a/userbot/modules/commands.py b/userbot/modules/commands.py index ddf7ba3..9b8b4cf 100644 --- a/userbot/modules/commands.py +++ b/userbot/modules/commands.py @@ -496,7 +496,6 @@ def register(self, client: Client) -> None: ) if self._ensure_middlewares_registered: # These middlewares are expected by the base module to be registered - # TODO (2022-11-06): flag to disable this behavior if icon_middleware not in self._middleware: self.add_middleware(icon_middleware) if translate_middleware not in self._middleware: