Skip to content
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

[5.4] Merge pull request #7144 from nicoddemus/async-testcase-7110 #7149

Merged
merged 1 commit into from May 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Expand Up @@ -70,7 +70,7 @@ jobs:
- name: "windows-py38"
python: "3.8"
os: windows-latest
tox_env: "py38-twisted"
tox_env: "py38-unittestextras"
use_coverage: true

- name: "ubuntu-py35"
Expand Down
1 change: 1 addition & 0 deletions changelog/7110.bugfix.rst
@@ -0,0 +1 @@
Fixed regression: ``asyncbase.TestCase`` tests are executed correctly again.
7 changes: 7 additions & 0 deletions src/_pytest/compat.py
Expand Up @@ -93,6 +93,13 @@ def syntax, and doesn't contain yield), or a function decorated with
return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False)


def is_async_function(func: object) -> bool:
"""Return True if the given function seems to be an async function or async generator"""
return iscoroutinefunction(func) or (
sys.version_info >= (3, 6) and inspect.isasyncgenfunction(func)
)


def getlocation(function, curdir=None) -> str:
function = get_real_func(function)
fn = py.path.local(inspect.getfile(function))
Expand Down
30 changes: 5 additions & 25 deletions src/_pytest/python.py
Expand Up @@ -34,8 +34,8 @@
from _pytest.compat import get_real_func
from _pytest.compat import getimfunc
from _pytest.compat import getlocation
from _pytest.compat import is_async_function
from _pytest.compat import is_generator
from _pytest.compat import iscoroutinefunction
from _pytest.compat import NOTSET
from _pytest.compat import REGEX_TYPE
from _pytest.compat import safe_getattr
Expand Down Expand Up @@ -159,7 +159,7 @@ def pytest_configure(config):
)


def async_warn(nodeid: str) -> None:
def async_warn_and_skip(nodeid: str) -> None:
msg = "async def functions are not natively supported and have been skipped.\n"
msg += (
"You need to install a suitable plugin for your async framework, for example:\n"
Expand All @@ -175,33 +175,13 @@ def async_warn(nodeid: str) -> None:
@hookimpl(trylast=True)
def pytest_pyfunc_call(pyfuncitem: "Function"):
testfunction = pyfuncitem.obj

try:
# ignoring type as the import is invalid in py37 and mypy thinks its a error
from unittest import IsolatedAsyncioTestCase # type: ignore
except ImportError:
async_ok_in_stdlib = False
else:
async_ok_in_stdlib = isinstance(
getattr(testfunction, "__self__", None), IsolatedAsyncioTestCase
)

if (
iscoroutinefunction(testfunction)
or (sys.version_info >= (3, 6) and inspect.isasyncgenfunction(testfunction))
) and not async_ok_in_stdlib:
async_warn(pyfuncitem.nodeid)
if is_async_function(testfunction):
async_warn_and_skip(pyfuncitem.nodeid)
funcargs = pyfuncitem.funcargs
testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
result = testfunction(**testargs)
if hasattr(result, "__await__") or hasattr(result, "__aiter__"):
if async_ok_in_stdlib:
# todo: investigate moving this to the unittest plugin
# by a test call result hook
testcase = testfunction.__self__
testcase._callMaybeAsync(lambda: result)
else:
async_warn(pyfuncitem.nodeid)
async_warn_and_skip(pyfuncitem.nodeid)
return True


Expand Down
19 changes: 12 additions & 7 deletions src/_pytest/unittest.py
Expand Up @@ -6,6 +6,7 @@
import _pytest._code
import pytest
from _pytest.compat import getimfunc
from _pytest.compat import is_async_function
from _pytest.config import hookimpl
from _pytest.outcomes import exit
from _pytest.outcomes import fail
Expand Down Expand Up @@ -227,13 +228,17 @@ def wrapped_testMethod(*args, **kwargs):
self._needs_explicit_tearDown = True
raise _GetOutOf_testPartExecutor(exc)

setattr(self._testcase, self._testcase._testMethodName, wrapped_testMethod)
try:
self._testcase(result=self)
except _GetOutOf_testPartExecutor as exc:
raise exc.args[0] from exc.args[0]
finally:
delattr(self._testcase, self._testcase._testMethodName)
# let the unittest framework handle async functions
if is_async_function(self.obj):
self._testcase(self)
else:
setattr(self._testcase, self._testcase._testMethodName, wrapped_testMethod)
try:
self._testcase(result=self)
except _GetOutOf_testPartExecutor as exc:
raise exc.args[0] from exc.args[0]
finally:
delattr(self._testcase, self._testcase._testMethodName)

def _prunetraceback(self, excinfo):
Function._prunetraceback(self, excinfo)
Expand Down
9 changes: 9 additions & 0 deletions testing/example_scripts/unittest/test_unittest_asyncio.py
@@ -1,7 +1,13 @@
from unittest import IsolatedAsyncioTestCase # type: ignore


teardowns = []


class AsyncArguments(IsolatedAsyncioTestCase):
async def asyncTearDown(self):
teardowns.append(None)

async def test_something_async(self):
async def addition(x, y):
return x + y
Expand All @@ -13,3 +19,6 @@ async def addition(x, y):
return x + y

self.assertEqual(await addition(2, 2), 3)

def test_teardowns(self):
assert len(teardowns) == 2
22 changes: 22 additions & 0 deletions testing/example_scripts/unittest/test_unittest_asynctest.py
@@ -0,0 +1,22 @@
"""Issue #7110"""
import asyncio

import asynctest


teardowns = []


class Test(asynctest.TestCase):
async def tearDown(self):
teardowns.append(None)

async def test_error(self):
await asyncio.sleep(0)
self.fail("failing on purpose")

async def test_ok(self):
await asyncio.sleep(0)

def test_teardowns(self):
assert len(teardowns) == 2
11 changes: 10 additions & 1 deletion testing/test_unittest.py
Expand Up @@ -1136,4 +1136,13 @@ def test_async_support(testdir):

testdir.copy_example("unittest/test_unittest_asyncio.py")
reprec = testdir.inline_run()
reprec.assertoutcome(failed=1, passed=1)
reprec.assertoutcome(failed=1, passed=2)


def test_asynctest_support(testdir):
"""Check asynctest support (#7110)"""
pytest.importorskip("asynctest")

testdir.copy_example("unittest/test_unittest_asynctest.py")
reprec = testdir.inline_run()
reprec.assertoutcome(failed=1, passed=2)
5 changes: 3 additions & 2 deletions tox.ini
Expand Up @@ -11,7 +11,7 @@ envlist =
py38
pypy
pypy3
py37-{pexpect,xdist,twisted,numpy,pluggymaster}
py37-{pexpect,xdist,unittestextras,numpy,pluggymaster}
doctesting
py37-freeze
docs
Expand Down Expand Up @@ -50,7 +50,8 @@ deps =
pexpect: pexpect
pluggymaster: git+https://github.com/pytest-dev/pluggy.git@master
pygments
twisted: twisted
unittestextras: twisted
unittestextras: asynctest
xdist: pytest-xdist>=1.13
{env:_PYTEST_TOX_EXTRA_DEP:}

Expand Down