eukaryote / pytest-tornasync

A pytest plugin for testing Tornado (version 5.0 or newer) apps using plain (undecoratored) native coroutine tests.
MIT License
25 stars 4 forks source link

Compatibility issue with 3.6.2 #1

Closed jsoucheiron closed 6 years ago

jsoucheiron commented 7 years ago

Hi, Recently we've found out an issue in our tests. It works with 3.6.1, but it breaks using 3.6.2. We've tried to distill a minimal example to reproduce the issue. Any ideas on how to fix it?

import asyncio
import functools

import pytest
from tornado.web import RequestHandler

class TimeoutClient:
    def __init__(self, http_client):
        self.http_client = http_client

    async def request(self, *args, **kwargs):
        async with self.http_client.request(*args, **kwargs) as response:
            await response.text()
            return response

@pytest.fixture(scope='function')
def timeoutclient(aiohttp_client):
    return TimeoutClient(http_client=aiohttp_client)

def asyncio_compat(func):
    """
    Compatibility decorator for using asyncio-based libraries inside Tornado.

    More info: https://github.com/aio-libs/aiohttp/issues/1180#issuecomment-247701597
    """

    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        return await asyncio.get_event_loop().create_task(func(*args, **kwargs))

    return wrapper

class TestBaseClient:
    @asyncio_compat
    async def test_timeout_response(self, timeoutclient, app, test_base_url):
        class TimeoutHandler(RequestHandler):
            @asyncio_compat
            async def get(self):
                await asyncio.sleep(10)

        app.add_handlers(r".*", [
            ("/timeout", TimeoutHandler),
        ])

        with pytest.raises(asyncio.TimeoutError):
            await timeoutclient.request('GET', f'{test_base_url}/timeout', timeout=0.1)
eukaryote commented 7 years ago

Sorry for the delay. I just noticed this issue.

Thanks for the test case. Could you also include the aiohttp_client fixture that is referenced from timeoutclient?

jsoucheiron commented 7 years ago

It's this:


from aiohttp.abc import AbstractCookieJar

class DisabledCookieJar(AbstractCookieJar):
    """
    CookieJar implementation of aiohttp.CookieJar that disables all cookies
    between requests.

    This allows to reuse connections with the same aiohttp.ClientSession but
    without sharing any state.
    """

    def clear(self):
        pass

    def update_cookies(self, cookies, response_url=None):
        pass

    def filter_cookies(self, request_url):
        pass

    def __iter__(self):
        yield from ()

    def __len__(self):
        return 0

@pytest.fixture(scope='function')
def aiohttp_client(io_loop):
    """
    Returns an aiohttp ClientSession configured with the current event_loop.
    """
    with aiohttp.ClientSession(loop=asyncio.get_event_loop(), cookie_jar=DisabledCookieJar()) as aiohttp_client:
        yield aiohttp_client

@pytest.fixture
def io_loop():
    """
    Configures pytest-tornasync to use the AsyncIOMainLoop from asyncio.
    """
    io_loop = _loop_create(tornado.platform.asyncio.AsyncIOMainLoop)
    yield io_loop
    _loop_destroy(io_loop)
eukaryote commented 7 years ago

