From fd2f172258d120912b6cb5fb2b9e7eab714b6098 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 1 May 2020 12:56:06 -0300 Subject: [PATCH] Let unittest frameworks deal with async functions Instead of trying to handle unittest-async functions in pytest_pyfunc_call, let the unittest framework handle them instead. This lets us remove the hack in pytest_pyfunc_call, with the upside that we should support any unittest-async based framework. Also included 'asynctest' as test dependency for py37-twisted, and renamed 'twisted' to 'unittestextras' to better reflect that we install 'twisted' and 'asynctest' now. This also fixes the problem of cleanUp functions not being properly called for async functions. Fix #7110 Fix #6924 --- .github/workflows/main.yml | 2 +- changelog/7110.bugfix.rst | 1 + src/_pytest/compat.py | 7 +++++ src/_pytest/python.py | 30 ++++--------------- src/_pytest/unittest.py | 19 +++++++----- .../unittest/test_unittest_asyncio.py | 9 ++++++ .../unittest/test_unittest_asynctest.py | 22 ++++++++++++++ testing/test_unittest.py | 11 ++++++- tox.ini | 5 ++-- 9 files changed, 70 insertions(+), 36 deletions(-) create mode 100644 changelog/7110.bugfix.rst create mode 100644 testing/example_scripts/unittest/test_unittest_asynctest.py 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:}