Thanks to visit codestin.com
Credit goes to github.com

Skip to content

PoC: pytest-socket like testing #4317

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ markers = [
"dev", # If you want to test a specific test, use this
"no_req",
"req",
"disable_httpx: Disable httpx requests for a specific test"
]
asyncio_mode = "auto"
log_format = "%(funcName)s - Line %(lineno)d - %(message)s"
Expand Down
25 changes: 15 additions & 10 deletions tests/_files/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from tests.auxil.files import data_file
from tests.auxil.networking import expect_bad_request
from tests.conftest import permissive_bot


@pytest.fixture()
Expand All @@ -30,10 +31,12 @@ def animation_file():


@pytest.fixture(scope="session")
async def animation(bot, chat_id, aiolib):
async def animation(permissive_bot, chat_id):
with data_file("game.gif").open("rb") as f, data_file("thumb.jpg").open("rb") as thumb:
return (
await bot.send_animation(chat_id, animation=f, read_timeout=50, thumbnail=thumb)
await permissive_bot.send_animation(
chat_id, animation=f, read_timeout=50, thumbnail=thumb
)
).animation


Expand All @@ -44,9 +47,11 @@ def audio_file():


@pytest.fixture(scope="session")
async def audio(bot, chat_id, aiolib):
async def audio(permissive_bot, chat_id):
with data_file("telegram.mp3").open("rb") as f, data_file("thumb.jpg").open("rb") as thumb:
return (await bot.send_audio(chat_id, audio=f, read_timeout=50, thumbnail=thumb)).audio
return (
await permissive_bot.send_audio(chat_id, audio=f, read_timeout=50, thumbnail=thumb)
).audio


@pytest.fixture()
Expand All @@ -56,9 +61,9 @@ def document_file():


@pytest.fixture(scope="session")
async def document(bot, chat_id, aiolib):
async def document(permissive_bot, chat_id):
with data_file("telegram.png").open("rb") as f:
return (await bot.send_document(chat_id, document=f, read_timeout=50)).document
return (await permissive_bot.send_document(chat_id, document=f, read_timeout=50)).document


@pytest.fixture()
Expand All @@ -68,10 +73,10 @@ def photo_file():


@pytest.fixture(scope="session")
async def photolist(bot, chat_id, aiolib):
async def photolist(permissive_bot, chat_id):
async def func():
with data_file("telegram.jpg").open("rb") as f:
return (await bot.send_photo(chat_id, photo=f, read_timeout=50)).photo
return (await permissive_bot.send_photo(chat_id, photo=f, read_timeout=50)).photo

