diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 80317f1c1..722835583 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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" diff --git a/changelog/7110.bugfix.rst b/changelog/7110.bugfix.rst new file mode 100644 index 000000000..935f6ea3c --- /dev/null +++ b/changelog/7110.bugfix.rst @@ -0,0 +1 @@ +Fixed regression: ``asyncbase.TestCase`` tests are executed correctly again. diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 8aff8d57d..cf051182f 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -93,6 +93,13 @@ def iscoroutinefunction(func: object) -> bool: 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)) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 2b9bf4f5b..e1bd62f0b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -34,8 +34,8 @@ from _pytest.compat import get_default_arg_names 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 @@ -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" @@ -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 diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 2047876e5..e461248b7 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -6,6 +6,7 @@ import traceback 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 @@ -227,13 +228,17 @@ class TestCaseFunction(Function): 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) diff --git a/testing/example_scripts/unittest/test_unittest_asyncio.py b/testing/example_scripts/unittest/test_unittest_asyncio.py index 16eec1026..76eebf74a 100644 --- a/testing/example_scripts/unittest/test_unittest_asyncio.py +++ b/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 @@ -13,3 +19,6 @@ class AsyncArguments(IsolatedAsyncioTestCase): return x + y self.assertEqual(await addition(2, 2), 3) + + def test_teardowns(self): + assert len(teardowns) == 2 diff --git a/testing/example_scripts/unittest/test_unittest_asynctest.py b/testing/example_scripts/unittest/test_unittest_asynctest.py new file mode 100644 index 000000000..bddbe250a --- /dev/null +++ b/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 diff --git a/testing/test_unittest.py b/testing/test_unittest.py index de51f7bd1..a026dc3f6 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -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) diff --git a/tox.ini b/tox.ini index 3a280abb7..8f23e3cf9 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ envlist = py37 py38 pypy3 - py37-{pexpect,xdist,twisted,numpy,pluggymaster} + py37-{pexpect,xdist,unittestextras,numpy,pluggymaster} doctesting py37-freeze docs @@ -49,7 +49,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:}