I'm not sure what the error is that you're seeing, but I'm not able to reproduce it. Here's what I tried (I'm on Linux [Ubuntu 16 LTS]):

# create two venvs using python 3.6.1 and 3.6.2:
/opt/python/3.6.1/bin/python3 -m venv /v/ptasync361
/opt/python/3.6.2/bin/python3 -m venv /v/ptasync362

# install deps to each:
/v/ptasync361/bin/pip install -U pip aiohttp pytest tornado pytest-tornasync
/v/ptasync362/bin/pip install -U pip aiohttp pytest tornado pytest-tornasync

# run tests under python 3.6.1 and 3.6.2:
~/repos/ptasync » source /v/ptasync361/bin/activate && /v/ptasync361/bin/pytest test_issue1.py && deactivate
================================ test session starts =================================
platform linux -- Python 3.6.1, pytest-3.2.1, py-1.4.34, pluggy-0.4.0
rootdir: ~/repos/ptasync, inifile:
plugins: tornasync-0.4.0
collected 1 item

test_issue1.py .

============================== 1 passed in 0.33 seconds ==============================
~/repos/ptasync » source /v/ptasync362/bin/activate && /v/ptasync362/bin/pytest test_issue1.py && deactivate
================================ test session starts =================================
platform linux -- Python 3.6.2, pytest-3.2.1, py-1.4.34, pluggy-0.4.0
rootdir: ~/repos/ptasync, inifile:
plugins: tornasync-0.4.0
collected 1 item

test_issue1.py .

============================== 1 passed in 0.55 seconds ==============================
~/repos/ptasync »

The test case passes in both cases. Here's test_issue1.py for completeness:

import asyncio
import functools

import pytest

import tornado
from tornado.web import RequestHandler, Application

import aiohttp
from aiohttp.abc import AbstractCookieJar

class MainHandler(RequestHandler):
    def get(self):
        self.write("Hello, world!")

@pytest.fixture
def app():
    return tornado.web.Application([(r"/", MainHandler)])

class TimeoutClient:

    def __init__(self, http_client):
        self.http_client = http_client

    async def request(self, *args, **kwargs):
        async with self.http_client.request(*args, **kwargs) as response:
            await response.text()
            return response

class DisabledCookieJar(AbstractCookieJar):
    """
    CookieJar implementation of aiohttp.CookieJar that disables all cookies
    between requests.

    This allows to reuse connections with the same aiohttp.ClientSession but
    without sharing any state.
    """

    def clear(self):
        pass

    def update_cookies(self, cookies, response_url=None):
        pass

    def filter_cookies(self, request_url):
        pass

    def __iter__(self):
        yield from ()

    def __len__(self):
        return 0

@pytest.fixture(scope='function')
def aiohttp_client(io_loop):
    """
    Returns an aiohttp ClientSession configured with the current event_loop.
    """
    with aiohttp.ClientSession(
            loop=asyncio.get_event_loop(),
            cookie_jar=DisabledCookieJar()) as aiohttp_client:
        yield aiohttp_client

@pytest.fixture
def io_loop():
    """
    Configures pytest-tornasync to use the AsyncIOMainLoop from asyncio.
    """
    io_loop = tornado.platform.asyncio.AsyncIOMainLoop()
    io_loop.make_current()
    yield io_loop
    io_loop.clear_current()
    if (not type(io_loop).initialized() or
            io_loop is not type(io_loop).instance()):
        io_loop.close(all_fds=True)

@pytest.fixture(scope='function')
def timeoutclient(aiohttp_client):
    return TimeoutClient(http_client=aiohttp_client)

def asyncio_compat(func):
    """
    Compatibility decorator for using asyncio-based libraries inside Tornado.

    More info: https://github.com/aio-libs/aiohttp/issues/1180#issuecomment-247701597
    """

    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        return await asyncio.get_event_loop().create_task(func(*args, **kwargs))

    return wrapper

@pytest.fixture
def test_base_url(http_server_port):
    return 'http://127.0.0.1:{}'.format(http_server_port[1])

class TestBaseClient:

    @asyncio_compat
    async def test_timeout_response(self, timeoutclient, app, test_base_url):
        class TimeoutHandler(RequestHandler):
            @asyncio_compat
            async def get(self):
                await asyncio.sleep(10)

        app.add_handlers(r".*", [
            ("/timeout", TimeoutHandler),
        ])

        with pytest.raises(asyncio.TimeoutError):
            await timeoutclient.request('GET', f'{test_base_url}/timeout',
                                        timeout=0.1)

These are the exact versions of all deps:

~/repos/ptasync » /v/ptasync361/bin/python -V
Python 3.6.1
~/repos/ptasync » /v/ptasync361/bin/pytest --version
This is pytest version 3.2.1, imported from /v/ptasync361/lib/python3.6/site-packages/pytest.py
setuptools registered plugins:
  pytest-tornasync-0.4.0 at /v/ptasync361/lib/python3.6/site-packages/pytest_tornasync/plugin.py
~/repos/ptasync » /v/ptasync361/bin/pip freeze
aiohttp==2.2.5
async-timeout==1.3.0
chardet==3.0.4
multidict==3.1.3
py==1.4.34
pytest==3.2.1
pytest-tornasync==0.4.0
tornado==4.5.2
yarl==0.12.0

~/repos/ptasync » /v/ptasync362/bin/python -V
Python 3.6.2
~/repos/ptasync » /v/ptasync362/bin/pytest --version
This is pytest version 3.2.1, imported from /v/ptasync362/lib/python3.6/site-packages/pytest.py
setuptools registered plugins:
  pytest-tornasync-0.4.0 at /v/ptasync362/lib/python3.6/site-packages/pytest_tornasync/plugin.py
~/repos/ptasync » /v/ptasync362/bin/pip freeze
aiohttp==2.2.5
async-timeout==1.3.0
chardet==3.0.4
multidict==3.1.3
py==1.4.34
pytest==3.2.1
pytest-tornasync==0.4.0
tornado==4.5.2
yarl==0.12.0
eukaryote commented 6 years ago

Closing for inactivity. Please comment with additional information about what's different between our setups if my test case above doesn't work for you in 3.6.2.