return await expect_bad_request(
func, "Type of file mismatch", "Telegram did not accept the file."
Expand All @@ -95,9 +100,9 @@ def video_file():


@pytest.fixture(scope="session")
async def video(bot, chat_id, aiolib):
async def video(permissive_bot, chat_id):
with data_file("telegram.mp4").open("rb") as f:
return (await bot.send_video(chat_id, video=f, read_timeout=50)).video
return (await permissive_bot.send_video(chat_id, video=f, read_timeout=50)).video


@pytest.fixture()
Expand Down
1 change: 1 addition & 0 deletions tests/_files/test_animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def test_slot_behaviour(self, animation):
assert getattr(animation, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(animation)) == len(set(mro_slots(animation))), "duplicate slot"

@pytest.mark.disable_httpx
def test_creation(self, animation):
assert isinstance(animation, Animation)
assert isinstance(animation.file_id, str)
Expand Down
48 changes: 48 additions & 0 deletions tests/auxil/networking.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import asyncio
import contextlib
import threading
from contextlib import contextmanager
from pathlib import Path
from typing import Optional

import httpx
import pytest
from httpx import AsyncClient, AsyncHTTPTransport, Response

Expand Down Expand Up @@ -119,3 +124,46 @@ async def send_webhook_message(
return await client.request(
url=url, method=get_method or "POST", data=payload, headers=headers
)


_unblocked_request = httpx.AsyncClient.request


class RequestProtector:

def __init__(self):
self._requests_allowed: bool = True
self._lock = contextlib.nullcontext()

@property
def requests_allowed(self) -> bool:
return self._requests_allowed

def allow_requests(self):
with self._lock:
self._requests_allowed = True

def block_requests(self):
with self._lock:
self._requests_allowed = False

@contextlib.contextmanager
def allowing_requests(
self: "RequestProtector",
) -> contextlib.AbstractAsyncContextManager["RequestProtector"]:
with self._lock:
orig_status = self._requests_allowed
self._requests_allowed = True
yield self
self._requests_allowed = orig_status

def build_request_method(self):
async def request(*args, **kwargs):
if not self._requests_allowed:
raise RuntimeError("This function should not be called")
return await _unblocked_request(*args, **kwargs)

return request


REQUEST_PROTECTOR = RequestProtector()
61 changes: 52 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from typing import Dict, List
from uuid import uuid4

import httpx
import pytest

from telegram import (
Expand All @@ -44,7 +45,7 @@
from tests.auxil.constants import PRIVATE_KEY, TEST_TOPIC_ICON_COLOR, TEST_TOPIC_NAME
from tests.auxil.envvars import RUN_TEST_OFFICIAL, TEST_WITH_OPT_DEPS
from tests.auxil.files import data_file
from tests.auxil.networking import NonchalantHttpxRequest
from tests.auxil.networking import NonchalantHttpxRequest, REQUEST_PROTECTOR
from tests.auxil.pytest_classes import PytestApplication, PytestBot, make_bot
from tests.auxil.timezones import BasicTimezone

Expand All @@ -61,6 +62,22 @@
collect_ignore_glob = ["test_official/*.py"]


# Setup for the fixture pytest.mark.disable_httpx that allows to cut of internet connection
# for tests to ensure that they don't make any requests. This needs to come rather early
# to ensure that httpx.AsyncClient.request is patched before any tests import httpx.
httpx.AsyncClient.request = REQUEST_PROTECTOR.build_request_method()


def pytest_runtest_teardown() -> None:
REQUEST_PROTECTOR.allow_requests()


def pytest_runtest_setup(item) -> None:
# If the test has the `disable_httpx` marker, it's explicitly disabled.
if "disable_httpx" in item.fixturenames or item.get_closest_marker("disable_httpx"):
REQUEST_PROTECTOR.block_requests()


# This is here instead of in setup.cfg due to https://github.com/pytest-dev/pytest/issues/8343
def pytest_runtestloop(session: pytest.Session):
session.add_marker(
Expand All @@ -77,27 +94,44 @@ def no_rerun_after_xfail_or_flood(error, name, test: pytest.Function, plugin):
return not (xfail_present or did_we_flood)


_test_root = Path(__file__).parent.resolve().absolute()
_ext_test_root = _test_root / "ext"


def pytest_collection_modifyitems(items: List[pytest.Item]):
"""Here we add a flaky marker to all request making tests and a (no_)req marker to the rest."""
for item in items: # items are the test methods
parent = item.parent # Get the parent of the item (class, or module if defined outside)
if parent is None: # should never happen, but just in case
# Get the parent of the item (class, or module if defined outside)
parent = item.parent
if parent is None:
# should never happen, but just in case
return
if ( # Check if the class name ends with 'WithRequest' and if it has no flaky marker
if (
# Check if the class name ends with 'WithRequest' and if it has no flaky marker
parent.name.endswith("WithRequest")
and not parent.get_closest_marker( # get_closest_marker gets pytest.marks with `name`
name="flaky"
) # don't add/override any previously set markers
and not (
# get_closest_marker gets pytest.marks with `name`
parent.get_closest_marker(name="flaky")
)
# don't add/override any previously set markers
and not parent.get_closest_marker(name="req")
): # Add the flaky marker with a rerun filter to the class
):
# Add the flaky marker with a rerun filter to the class
parent.add_marker(pytest.mark.flaky(3, 1, rerun_filter=no_rerun_after_xfail_or_flood))
parent.add_marker(pytest.mark.req)
# Add the no_req marker to all classes that end with 'WithoutRequest' and don't have it
elif parent.name.endswith("WithoutRequest") and not parent.get_closest_marker(
name="no_req"
):
# Add the no_req marker to all classes that end with 'WithoutRequest' and don't have it
parent.add_marker(pytest.mark.no_req)

# We cut of the networking connection for all classes that end with 'WithoutRequest' and
# for all tests in /tests/ext to ensure that we find wrongly located test cases.
if (
parent.name.endswith("WithoutRequest") or (_ext_test_root in item.path.parents)
) and not parent.get_closest_marker(name="disable_httpx"):
parent.add_marker(pytest.mark.disable_httpx)


# Redefine the event_loop fixture to have a session scope. Otherwise `bot` fixture can't be
# session. See https://github.com/pytest-dev/pytest-asyncio/issues/68 for more details.
Expand All @@ -124,6 +158,15 @@ async def bot(bot_info):
yield _bot


@pytest.fixture(scope="session")
def permissive_bot(bot):
"""A bot that is always allowed to make requests. Useful for fixtures that need to make
requests but may be used in *WithoutReq test classes.
"""
with REQUEST_PROTECTOR.allowing_requests():
yield bot


@pytest.fixture()
def one_time_bot(bot_info):
"""A function scoped bot since the session bot would shutdown when `async with app` finishes"""
Expand Down
13 changes: 13 additions & 0 deletions tests/test_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2233,6 +2233,19 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs):
monkeypatch.setattr(bot.request, "post", make_assertion)
assert await bot.refund_star_payment(42, "37")

async def test_send_chat_action_all_args(self, bot, chat_id, monkeypatch):
async def make_assertion(*args, **_):
kwargs = args[1]
return (
kwargs["chat_id"] == chat_id
and kwargs["action"] == "action"
and kwargs["message_thread_id"] == 1
and kwargs["business_connection_id"] == 3
)

monkeypatch.setattr(bot, "_post", make_assertion)
assert await bot.send_chat_action(chat_id, "action", 1, 3)


class TestBotWithRequest:
"""
Expand Down
Loading