Coverage for /usr/lib/python3/dist-packages/scipy/_lib/_testutils.py: 22%
121 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""
2Generic test utilities.
4"""
6import os
7import re
8import sys
9import numpy as np
10import inspect
13__all__ = ['PytestTester', 'check_free_memory', '_TestPythranFunc']
16class FPUModeChangeWarning(RuntimeWarning):
17 """Warning about FPU mode change"""
18 pass
21class PytestTester:
22 """
23 Run tests for this namespace
25 ``scipy.test()`` runs tests for all of SciPy, with the default settings.
26 When used from a submodule (e.g., ``scipy.cluster.test()``, only the tests
27 for that namespace are run.
29 Parameters
30 ----------
31 label : {'fast', 'full'}, optional
32 Whether to run only the fast tests, or also those marked as slow.
33 Default is 'fast'.
34 verbose : int, optional
35 Test output verbosity. Default is 1.
36 extra_argv : list, optional
37 Arguments to pass through to Pytest.
38 doctests : bool, optional
39 Whether to run doctests or not. Default is False.
40 coverage : bool, optional
41 Whether to run tests with code coverage measurements enabled.
42 Default is False.
43 tests : list of str, optional
44 List of module names to run tests for. By default, uses the module
45 from which the ``test`` function is called.
46 parallel : int, optional
47 Run tests in parallel with pytest-xdist, if number given is larger than
48 1. Default is 1.
50 """
51 def __init__(self, module_name):
52 self.module_name = module_name
54 def __call__(self, label="fast", verbose=1, extra_argv=None, doctests=False,
55 coverage=False, tests=None, parallel=None):
56 import pytest
58 module = sys.modules[self.module_name]
59 module_path = os.path.abspath(module.__path__[0])
61 pytest_args = ['--showlocals', '--tb=short']
63 if doctests:
64 raise ValueError("Doctests not supported")
66 if extra_argv:
67 pytest_args += list(extra_argv)
69 if verbose and int(verbose) > 1:
70 pytest_args += ["-" + "v"*(int(verbose)-1)]
72 if coverage:
73 pytest_args += ["--cov=" + module_path]
75 if label == "fast":
76 pytest_args += ["-m", "not slow"]
77 elif label != "full":
78 pytest_args += ["-m", label]
80 if tests is None:
81 tests = [self.module_name]
83 if parallel is not None and parallel > 1:
84 if _pytest_has_xdist():
85 pytest_args += ['-n', str(parallel)]
86 else:
87 import warnings
88 warnings.warn('Could not run tests in parallel because '
89 'pytest-xdist plugin is not available.')
91 pytest_args += ['--pyargs'] + list(tests)
93 try:
94 code = pytest.main(pytest_args)
95 except SystemExit as exc:
96 code = exc.code
98 return (code == 0)
101class _TestPythranFunc:
102 '''
103 These are situations that can be tested in our pythran tests:
104 - A function with multiple array arguments and then
105 other positional and keyword arguments.
106 - A function with array-like keywords (e.g. `def somefunc(x0, x1=None)`.
107 Note: list/tuple input is not yet tested!
109 `self.arguments`: A dictionary which key is the index of the argument,
110 value is tuple(array value, all supported dtypes)
111 `self.partialfunc`: A function used to freeze some non-array argument
112 that of no interests in the original function
113 '''
114 ALL_INTEGER = [np.int8, np.int16, np.int32, np.int64, np.intc, np.intp]
115 ALL_FLOAT = [np.float32, np.float64]
116 ALL_COMPLEX = [np.complex64, np.complex128]
118 def setup_method(self):
119 self.arguments = {}
120 self.partialfunc = None
121 self.expected = None
123 def get_optional_args(self, func):
124 # get optional arguments with its default value,
125 # used for testing keywords
126 signature = inspect.signature(func)
127 optional_args = {}
128 for k, v in signature.parameters.items():
129 if v.default is not inspect.Parameter.empty:
130 optional_args[k] = v.default
131 return optional_args
133 def get_max_dtype_list_length(self):
134 # get the max supported dtypes list length in all arguments
135 max_len = 0
136 for arg_idx in self.arguments:
137 cur_len = len(self.arguments[arg_idx][1])
138 if cur_len > max_len:
139 max_len = cur_len
140 return max_len
142 def get_dtype(self, dtype_list, dtype_idx):
143 # get the dtype from dtype_list via index
144 # if the index is out of range, then return the last dtype
145 if dtype_idx > len(dtype_list)-1:
146 return dtype_list[-1]
147 else:
148 return dtype_list[dtype_idx]
150 def test_all_dtypes(self):
151 for type_idx in range(self.get_max_dtype_list_length()):
152 args_array = []
153 for arg_idx in self.arguments:
154 new_dtype = self.get_dtype(self.arguments[arg_idx][1],
155 type_idx)
156 args_array.append(self.arguments[arg_idx][0].astype(new_dtype))
157 self.pythranfunc(*args_array)
159 def test_views(self):
160 args_array = []
161 for arg_idx in self.arguments:
162 args_array.append(self.arguments[arg_idx][0][::-1][::-1])
163 self.pythranfunc(*args_array)
165 def test_strided(self):
166 args_array = []
167 for arg_idx in self.arguments:
168 args_array.append(np.repeat(self.arguments[arg_idx][0],
169 2, axis=0)[::2])
170 self.pythranfunc(*args_array)
173def _pytest_has_xdist():
174 """
175 Check if the pytest-xdist plugin is installed, providing parallel tests
176 """
177 # Check xdist exists without importing, otherwise pytests emits warnings
178 from importlib.util import find_spec
179 return find_spec('xdist') is not None
182def check_free_memory(free_mb):
183 """
184 Check *free_mb* of memory is available, otherwise do pytest.skip
185 """
186 import pytest
188 try:
189 mem_free = _parse_size(os.environ['SCIPY_AVAILABLE_MEM'])
190 msg = '{} MB memory required, but environment SCIPY_AVAILABLE_MEM={}'.format(
191 free_mb, os.environ['SCIPY_AVAILABLE_MEM'])
192 except KeyError:
193 mem_free = _get_mem_available()
194 if mem_free is None:
195 pytest.skip("Could not determine available memory; set SCIPY_AVAILABLE_MEM "
196 "variable to free memory in MB to run the test.")
197 msg = '{} MB memory required, but {} MB available'.format(
198 free_mb, mem_free/1e6)
200 if mem_free < free_mb * 1e6:
201 pytest.skip(msg)
204def _parse_size(size_str):
205 suffixes = {'': 1e6,
206 'b': 1.0,
207 'k': 1e3, 'M': 1e6, 'G': 1e9, 'T': 1e12,
208 'kb': 1e3, 'Mb': 1e6, 'Gb': 1e9, 'Tb': 1e12,
209 'kib': 1024.0, 'Mib': 1024.0**2, 'Gib': 1024.0**3, 'Tib': 1024.0**4}
210 m = re.match(r'^\s*(\d+)\s*({})\s*$'.format('|'.join(suffixes.keys())),
211 size_str,
212 re.I)
213 if not m or m.group(2) not in suffixes:
214 raise ValueError("Invalid size string")
216 return float(m.group(1)) * suffixes[m.group(2)]
219def _get_mem_available():
220 """
221 Get information about memory available, not counting swap.
222 """
223 try:
224 import psutil
225 return psutil.virtual_memory().available
226 except (ImportError, AttributeError):
227 pass
229 if sys.platform.startswith('linux'):
230 info = {}
231 with open('/proc/meminfo') as f:
232 for line in f:
233 p = line.split()
234 info[p[0].strip(':').lower()] = float(p[1]) * 1e3
236 if 'memavailable' in info:
237 # Linux >= 3.14
238 return info['memavailable']
239 else:
240 return info['memfree'] + info['cached']
242 return None