diff --git a/HISTORY.rst b/HISTORY.rst index d70cee6c9..c854ef80b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,6 @@ *Bug tracker at https://github.com/giampaolo/psutil/issues* -6.0.1 (IN DEVELOPMENT) +6.1.0 (IN DEVELOPMENT) ====================== XXXX-XX-XX @@ -14,6 +14,10 @@ XXXX-XX-XX targets. They can be used to install dependencies meant for running tests and for local development. They can also be installed via ``pip install .[test]`` and ``pip install .[dev]``. +- 2456_: allow to run tests via ``python3 -m psutil.tests`` even if ``pytest`` + module is not installed. This is useful for production environments that + don't have pytest installed, but still want to be able to test psutil + installation. **Bug fixes** diff --git a/docs/DEVGUIDE.rst b/docs/DEVGUIDE.rst index 456714603..563c9b883 100644 --- a/docs/DEVGUIDE.rst +++ b/docs/DEVGUIDE.rst @@ -18,6 +18,14 @@ Once you have a compiler installed run: make install make test +- If you don't have the source code, and just want to test psutil installation. + This will work also if ``pytest`` module is not installed (e.g. production + environments) by using unittest's test runner: + +.. code-block:: bash + + python3 -m psutil.tests + - ``make`` (and the accompanying `Makefile`_) is the designated tool to build, install, run tests and do pretty much anything that involves development. This also includes Windows, meaning that you can run `make command` similarly diff --git a/psutil/__init__.py b/psutil/__init__.py index c1e038f3e..763a5f00e 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -214,7 +214,7 @@ AF_LINK = _psplatform.AF_LINK __author__ = "Giampaolo Rodola'" -__version__ = "6.0.1" +__version__ = "6.1.0" version_info = tuple([int(num) for num in __version__.split('.')]) _timer = getattr(time, 'monotonic', time.time) diff --git a/psutil/tests/README.rst b/psutil/tests/README.rst index 5c7336fb0..8e5b6e7d8 100644 --- a/psutil/tests/README.rst +++ b/psutil/tests/README.rst @@ -1,40 +1,35 @@ Instructions for running tests ============================== -Setup: +There are 2 ways of running tests. If you have the source code: .. code-block:: bash make install-pydeps-test # install pytest + make test -There are two ways of running tests. As a "user", if psutil is already -installed and you just want to test it works on your system: +If you don't have the source code, and just want to test psutil installation. +This will work also if ``pytest`` module is not installed (e.g. production +environments) by using unittest's test runner: .. code-block:: bash python -m psutil.tests -As a "developer", if you have a copy of the source code and you're working on -it: - -.. code-block:: bash - - make test - -You can run tests in parallel with: +To run tests in parallel (faster): .. code-block:: bash make test-parallel -You can run a specific test with: +Run a specific test: .. code-block:: bash make test ARGS=psutil.tests.test_system.TestDiskAPIs -You can run memory leak tests with: +Test C extension memory leaks: .. code-block:: bash - make test-parallel + make test-memleaks diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index e4667ab69..dd31392c0 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -36,7 +36,11 @@ from socket import AF_INET6 from socket import SOCK_STREAM -import pytest + +try: + import pytest +except ImportError: + pytest = None import psutil from psutil import AIX @@ -71,6 +75,8 @@ if PY3: import enum else: + import unittest2 as unittest + enum = None if POSIX: @@ -913,6 +919,76 @@ def get_testfn(suffix="", dir=None): # =================================================================== +class fake_pytest: + """A class that mimics some basic pytest APIs. This is meant for + when unit tests are run in production, where pytest may not be + installed. Still, the user can test psutil installation via: + + $ python3 -m psutil.tests + """ + + @staticmethod + def main(*args, **kw): # noqa ARG004 + """Mimics pytest.main(). It has the same effect as running + `python3 -m unittest -v` from the project root directory. + """ + suite = unittest.TestLoader().discover(HERE) + unittest.TextTestRunner(verbosity=2).run(suite) + warnings.warn( + "Fake pytest module was used. Test results may be inaccurate.", + UserWarning, + stacklevel=1, + ) + return suite + + @staticmethod + def raises(exc, match=None): + """Mimics `pytest.raises`.""" + + class ExceptionInfo: + _exc = None + + @property + def value(self): + return self._exc + + @contextlib.contextmanager + def context(exc, match=None): + einfo = ExceptionInfo() + try: + yield einfo + except exc as err: + if match and not re.search(match, str(err)): + msg = '"{}" does not match "{}"'.format(match, str(err)) + raise AssertionError(msg) + einfo._exc = err + else: + raise AssertionError("%r not raised" % exc) + + return context(exc, match=match) + + @staticmethod + def warns(warning, match=None): + """Mimics `pytest.warns`.""" + if match: + return unittest.TestCase().assertWarnsRegex(warning, match) + return unittest.TestCase().assertWarns(warning) + + class mark: + class xdist_group: + """Mimics `@pytest.mark.xdist_group` decorator (no-op).""" + + def __init__(self, name=None): + pass + + def __call__(self, cls_or_meth): + return cls_or_meth + + +if pytest is None: + pytest = fake_pytest + + class TestCase(unittest.TestCase): # ...otherwise multiprocessing.Pool complains if not PY3: diff --git a/psutil/tests/__main__.py b/psutil/tests/__main__.py index f43b751f9..ce6fc24c7 100644 --- a/psutil/tests/__main__.py +++ b/psutil/tests/__main__.py @@ -6,7 +6,7 @@ $ python -m psutil.tests. """ -import pytest +from psutil.tests import pytest pytest.main(["-v", "-s", "--tb=short"]) diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index 13c877460..3af505b60 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -16,8 +16,6 @@ import time import unittest -import pytest - import psutil from psutil import BSD from psutil import FREEBSD @@ -26,6 +24,7 @@ from psutil.tests import HAS_BATTERY from psutil.tests import TOLERANCE_SYS_MEM from psutil.tests import PsutilTestCase +from psutil.tests import pytest from psutil.tests import retry_on_failure from psutil.tests import sh from psutil.tests import spawn_testproc diff --git a/psutil/tests/test_connections.py b/psutil/tests/test_connections.py index 783bf8145..7f29900b2 100755 --- a/psutil/tests/test_connections.py +++ b/psutil/tests/test_connections.py @@ -16,8 +16,6 @@ from socket import SOCK_DGRAM from socket import SOCK_STREAM -import pytest - import psutil from psutil import FREEBSD from psutil import LINUX @@ -38,6 +36,7 @@ from psutil.tests import check_connection_ntuple from psutil.tests import create_sockets from psutil.tests import filter_proc_net_connections +from psutil.tests import pytest from psutil.tests import reap_children from psutil.tests import retry_on_failure from psutil.tests import skip_on_access_denied diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index aa51739b1..788fe9914 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -23,8 +23,6 @@ import unittest import warnings -import pytest - import psutil from psutil import LINUX from psutil._compat import PY3 @@ -46,6 +44,7 @@ from psutil.tests import ThreadTask from psutil.tests import call_until from psutil.tests import mock +from psutil.tests import pytest from psutil.tests import reload_module from psutil.tests import retry_on_failure from psutil.tests import safe_rmpath diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py index adafea947..8ddaec28a 100755 --- a/psutil/tests/test_misc.py +++ b/psutil/tests/test_misc.py @@ -18,8 +18,6 @@ import sys import unittest -import pytest - import psutil import psutil.tests from psutil import POSIX @@ -50,6 +48,7 @@ from psutil.tests import PsutilTestCase from psutil.tests import mock from psutil.tests import process_namespace +from psutil.tests import pytest from psutil.tests import reload_module from psutil.tests import sh from psutil.tests import system_namespace @@ -634,26 +633,29 @@ def test_debug(self): else: from StringIO import StringIO - with redirect_stderr(StringIO()) as f: - debug("hello") - sys.stderr.flush() + with mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): + with redirect_stderr(StringIO()) as f: + debug("hello") + sys.stderr.flush() msg = f.getvalue() assert msg.startswith("psutil-debug"), msg assert "hello" in msg assert __file__.replace('.pyc', '.py') in msg # supposed to use repr(exc) - with redirect_stderr(StringIO()) as f: - debug(ValueError("this is an error")) + with mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): + with redirect_stderr(StringIO()) as f: + debug(ValueError("this is an error")) msg = f.getvalue() assert "ignoring ValueError" in msg assert "'this is an error'" in msg # supposed to use str(exc), because of extra info about file name - with redirect_stderr(StringIO()) as f: - exc = OSError(2, "no such file") - exc.filename = "/foo" - debug(exc) + with mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): + with redirect_stderr(StringIO()) as f: + exc = OSError(2, "no such file") + exc.filename = "/foo" + debug(exc) msg = f.getvalue() assert "no such file" in msg assert "/foo" in msg diff --git a/psutil/tests/test_posix.py b/psutil/tests/test_posix.py index dd2d179dd..5c2c86c9e 100755 --- a/psutil/tests/test_posix.py +++ b/psutil/tests/test_posix.py @@ -15,8 +15,6 @@ import time import unittest -import pytest - import psutil from psutil import AIX from psutil import BSD @@ -31,6 +29,7 @@ from psutil.tests import QEMU_USER from psutil.tests import PsutilTestCase from psutil.tests import mock +from psutil.tests import pytest from psutil.tests import retry_on_failure from psutil.tests import sh from psutil.tests import skip_on_access_denied diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index a339ac2d3..b3e6569ee 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -21,8 +21,6 @@ import types import unittest -import pytest - import psutil from psutil import AIX from psutil import BSD @@ -65,6 +63,7 @@ from psutil.tests import create_py_exe from psutil.tests import mock from psutil.tests import process_namespace +from psutil.tests import pytest from psutil.tests import reap_children from psutil.tests import retry_on_failure from psutil.tests import sh @@ -1452,8 +1451,9 @@ def test_reused_pid(self): # make sure is_running() removed PID from process_iter() # internal cache - with redirect_stderr(StringIO()) as f: - list(psutil.process_iter()) + with mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): + with redirect_stderr(StringIO()) as f: + list(psutil.process_iter()) assert ( "refreshing Process instance for reused PID %s" % p.pid in f.getvalue() diff --git a/psutil/tests/test_process_all.py b/psutil/tests/test_process_all.py index a6025390e..1550046ba 100755 --- a/psutil/tests/test_process_all.py +++ b/psutil/tests/test_process_all.py @@ -16,8 +16,6 @@ import time import traceback -import pytest - import psutil from psutil import AIX from psutil import BSD @@ -43,6 +41,7 @@ from psutil.tests import is_namedtuple from psutil.tests import is_win_secure_system_proc from psutil.tests import process_namespace +from psutil.tests import pytest # Cuts the time in half, but (e.g.) on macOS the process pool stays diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 7403326bf..298d2d9e4 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -19,8 +19,6 @@ import time import unittest -import pytest - import psutil from psutil import AIX from psutil import BSD @@ -56,6 +54,7 @@ from psutil.tests import check_net_address from psutil.tests import enum from psutil.tests import mock +from psutil.tests import pytest from psutil.tests import retry_on_failure diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index d74df153a..7293aa010 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -14,9 +14,9 @@ import socket import stat import subprocess +import textwrap import unittest - -import pytest +import warnings import psutil import psutil.tests @@ -29,6 +29,7 @@ from psutil.tests import CI_TESTING from psutil.tests import COVERAGE from psutil.tests import HAS_NET_CONNECTIONS_UNIX +from psutil.tests import HERE from psutil.tests import PYTHON_EXE from psutil.tests import PYTHON_EXE_ENV from psutil.tests import PsutilTestCase @@ -38,11 +39,13 @@ from psutil.tests import call_until from psutil.tests import chdir from psutil.tests import create_sockets +from psutil.tests import fake_pytest from psutil.tests import filter_proc_net_connections from psutil.tests import get_free_port from psutil.tests import is_namedtuple from psutil.tests import mock from psutil.tests import process_namespace +from psutil.tests import pytest from psutil.tests import reap_children from psutil.tests import retry from psutil.tests import retry_on_failure @@ -167,6 +170,7 @@ def test_wait_for_file_no_delete(self): def test_call_until(self): call_until(lambda: 1) + # TODO: test for timeout class TestFSTestUtils(PsutilTestCase): @@ -444,6 +448,85 @@ def fun_2(): self.execute_w_exc(ZeroDivisionError, fun_2) +class TestFakePytest(PsutilTestCase): + def test_raises(self): + with fake_pytest.raises(ZeroDivisionError) as cm: + 1 / 0 # noqa + assert isinstance(cm.value, ZeroDivisionError) + + with fake_pytest.raises(ValueError, match="foo") as cm: + raise ValueError("foo") + + try: + with fake_pytest.raises(ValueError, match="foo") as cm: + raise ValueError("bar") + except AssertionError as err: + assert str(err) == '"foo" does not match "bar"' + else: + raise self.fail("exception not raised") + + def test_mark(self): + @fake_pytest.mark.xdist_group(name="serial") + def foo(): + return 1 + + assert foo() == 1 + + @fake_pytest.mark.xdist_group(name="serial") + class Foo: + def bar(self): + return 1 + + assert Foo().bar() == 1 + + def test_main(self): + tmpdir = self.get_testfn(dir=HERE) + os.mkdir(tmpdir) + with open(os.path.join(tmpdir, "__init__.py"), "w"): + pass + with open(os.path.join(tmpdir, "test_file.py"), "w") as f: + f.write(textwrap.dedent("""\ + import unittest + + class TestCase(unittest.TestCase): + def test_passed(self): + pass + """).lstrip()) + with mock.patch.object(psutil.tests, "HERE", tmpdir): + with self.assertWarnsRegex( + UserWarning, "Fake pytest module was used" + ): + suite = fake_pytest.main() + assert suite.countTestCases() == 1 + + def test_warns(self): + # success + with fake_pytest.warns(UserWarning): + warnings.warn("foo", UserWarning, stacklevel=1) + + # failure + try: + with fake_pytest.warns(UserWarning): + warnings.warn("foo", DeprecationWarning, stacklevel=1) + except AssertionError: + pass + else: + raise self.fail("exception not raised") + + # match success + with fake_pytest.warns(UserWarning, match="foo"): + warnings.warn("foo", UserWarning, stacklevel=1) + + # match failure + try: + with fake_pytest.warns(UserWarning, match="foo"): + warnings.warn("bar", UserWarning, stacklevel=1) + except AssertionError: + pass + else: + raise self.fail("exception not raised") + + class TestTestingUtils(PsutilTestCase): def test_process_namespace(self): p = psutil.Process() diff --git a/psutil/tests/test_unicode.py b/psutil/tests/test_unicode.py index 9177df5d7..cc7a5475b 100755 --- a/psutil/tests/test_unicode.py +++ b/psutil/tests/test_unicode.py @@ -79,8 +79,6 @@ import warnings from contextlib import closing -import pytest - import psutil from psutil import BSD from psutil import POSIX @@ -103,6 +101,7 @@ from psutil.tests import copyload_shared_lib from psutil.tests import create_py_exe from psutil.tests import get_testfn +from psutil.tests import pytest from psutil.tests import safe_mkdir from psutil.tests import safe_rmpath from psutil.tests import skip_on_access_denied diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index 0a2683dd2..7b5ba7ba1 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -20,8 +20,6 @@ import unittest import warnings -import pytest - import psutil from psutil import WINDOWS from psutil._compat import FileNotFoundError @@ -37,6 +35,7 @@ from psutil.tests import TOLERANCE_SYS_MEM from psutil.tests import PsutilTestCase from psutil.tests import mock +from psutil.tests import pytest from psutil.tests import retry_on_failure from psutil.tests import sh from psutil.tests import spawn_testproc diff --git a/setup.py b/setup.py index fb36da0bc..949716a5b 100755 --- a/setup.py +++ b/setup.py @@ -80,6 +80,7 @@ "pytest-xdist", "pytest==4.6.11", "setuptools", + "unittest2", ] if WINDOWS and not PYPY: TEST_DEPS.append("pywin32")