|
| 1 | +# Copyright (c) Jupyter Development Team. |
| 2 | +# Distributed under the terms of the Modified BSD License. |
| 3 | + |
| 4 | +""" |
| 5 | +test_packages |
| 6 | +~~~~~~~~~~~~~~~ |
| 7 | +This test module tests if R and Python packages installed can be imported. |
| 8 | +It's a basic test aiming to prove that the package is working properly. |
| 9 | +
|
| 10 | +The goal is to detect import errors that can be caused by incompatibilities between packages for example: |
| 11 | +
|
| 12 | +- #1012: issue importing `sympy` |
| 13 | +- #966: isssue importing `pyarrow` |
| 14 | +
|
| 15 | +This module checks dynmamically, through the `CondaPackageHelper`, only the specified packages i.e. packages requested by `conda install` in the `Dockerfiles`. |
| 16 | +This means that it does not check dependencies. This choice is a tradeoff to cover the main requirements while achieving reasonable test duration. |
| 17 | +However it could be easily changed (or completed) to cover also dependencies `package_helper.installed_packages()` instead of `package_helper.specified_packages()`. |
| 18 | +
|
| 19 | +Example: |
| 20 | +
|
| 21 | + $ make test/datascience-notebook |
| 22 | +
|
| 23 | + # [...] |
| 24 | + # test/test_packages.py::test_python_packages |
| 25 | + # --------------------------------------------------------------------------------------------- live log setup ---------------------------------------------------------------------------------------------- |
| 26 | + # 2020-03-08 09:56:04 [ INFO] Starting container jupyter/datascience-notebook ... (helpers.py:51) |
| 27 | + # 2020-03-08 09:56:04 [ INFO] Running jupyter/datascience-notebook with args {'detach': True, 'ports': {'8888/tcp': 8888}, 'tty': True, 'command': ['start.sh', 'bash', '-c', 'sleep infinity']} ... (conftest.py:78) |
| 28 | + # 2020-03-08 09:56:04 [ INFO] Grabing the list of specifications ... (helpers.py:76) |
| 29 | + # ---------------------------------------------------------------------------------------------- live log call ---------------------------------------------------------------------------------------------- |
| 30 | + # 2020-03-08 09:56:07 [ INFO] Testing the import of packages ... (test_packages.py:125) |
| 31 | + # 2020-03-08 09:56:07 [ INFO] Trying to import conda (test_packages.py:127) |
| 32 | + # 2020-03-08 09:56:07 [ INFO] Trying to import notebook (test_packages.py:127) |
| 33 | + # 2020-03-08 09:56:08 [ INFO] Trying to import jupyterhub (test_packages.py:127) |
| 34 | + # [...] |
| 35 | +
|
| 36 | +""" |
| 37 | + |
| 38 | +import logging |
| 39 | + |
| 40 | +import pytest |
| 41 | + |
| 42 | +from helpers import CondaPackageHelper |
| 43 | + |
| 44 | +LOGGER = logging.getLogger(__name__) |
| 45 | + |
| 46 | +# Mapping between package and module name |
| 47 | +PACKAGE_MAPPING = { |
| 48 | + # Python |
| 49 | + "matplotlib-base": "matplotlib", |
| 50 | + "beautifulsoup4": "bs4", |
| 51 | + "scikit-learn": "sklearn", |
| 52 | + "scikit-image": "skimage", |
| 53 | + "spylon-kernel": "spylon_kernel", |
| 54 | + # R |
| 55 | + "randomforest": "randomForest", |
| 56 | + "rsqlite": "DBI", |
| 57 | + "rcurl": "RCurl", |
| 58 | + "rodbc": "RODBC", |
| 59 | +} |
| 60 | + |
| 61 | +# List of packages that cannot be tested in a standard way |
| 62 | +EXCLUDED_PACKAGES = [ |
| 63 | + "tini", |
| 64 | + "python", |
| 65 | + "hdf5", |
| 66 | + "conda-forge::blas[build=openblas]", |
| 67 | + "protobuf", |
| 68 | + "r-irkernel", |
| 69 | + "unixodbc", |
| 70 | +] |
| 71 | + |
| 72 | + |
| 73 | +@pytest.fixture(scope="function") |
| 74 | +def package_helper(container): |
| 75 | + """Return a package helper object that can be used to perform tests on installed packages""" |
| 76 | + return CondaPackageHelper(container) |
| 77 | + |
| 78 | + |
| 79 | +@pytest.fixture(scope="function") |
| 80 | +def packages(package_helper): |
| 81 | + """Return the list of specified packages (i.e. packages explicitely installed excluding dependencies)""" |
| 82 | + return package_helper.specified_packages() |
| 83 | + |
| 84 | + |
| 85 | +def package_map(package): |
| 86 | + """Perform a mapping between the python package name and the name used for the import""" |
| 87 | + _package = package |
| 88 | + if _package in PACKAGE_MAPPING: |
| 89 | + _package = PACKAGE_MAPPING.get(_package) |
| 90 | + return _package |
| 91 | + |
| 92 | + |
| 93 | +def excluded_package_predicate(package): |
| 94 | + """Return whether a package is excluded from the list (i.e. a package that cannot be tested with standard imports)""" |
| 95 | + return package in EXCLUDED_PACKAGES |
| 96 | + |
| 97 | + |
| 98 | +def python_package_predicate(package): |
| 99 | + """Predicate matching python packages""" |
| 100 | + return not excluded_package_predicate(package) and not r_package_predicate(package) |
| 101 | + |
| 102 | + |
| 103 | +def r_package_predicate(package): |
| 104 | + """Predicate matching R packages""" |
| 105 | + return not excluded_package_predicate(package) and package.startswith("r-") |
| 106 | + |
| 107 | + |
| 108 | +def _check_import_package(package_helper, command): |
| 109 | + """Generic function executing a command""" |
| 110 | + LOGGER.debug(f"Trying to import a package with [{command}] ...") |
| 111 | + rc = package_helper.running_container.exec_run(command) |
| 112 | + return rc.exit_code |
| 113 | + |
| 114 | + |
| 115 | +def check_import_python_package(package_helper, package): |
| 116 | + """Try to import a Python package from the command line""" |
| 117 | + return _check_import_package(package_helper, ["python", "-c", f"import {package}"]) |
| 118 | + |
| 119 | + |
| 120 | +def check_import_r_package(package_helper, package): |
| 121 | + """Try to import a R package from the command line""" |
| 122 | + return _check_import_package( |
| 123 | + package_helper, ["R", "--slave", "-e", f"library({package})"] |
| 124 | + ) |
| 125 | + |
| 126 | + |
| 127 | +def _import_packages(package_helper, filtered_packages, check_function, max_failures): |
| 128 | + """Test if packages can be imported |
| 129 | +
|
| 130 | + Note: using a list of packages instead of a fixture for the list of packages since pytest prevents use of multiple yields |
| 131 | + """ |
| 132 | + failures = {} |
| 133 | + LOGGER.info(f"Testing the import of packages ...") |
| 134 | + for package in filtered_packages: |
| 135 | + LOGGER.info(f"Trying to import {package}") |
| 136 | + try: |
| 137 | + assert ( |
| 138 | + check_function(package_helper, package) == 0 |
| 139 | + ), f"Package [{package}] import failed" |
| 140 | + except AssertionError as err: |
| 141 | + failures[package] = err |
| 142 | + if len(failures) > max_failures: |
| 143 | + raise AssertionError(failures) |
| 144 | + elif len(failures) > 0: |
| 145 | + LOGGER.warning(f"Some import(s) has(have) failed: {failures}") |
| 146 | + |
| 147 | + |
| 148 | +@pytest.fixture(scope="function") |
| 149 | +def r_packages(packages): |
| 150 | + """Return an iterable of R packages""" |
| 151 | + # package[2:] is to remove the leading "r-" appended by conda on R packages |
| 152 | + return map( |
| 153 | + lambda package: package_map(package[2:]), filter(r_package_predicate, packages) |
| 154 | + ) |
| 155 | + |
| 156 | + |
| 157 | +def test_python_packages(package_helper, python_packages, max_failures=0): |
| 158 | + """Test the import of specified python packages""" |
| 159 | + return _import_packages( |
| 160 | + package_helper, python_packages, check_import_python_package, max_failures |
| 161 | + ) |
| 162 | + |
| 163 | + |
| 164 | +@pytest.fixture(scope="function") |
| 165 | +def python_packages(packages): |
| 166 | + """Return an iterable of Python packages""" |
| 167 | + return map(package_map, filter(python_package_predicate, packages)) |
| 168 | + |
| 169 | + |
| 170 | +def test_r_packages(package_helper, r_packages, max_failures=0): |
| 171 | + """Test the import of specified R packages""" |
| 172 | + return _import_packages( |
| 173 | + package_helper, r_packages, check_import_r_package, max_failures |
| 174 | + ) |
0 commit comments