======================== Tests in Python packages ======================== .. highlight:: bash Why is running tests important? =============================== Since Python performs only minimal build-time (or more precisely, import-time) checking of correctness, it is important to run tests of Python packages in order to catch any problems early. This is especially important for permitting others to verify support for new Python implementations. .. index:: distutils_enable_tests Using distutils_enable_tests ============================ Basic use case -------------- The simplest way of enabling tests is to call ``distutils_enable_tests`` in global scope, passing the test runner name as the first argument. This function takes care of declaring test phase, setting appropriate dependencies and ``test`` USE flag if necessary. If called after setting ``RDEPEND``, it also copies it to test dependencies. .. code-block:: bash :emphasize-lines: 22 # Copyright 1999-2020 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 EAPI=7 PYTHON_COMPAT=( python2_7 python3_{6,7,8} pypy3 ) inherit distutils-r1 DESCRIPTION="An easy whitelist-based HTML-sanitizing tool" HOMEPAGE="https://github.com/mozilla/bleach https://pypi.org/project/bleach/" SRC_URI="mirror://pypi/${PN:0:1}/${PN}/${P}.tar.gz" LICENSE="Apache-2.0" SLOT="0" KEYWORDS="~alpha ~amd64 ~arm ~arm64 ~hppa ~ia64 ~mips ~ppc ~ppc64 ~s390 ~sparc ~x86" RDEPEND=" dev-python/six[${PYTHON_USEDEP}] dev-python/webencodings[${PYTHON_USEDEP}] " distutils_enable_tests pytest The valid values include: - ``pytest`` for ``dev-python/pytest`` - ``setup.py`` to call ``setup.py test`` (*deprecated*) - ``unittest`` to use built-in unittest discovery See `choosing the correct test runner`_ for more information. Adding more test dependencies ----------------------------- Additional test dependencies can be specified in ``test?`` conditional. The flag normally does not need to be explicitly declared, as ``distutils_enable_tests`` does that in the majority of cases. Please read the section on `undesirable test dependencies`_ too. .. code-block:: bash :emphasize-lines: 18,21 # Copyright 1999-2020 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 EAPI=6 PYTHON_COMPAT=( python2_7 python3_{6,7,8} pypy3 ) inherit distutils-r1 DESCRIPTION="Universal encoding detector" HOMEPAGE="https://github.com/chardet/chardet https://pypi.org/project/chardet/" SRC_URI="https://github.com/chardet/chardet/archive/${PV}.tar.gz -> ${P}.tar.gz" LICENSE="LGPL-2.1" SLOT="0" KEYWORDS="~alpha amd64 arm arm64 hppa ia64 ~m68k ~mips ppc ppc64 s390 ~sh sparc x86 ~x64-cygwin ~amd64-linux ~x86-linux ~x64-macos ~x86-macos ~x64-solaris" DEPEND=" test? ( dev-python/hypothesis[${PYTHON_USEDEP}] ) " distutils_enable_tests pytest Note that ``distutils_enable_tests`` modifies variables directly in the ebuild environment. This means that if you wish to change their values, you need to append to them, i.e. the bottom part of that ebuild can be rewritten as: .. code-block:: bash :emphasize-lines: 3 distutils_enable_tests pytest DEPEND+=" test? ( dev-python/hypothesis[${PYTHON_USEDEP}] ) " Installing the package before running tests ------------------------------------------- In PEP 517 mode, the eclass automatically exposes a venv-style install tree to the test phase. No explicit action in necessary. In the legacy mode, ``distutils_enable_tests`` has an optional ``--install`` option that can be used to force performing an install to a temporary directory. More information can be found in the legacy section. Customizing the test phase -------------------------- If additional pre-/post-test phase actions need to be performed, they can be easily injected via overriding ``src_test()`` and making it call ``distutils-r1_src_test``: .. code-block:: bash :emphasize-lines: 30-34 # Copyright 1999-2020 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 EAPI=7 PYTHON_COMPAT=( python3_{6,7,8} ) inherit distutils-r1 DESCRIPTION="Extra features for standard library's cmd module" HOMEPAGE="https://github.com/python-cmd2/cmd2" SRC_URI="mirror://pypi/${PN:0:1}/${PN}/${P}.tar.gz" LICENSE="MIT" SLOT="0" KEYWORDS="~amd64 ~arm ~arm64 ~ppc64 ~x86 ~amd64-linux ~x86-linux" RDEPEND=" dev-python/attrs[${PYTHON_USEDEP}] >=dev-python/colorama-0.3.7[${PYTHON_USEDEP}] >=dev-python/pyperclip-1.6[${PYTHON_USEDEP}] dev-python/six[${PYTHON_USEDEP}] dev-python/wcwidth[${PYTHON_USEDEP}] " BDEPEND=" dev-python/setuptools_scm[${PYTHON_USEDEP}] " distutils_enable_tests pytest src_test() { # tests rely on very specific text wrapping... local -x COLUMNS=80 distutils-r1_src_test } If the actual test command needs to be customized, or a non-standard test tool needs to be used, you can define a ``python_test()`` sub-phase function. This function is called for every enabled Python target by the default ``src_test`` implementation. This can either be combined with ``distutils_enable_tests`` call, or used instead of it. In fact, the former function simply defines a ``python_test()`` function as part of its logic. .. code-block:: bash :emphasize-lines: 16,17,26-31,33-35 # Copyright 1999-2020 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 EAPI=7 PYTHON_COMPAT=( python{2_7,3_6,3_7,3_8} pypy3 ) inherit distutils-r1 DESCRIPTION="Bash tab completion for argparse" HOMEPAGE="https://pypi.org/project/argcomplete/" SRC_URI="mirror://pypi/${PN:0:1}/${PN}/${P}.tar.gz" LICENSE="Apache-2.0" SLOT="0" KEYWORDS="~amd64 ~arm ~arm64 ~hppa ~x86 ~amd64-linux ~x86-linux ~x64-macos" IUSE="test" RESTRICT="!test? ( test )" RDEPEND=" $(python_gen_cond_dep ' :1014: in _gcd_import ??? :991: in _find_and_load ??? :975: in _find_and_load_unlocked ??? :671: in _load_unlocked ??? /usr/lib/python3.8/site-packages/_pytest/assertion/rewrite.py:170: in exec_module exec(co, module.__dict__) systemd/test/test_login.py:6: in from systemd import login E ImportError: cannot import name 'login' from 'systemd' (/tmp/portage/dev-python/python-systemd-234-r2/work/python-systemd-234/systemd/__init__.py) .. Note:: Historically, the primary recommendation was to change directory prior to running the test suite. However, this was change since the ``rm`` approach is easier in PEP 517 mode, and less likely to trigger other issues (e.g. pytest missing its configuration, tests relying on relative paths). The modern preference is to remove the package directory prior to running the test suite. In PEP 517 mode, the build system is only run as part of ``src_compile``, and therefore the original sources are not needed afterwards. For example, when the tests are in a separate directory:: python_test() { rm -rf frozendict || die epytest } When the tests are installed as a part of the installed package, ``--pyargs`` option can be used to find them via package lookup:: python_test() { rm -rf numpy || die epytest --pyargs numpy } The other possible solution is to change the working directory prior to running the test suite, either into an arbitrary directory that does not feature the collision, or into the install directory. The latter could possible be necessary if the tests are installed as part of the package, and assume paths relative to the source directory:: python_test() { cd "${BUILD_DIR}/install$(python_get_sitedir)" || die epytest } However, please note that changing the working directory leads to pytest missing its configuration (``pytest.ini``, ``setup.cfg``, ``pyproject.toml``) which in turn could lead to warnings about missing marks or misleading test suite problems. Checklist for dealing with test failures ======================================== If you see some test failures but do not have a guess as to why they would be happening, try the following for a start: 1. Check upstream CI (if any). That's a quick way of verifying that there is no known breakage at the relevant tag. 2. Try running tests as your regular user, the way upstream suggests (e.g. via ``tox``). Try using a git checkout at the specific tag. This is the basic way of determining whether the package is actually broken or if it is something on our end. 3. If the tests fail at the specified tag, try upstream master branch. Maybe there was a fix committed. If it seems that the issue is on our end, try the following and see if it causes the subset of failing tests to change: 1. Make sure that the test runner is started via ``${EPYTHON}`` (the eclass-provided ``epytest`` and ``eunittest`` wrappers do that). Calling system executables directly (either Python via absolute path or system-installed tools that use absolute path in their shebangs) may cause just-built modules not to be imported correctly. 2. Try running the test suite from another directory. If you're seeing failures to load compiled extensions, Python may be wrongly importing modules from the current directory instead of the build/install tree. Some test suite also depend on paths relative to where upstream run tests. 3. Either switch to PEP 517 mode (preferred), or add ``distutils_install_for_testing`` to the test sub-phase or ``--install`` to the ``distutils_enable_tests`` call. This resolves the majority of problems that arise from the test suite requiring the package to be installed prior to testing. 4. Actually install the package to the system (with tests disabled). This can confirm cases of package for whom the above function does not work. In the worst case, you can set a test self-dependency to force users to install the package before testing:: test? ( ~dev-python/myself-${PV} ) 5. Try testing a different Python implementation. If a subset of tests is failing with Python 3.6, see if it still happens with 3.7 or 3.8. If 3.8 is passing but 3.9 is not, it's most likely some incompatibility upstream did not account for. 6. Run tests with ``FEATURES=-network-sandbox``. Sometimes lack of Internet access causes non-obvious failures. 7. Try a different test runner. Sometimes the subtle differences in how tests are executed can lead to test failures. But beware: some test runners may not run the full set of tests, so verify that you have actually fixed them and not just caused them to be skipped. Skipping problematic tests ========================== While generally it is preferable to fix tests, sometimes you will face failures that cannot be easily resolved. This especially applies to tests that are broken themselves rather than indicating real problems with the software. However, in some cases you will even find yourself ignoring minor test failures. .. Note:: When possible, it is preferable to use pytest along with its convenient ignore/deselect options to skip problematic tests. Using pytest instead of unittest is usually possible. Tests that are known to fail reliably can be marked as *expected failures*. This has the advantage that the test in question will continue being run and the test suite will report when it unexpectedly starts passing again. Expected failures are not supported by the standard Python unittest module. It is supported e.g. by pytest. :: sed -i -e \ "/def test_start_params_bug():/i@pytest.mark.xfail(reason='Known to fail on Gentoo')" \ statsmodels/tsa/tests/test_arima.py || die Tests that cause inconsistent results, trigger errors, consume horrendous amounts of disk space or cause another kind of undesirable mayhem can be *skipped* instead. Skipping means that they will not be run at all. There are multiple ways to skip a test. You can patch it to use a skip decorator, possibly with a condition:: # broken on py2.7, upstream knows sed -i -e '5a\ import sys' \ -e '/test_null_bytes/i\ @pytest.mark.skipif(sys.hexversion < 0x03000000, reason="broken on py2")' \ test/server.py || die The easy way to skip a test unconditioanlly is to prefix its name with an underscore:: # tests requiring specific locales sed -i -e 's:test_babel_with_language_:_&:' \ tests/test_build_latex.py || die sed -i -e 's:test_polyglossia_with_language_:_&:' \ tests/test_build_latex.py || die Finally, if all tests in a particular file are problematic, you can simply remove that file. If all tests belonging to the package are broken, you can use ``RESTRICT=test`` to disable testing altogether. Tests requiring Internet access =============================== One of more common causes of test failures are attempts to use Internet. With Portage blocking network access by default, packages performing tests against remote servers often fail. Ideally, packages would use mocking or replay tests rather than using real Internet services. Devmanual provides a detailed explanation `why tests must not use Internet`_. Some packages provide explicit methods of disabling network-based tests. For example, ``dev-python/tox`` provides a switch for that:: python_test() { distutils_install_for_testing epytest --no-network } There are packages that skip tests if they fail specifically due to connection errors, or detect whether Internet is accessible. Ideally, you should modify those packages to disable network tests unconditionally. For example, ``dev-python/pygit2`` ebuild does this:: # unconditionally prevent it from using network sed -i -e '/def no_network/a \ return True' test/utils.py || die In other cases, you will have to explicitly disable these tests. In some cases, it will be reasonable to remove whole test files or even restrict tests entirely. If the package's test suite relies on Internet access entirely and there is no point in running even a subset of tests, please implement running tests and combine test restriction with ``PROPERTIES=test_network`` to allow interested users to run tests when possible:: # users can use ALLOW_TEST=network to override this PROPERTIES="test_network" RESTRICT="test" distutils_enable_tests pytest Tests aborting (due to assertions) ================================== There are cases of package's tests terminating with an unclear error message and backtrace similar to the following:: ============================= test session starts ============================== platform linux -- Python 3.7.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 -- /usr/bin/python3.7 cachedir: .pytest_cache rootdir: /var/tmp/portage/dev-python/sabyenc-4.0.2/work/sabyenc-4.0.2, configfile: pytest.ini collecting ... collected 24 items [...] tests/test_decoder.py::test_crc_pickles PASSED [ 54%] tests/test_decoder.py::test_empty_size_pickles Fatal Python error: Aborted Current thread 0x00007f748bc47740 (most recent call first): File "/var/tmp/portage/dev-python/sabyenc-4.0.2/work/sabyenc-4.0.2/tests/testsupport.py", line 74 in sabyenc3_wrapper File "/var/tmp/portage/dev-python/sabyenc-4.0.2/work/sabyenc-4.0.2/tests/test_decoder.py", line 119 in test_empty_size_pickles File "/usr/lib/python3.7/site-packages/_pytest/python.py", line 180 in pytest_pyfunc_call File "/usr/lib/python3.7/site-packages/pluggy/callers.py", line 187 in _multicall [...] File "/usr/lib/python-exec/python3.7/pytest", line 11 in /var/tmp/portage/dev-python/sabyenc-4.0.2/temp/environment: line 2934: 66 Aborted (core dumped) pytest -vv This usually indicates that the C code of some Python extension failed an assertion. Since pytest does not print captured output when exiting due to a signal, you need to disable output capture (using ``-s``) to get a more useful error, e.g.:: $ python3.7 -m pytest -s =============================================================== test session starts =============================================================== platform linux -- Python 3.7.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 rootdir: /tmp/sabyenc, configfile: pytest.ini plugins: asyncio-0.14.0, forked-1.3.0, xdist-1.34.0, hypothesis-5.23.9, mock-3.2.0, flaky-3.7.0, timeout-1.4.2, freezegun-0.4.2 collected 25 items tests/test_decoder.py .............python3.7: src/sabyenc3.c:596: decode_usenet_chunks: Assertion `PyByteArray_Check(PyList_GetItem(Py_input_list, lp))' failed. Fatal Python error: Aborted Current thread 0x00007fb5db746740 (most recent call first): File "/tmp/sabyenc/tests/testsupport.py", line 73 in sabyenc3_wrapper File "/tmp/sabyenc/tests/test_decoder.py", line 117 in test_empty_size_pickles File "/usr/lib/python3.7/site-packages/_pytest/python.py", line 180 in pytest_pyfunc_call File "/usr/lib/python3.7/site-packages/pluggy/callers.py", line 187 in _multicall File "/usr/lib/python3.7/site-packages/pluggy/manager.py", line 87 in [...] File "/usr/lib/python3.7/site-packages/pytest/__main__.py", line 7 in File "/usr/lib/python3.7/runpy.py", line 85 in _run_code File "/usr/lib/python3.7/runpy.py", line 193 in _run_module_as_main Aborted (core dumped) Now the message clearly indicates the failed assertion. It is also common that upstream is initially unable to reproduce the bug. This is because Ubuntu and many other common distributions build Python with ``-DNDEBUG`` and the flag leaks to extension builds. As a result, all assertions are stripped at build time. Upstream can work around that by explicitly setting ``CFLAGS`` for the build, e.g.:: $ CFLAGS='-O0 -g' python setup.py build build_ext -i $ pytest -s Installing extra dependencies in test environment (PEP 517 mode) ================================================================ Rarely, the test suite expects some package being installed that does not fit being packaged and installed system-wide. For example, isort's tests use a few example plugins that are not useful to end users, or pip's test suite still requires old virtualenv that collides with the modern versions. These problems can be resolved by installing the packages locally within the ebuild. The ``distutils-r1.eclass`` provides a ``distutils_pep517_install`` helper that can be used to install additional packages. Please note that this helper is intended for expert users only, and special care needs to be taken when using it. The function takes a single argument specifying the destination install root, and installs the package from the current directory. For example, ``dev-python/isort`` uses the following test phase to duplicate the install tree and then install additional packages into it for the purpose of testing. Note that ``PATH`` is manipulated (rather than ``PYTHONPATH``) to use virtualenv-style install root. .. code-block:: bash python_test() { cp -a "${BUILD_DIR}"/{install,test} || die local -x PATH=${BUILD_DIR}/test/usr/bin:${PATH} # Install necessary plugins local p for p in example*/; do pushd "${p}" >/dev/null || die distutils_pep517_install "${BUILD_DIR}"/test popd >/dev/null || die done epytest } .. _unittest: https://docs.python.org/3/library/unittest.html .. _pytest: https://docs.pytest.org/en/latest/ .. _django: https://www.djangoproject.com/ .. _why tests must not use Internet: https://devmanual.gentoo.org/ebuild-writing/functions/src_test/#tests-that-require-network-or-service-access