_testutils.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. """
  2. Generic test utilities.
  3. """
  4. import os
  5. import re
  6. import sys
  7. import numpy as np
  8. import inspect
  9. __all__ = ['PytestTester', 'check_free_memory', '_TestPythranFunc']
  10. class FPUModeChangeWarning(RuntimeWarning):
  11. """Warning about FPU mode change"""
  12. pass
  13. class PytestTester:
  14. """
  15. Pytest test runner entry point.
  16. """
  17. def __init__(self, module_name):
  18. self.module_name = module_name
  19. def __call__(self, label="fast", verbose=1, extra_argv=None, doctests=False,
  20. coverage=False, tests=None, parallel=None):
  21. import pytest
  22. module = sys.modules[self.module_name]
  23. module_path = os.path.abspath(module.__path__[0])
  24. pytest_args = ['--showlocals', '--tb=short']
  25. if doctests:
  26. raise ValueError("Doctests not supported")
  27. if extra_argv:
  28. pytest_args += list(extra_argv)
  29. if verbose and int(verbose) > 1:
  30. pytest_args += ["-" + "v"*(int(verbose)-1)]
  31. if coverage:
  32. pytest_args += ["--cov=" + module_path]
  33. if label == "fast":
  34. pytest_args += ["-m", "not slow"]
  35. elif label != "full":
  36. pytest_args += ["-m", label]
  37. if tests is None:
  38. tests = [self.module_name]
  39. if parallel is not None and parallel > 1:
  40. if _pytest_has_xdist():
  41. pytest_args += ['-n', str(parallel)]
  42. else:
  43. import warnings
  44. warnings.warn('Could not run tests in parallel because '
  45. 'pytest-xdist plugin is not available.')
  46. pytest_args += ['--pyargs'] + list(tests)
  47. try:
  48. code = pytest.main(pytest_args)
  49. except SystemExit as exc:
  50. code = exc.code
  51. return (code == 0)
  52. class _TestPythranFunc:
  53. '''
  54. These are situations that can be tested in our pythran tests:
  55. - A function with multiple array arguments and then
  56. other positional and keyword arguments.
  57. - A function with array-like keywords (e.g. `def somefunc(x0, x1=None)`.
  58. Note: list/tuple input is not yet tested!
  59. `self.arguments`: A dictionary which key is the index of the argument,
  60. value is tuple(array value, all supported dtypes)
  61. `self.partialfunc`: A function used to freeze some non-array argument
  62. that of no interests in the original function
  63. '''
  64. ALL_INTEGER = [np.int8, np.int16, np.int32, np.int64, np.intc, np.intp]
  65. ALL_FLOAT = [np.float32, np.float64]
  66. ALL_COMPLEX = [np.complex64, np.complex128]
  67. def setup_method(self):
  68. self.arguments = {}
  69. self.partialfunc = None
  70. self.expected = None
  71. def get_optional_args(self, func):
  72. # get optional arguments with its default value,
  73. # used for testing keywords
  74. signature = inspect.signature(func)
  75. optional_args = {}
  76. for k, v in signature.parameters.items():
  77. if v.default is not inspect.Parameter.empty:
  78. optional_args[k] = v.default
  79. return optional_args
  80. def get_max_dtype_list_length(self):
  81. # get the max supported dtypes list length in all arguments
  82. max_len = 0
  83. for arg_idx in self.arguments:
  84. cur_len = len(self.arguments[arg_idx][1])
  85. if cur_len > max_len:
  86. max_len = cur_len
  87. return max_len
  88. def get_dtype(self, dtype_list, dtype_idx):
  89. # get the dtype from dtype_list via index
  90. # if the index is out of range, then return the last dtype
  91. if dtype_idx > len(dtype_list)-1:
  92. return dtype_list[-1]
  93. else:
  94. return dtype_list[dtype_idx]
  95. def test_all_dtypes(self):
  96. for type_idx in range(self.get_max_dtype_list_length()):
  97. args_array = []
  98. for arg_idx in self.arguments:
  99. new_dtype = self.get_dtype(self.arguments[arg_idx][1],
  100. type_idx)
  101. args_array.append(self.arguments[arg_idx][0].astype(new_dtype))
  102. self.pythranfunc(*args_array)
  103. def test_views(self):
  104. args_array = []
  105. for arg_idx in self.arguments:
  106. args_array.append(self.arguments[arg_idx][0][::-1][::-1])
  107. self.pythranfunc(*args_array)
  108. def test_strided(self):
  109. args_array = []
  110. for arg_idx in self.arguments:
  111. args_array.append(np.repeat(self.arguments[arg_idx][0],
  112. 2, axis=0)[::2])
  113. self.pythranfunc(*args_array)
  114. def _pytest_has_xdist():
  115. """
  116. Check if the pytest-xdist plugin is installed, providing parallel tests
  117. """
  118. # Check xdist exists without importing, otherwise pytests emits warnings
  119. from importlib.util import find_spec
  120. return find_spec('xdist') is not None
  121. def check_free_memory(free_mb):
  122. """
  123. Check *free_mb* of memory is available, otherwise do pytest.skip
  124. """
  125. import pytest
  126. try:
  127. mem_free = _parse_size(os.environ['SCIPY_AVAILABLE_MEM'])
  128. msg = '{0} MB memory required, but environment SCIPY_AVAILABLE_MEM={1}'.format(
  129. free_mb, os.environ['SCIPY_AVAILABLE_MEM'])
  130. except KeyError:
  131. mem_free = _get_mem_available()
  132. if mem_free is None:
  133. pytest.skip("Could not determine available memory; set SCIPY_AVAILABLE_MEM "
  134. "variable to free memory in MB to run the test.")
  135. msg = '{0} MB memory required, but {1} MB available'.format(
  136. free_mb, mem_free/1e6)
  137. if mem_free < free_mb * 1e6:
  138. pytest.skip(msg)
  139. def _parse_size(size_str):
  140. suffixes = {'': 1e6,
  141. 'b': 1.0,
  142. 'k': 1e3, 'M': 1e6, 'G': 1e9, 'T': 1e12,
  143. 'kb': 1e3, 'Mb': 1e6, 'Gb': 1e9, 'Tb': 1e12,
  144. 'kib': 1024.0, 'Mib': 1024.0**2, 'Gib': 1024.0**3, 'Tib': 1024.0**4}
  145. m = re.match(r'^\s*(\d+)\s*({0})\s*$'.format('|'.join(suffixes.keys())),
  146. size_str,
  147. re.I)
  148. if not m or m.group(2) not in suffixes:
  149. raise ValueError("Invalid size string")
  150. return float(m.group(1)) * suffixes[m.group(2)]
  151. def _get_mem_available():
  152. """
  153. Get information about memory available, not counting swap.
  154. """
  155. try:
  156. import psutil
  157. return psutil.virtual_memory().available
  158. except (ImportError, AttributeError):
  159. pass
  160. if sys.platform.startswith('linux'):
  161. info = {}
  162. with open('/proc/meminfo', 'r') as f:
  163. for line in f:
  164. p = line.split()
  165. info[p[0].strip(':').lower()] = float(p[1]) * 1e3
  166. if 'memavailable' in info:
  167. # Linux >= 3.14
  168. return info['memavailable']
  169. else:
  170. return info['memfree'] + info['cached']
  171. return None