| from __future__ import annotations |
|
|
| import functools |
| import inspect |
| import os |
| import random |
| import traceback |
| import unittest |
| import warnings |
|
|
| import numpy |
|
|
| import cupy |
| from cupy.exceptions import AxisError |
| from cupy.testing import _array |
| from cupy.testing import _parameterized |
|
|
| from cupy.testing._pytest_impl import is_available |
|
|
|
|
| if is_available(): |
| import _pytest.outcomes |
| _is_pytest_available = True |
| _skip_classes: tuple[type, ...] = ( |
| unittest.SkipTest, _pytest.outcomes.Skipped) |
| else: |
| _is_pytest_available = False |
| _skip_classes = unittest.SkipTest, |
|
|
|
|
| def _format_exception(exc): |
| if exc is None: |
| return None |
| |
| return ''.join(traceback.TracebackException.from_exception(exc).format()) |
|
|
|
|
| def _call_func(impl, args, kw): |
| |
| exceptions = Exception, |
| if _is_pytest_available: |
| exceptions += _pytest.outcomes.Skipped, |
|
|
| try: |
| result = impl(*args, **kw) |
| error = None |
| except exceptions as e: |
| tb = e.__traceback__ |
| if tb.tb_next is None: |
| |
| raise e |
| result = None |
| error = e |
|
|
| return result, error |
|
|
|
|
| def _call_func_cupy(impl, args, kw, name, sp_name, scipy_name): |
| assert isinstance(name, str) |
| assert sp_name is None or isinstance(sp_name, str) |
| assert scipy_name is None or isinstance(scipy_name, str) |
| kw = kw.copy() |
|
|
| |
| if sp_name: |
| import cupyx.scipy.sparse |
| kw[sp_name] = cupyx.scipy.sparse |
| if scipy_name: |
| import cupyx |
| kw[scipy_name] = cupyx.scipy |
| kw[name] = cupy |
| result, error = _call_func(impl, args, kw) |
| return result, error |
|
|
|
|
| def _call_func_numpy(impl, args, kw, name, sp_name, scipy_name): |
| assert isinstance(name, str) |
| assert sp_name is None or isinstance(sp_name, str) |
| assert scipy_name is None or isinstance(scipy_name, str) |
| kw = kw.copy() |
|
|
| |
| kw[name] = numpy |
| if sp_name: |
| import scipy.sparse |
| kw[sp_name] = scipy.sparse |
| if scipy_name: |
| import scipy |
| kw[scipy_name] = scipy |
| result, error = _call_func(impl, args, kw) |
| return result, error |
|
|
|
|
| def _call_func_numpy_cupy(impl, args, kw, name, sp_name, scipy_name): |
| |
| cupy_result, cupy_error = _call_func_cupy( |
| impl, args, kw, name, sp_name, scipy_name) |
|
|
| |
| numpy_result, numpy_error = _call_func_numpy( |
| impl, args, kw, name, sp_name, scipy_name) |
|
|
| return ( |
| cupy_result, cupy_error, |
| numpy_result, numpy_error) |
|
|
|
|
| _numpy_errors = [ |
| AttributeError, Exception, IndexError, TypeError, ValueError, |
| NotImplementedError, DeprecationWarning, |
| AxisError, numpy.linalg.LinAlgError, |
| ] |
|
|
|
|
| def _check_numpy_cupy_error_compatible(cupy_error, numpy_error): |
| """Checks if try/except blocks are equivalent up to public error classes |
| """ |
|
|
| return all(isinstance(cupy_error, err) == isinstance(numpy_error, err) |
| for err in _numpy_errors) |
|
|
|
|
| def _fail_test_with_unexpected_errors( |
| tb, msg_format, cupy_error, numpy_error): |
| |
| |
| |
|
|
| msg = msg_format.format( |
| cupy_error=_format_exception(cupy_error), |
| numpy_error=_format_exception(numpy_error)) |
|
|
| |
| raise AssertionError(msg).with_traceback(tb) |
|
|
|
|
| def _check_cupy_numpy_error(cupy_error, numpy_error, |
| accept_error=False): |
| |
| if (isinstance(cupy_error, _skip_classes) |
| and isinstance(numpy_error, _skip_classes)): |
| if cupy_error.__class__ is not numpy_error.__class__: |
| raise AssertionError( |
| 'Both numpy and cupy were skipped but with different ' |
| 'exceptions.') |
| if cupy_error.args != numpy_error.args: |
| raise AssertionError( |
| 'Both numpy and cupy were skipped but with different causes.') |
| raise numpy_error |
|
|
| |
| if os.environ.get('CUPY_CI', '') != '' and cupy_error is not None: |
| frame = traceback.extract_tb(cupy_error.__traceback__)[-1] |
| filename = os.path.basename(frame.filename) |
| if filename == 'test_helper.py': |
| |
| pass |
| elif filename.startswith('test_'): |
| _fail_test_with_unexpected_errors( |
| cupy_error.__traceback__, |
| 'Error was raised from test code.\n\n{cupy_error}', |
| cupy_error, None) |
|
|
| |
| if accept_error is True: |
| accept_error = Exception |
| elif not accept_error: |
| accept_error = () |
| |
| if cupy_error is None and numpy_error is None: |
| raise AssertionError( |
| 'Both cupy and numpy are expected to raise errors, but not') |
| elif cupy_error is None: |
| _fail_test_with_unexpected_errors( |
| numpy_error.__traceback__, |
| 'Only numpy raises error\n\n{numpy_error}', |
| None, numpy_error) |
| elif numpy_error is None: |
| _fail_test_with_unexpected_errors( |
| cupy_error.__traceback__, |
| 'Only cupy raises error\n\n{cupy_error}', |
| cupy_error, None) |
|
|
| elif not _check_numpy_cupy_error_compatible(cupy_error, numpy_error): |
| _fail_test_with_unexpected_errors( |
| cupy_error.__traceback__, |
| '''Different types of errors occurred |
| |
| cupy |
| {cupy_error} |
| |
| numpy |
| {numpy_error} |
| ''', |
| cupy_error, numpy_error) |
|
|
| elif not (isinstance(cupy_error, accept_error) |
| and isinstance(numpy_error, accept_error)): |
| _fail_test_with_unexpected_errors( |
| cupy_error.__traceback__, |
| '''Both cupy and numpy raise exceptions |
| |
| cupy |
| {cupy_error} |
| |
| numpy |
| {numpy_error} |
| ''', |
| cupy_error, numpy_error) |
|
|
|
|
| def _signed_counterpart(dtype): |
| return numpy.dtype(numpy.dtype(dtype).char.lower()).type |
|
|
|
|
| def _make_positive_masks(impl, args, kw, name, sp_name, scipy_name): |
| |
| |
| ks = [k for k, v in kw.items() if v in _unsigned_dtypes] |
| for k in ks: |
| kw[k] = _signed_counterpart(kw[k]) |
| result, error = _call_func_cupy( |
| impl, args, kw, name, sp_name, scipy_name) |
| assert error is None |
| if not isinstance(result, (tuple, list)): |
| result = result, |
| return [cupy.asnumpy(r) >= 0 for r in result] |
|
|
|
|
| def _contains_signed_and_unsigned(kw): |
| def isdtype(v): |
| if isinstance(v, numpy.dtype): |
| return True |
| elif isinstance(v, str): |
| return True |
| elif isinstance(v, type) and issubclass(v, numpy.number): |
| return True |
| else: |
| return False |
| vs = set(v for v in kw.values() if isdtype(v)) |
| return any(d in vs for d in _unsigned_dtypes) and \ |
| any(d in vs for d in _float_dtypes + _signed_dtypes) |
|
|
|
|
| def _wraps_partial(wrapped, *names): |
| |
| def decorator(impl): |
| impl = functools.wraps(wrapped)(impl) |
| impl.__signature__ = inspect.signature( |
| functools.partial(wrapped, **{name: None for name in names})) |
| return impl |
| return decorator |
|
|
|
|
| def _wraps_partial_xp(wrapped, name, sp_name, scipy_name): |
| names = [name, sp_name, scipy_name] |
| names = [n for n in names if n is not None] |
| return _wraps_partial(wrapped, *names) |
|
|
|
|
| def _make_decorator(check_func, name, type_check, contiguous_check, |
| accept_error, sp_name=None, scipy_name=None, |
| check_sparse_format=True): |
| assert isinstance(name, str) |
| assert sp_name is None or isinstance(sp_name, str) |
| assert scipy_name is None or isinstance(scipy_name, str) |
|
|
| def decorator(impl): |
| @_wraps_partial_xp(impl, name, sp_name, scipy_name) |
| def test_func(*args, **kw): |
| |
| ( |
| cupy_result, cupy_error, |
| numpy_result, numpy_error) = ( |
| _call_func_numpy_cupy( |
| impl, args, kw, name, sp_name, scipy_name)) |
| assert cupy_result is not None or cupy_error is not None |
| assert numpy_result is not None or numpy_error is not None |
|
|
| |
| if cupy_error or numpy_error: |
| _check_cupy_numpy_error(cupy_error, |
| numpy_error, |
| accept_error=accept_error) |
| return |
|
|
| |
|
|
| if not isinstance(cupy_result, (tuple, list)): |
| cupy_result = cupy_result, |
| if not isinstance(numpy_result, (tuple, list)): |
| numpy_result = numpy_result, |
|
|
| assert len(cupy_result) == len(numpy_result) |
|
|
| |
| cupy_numpy_result_ndarrays = [ |
| _convert_output_to_ndarray( |
| cupy_r, numpy_r, sp_name, check_sparse_format) |
| for cupy_r, numpy_r in zip(cupy_result, numpy_result)] |
|
|
| |
| if type_check: |
| for cupy_r, numpy_r in cupy_numpy_result_ndarrays: |
| if cupy_r.dtype != numpy_r.dtype: |
| raise AssertionError( |
| '''ndarrays of different dtypes are returned. |
| cupy: {} |
| numpy: {}'''.format(cupy_r.dtype, numpy_r.dtype)) |
|
|
| |
| if contiguous_check: |
| for cupy_r, numpy_r in zip(cupy_result, numpy_result): |
| if isinstance(numpy_r, numpy.ndarray): |
| if (numpy_r.flags.c_contiguous |
| and not cupy_r.flags.c_contiguous): |
| raise AssertionError( |
| 'The state of c_contiguous flag is false. ' |
| '(cupy_result:{} numpy_result:{})'.format( |
| cupy_r.flags.c_contiguous, |
| numpy_r.flags.c_contiguous)) |
| if (numpy_r.flags.f_contiguous |
| and not cupy_r.flags.f_contiguous): |
| raise AssertionError( |
| 'The state of f_contiguous flag is false. ' |
| '(cupy_result:{} numpy_result:{})'.format( |
| cupy_r.flags.f_contiguous, |
| numpy_r.flags.f_contiguous)) |
|
|
| |
| for cupy_r, numpy_r in cupy_numpy_result_ndarrays: |
| assert cupy_r.shape == numpy_r.shape |
|
|
| masks = [None] * len(cupy_result) |
| if _contains_signed_and_unsigned(kw): |
| needs_mask = [ |
| cupy_r.dtype in _unsigned_dtypes |
| for cupy_r in cupy_result] |
| if any(needs_mask): |
| masks = _make_positive_masks( |
| impl, args, kw, name, sp_name, scipy_name) |
| for i, flag in enumerate(needs_mask): |
| if not flag: |
| masks[i] = None |
|
|
| |
| for (cupy_r, numpy_r), mask in zip( |
| cupy_numpy_result_ndarrays, masks): |
| |
| |
| |
| |
| |
|
|
| skip = False |
| if mask is not None: |
| if cupy_r.shape == (): |
| skip = (mask == 0).all() |
| else: |
| cupy_r = cupy_r[mask].get() |
| numpy_r = numpy_r[mask] |
|
|
| if not skip: |
| check_func(cupy_r, numpy_r) |
| return test_func |
| return decorator |
|
|
|
|
| def _convert_output_to_ndarray(c_out, n_out, sp_name, check_sparse_format): |
| """Checks type of cupy/numpy results and returns cupy/numpy ndarrays. |
| |
| Args: |
| c_out (cupy.ndarray, cupyx.scipy.sparse matrix, cupy.poly1d or scalar): |
| cupy result |
| n_out (numpy.ndarray, scipy.sparse matrix, numpy.poly1d or scalar): |
| numpy result |
| sp_name(str or None): Argument name whose value is either |
| ``scipy.sparse`` or ``cupyx.scipy.sparse`` module. If ``None``, no |
| argument is given for the modules. |
| check_sparse_format (bool): If ``True``, consistency of format of |
| sparse matrix is also checked. Default is ``True``. |
| |
| Returns: |
| The tuple of cupy.ndarray and numpy.ndarray. |
| """ |
| import cupyx.scipy.sparse |
| if sp_name is not None and cupyx.scipy.sparse.issparse(c_out): |
| |
| import scipy.sparse |
| assert scipy.sparse.issparse(n_out) |
| if check_sparse_format: |
| assert c_out.format == n_out.format |
| return c_out.toarray(), n_out.toarray() |
| if (isinstance(c_out, cupy.ndarray) |
| and isinstance(n_out, (numpy.ndarray, numpy.generic))): |
| |
| return c_out, n_out |
| if isinstance(c_out, cupy.poly1d) and isinstance(n_out, numpy.poly1d): |
| |
| assert c_out.variable == n_out.variable |
| return c_out.coeffs, n_out.coeffs |
| if isinstance(c_out, numpy.generic) and isinstance(n_out, numpy.generic): |
| |
| return c_out, n_out |
| if numpy.isscalar(c_out) and numpy.isscalar(n_out): |
| |
| return cupy.array(c_out), numpy.array(n_out) |
| raise AssertionError( |
| 'numpy and cupy returns different type of return value:\n' |
| 'cupy: {}\nnumpy: {}'.format( |
| type(c_out), type(n_out))) |
|
|
|
|
| def _check_tolerance_keys(rtol, atol): |
| def _check(tol): |
| if isinstance(tol, dict): |
| for k in tol.keys(): |
| if type(k) is type: |
| continue |
| if type(k) is str and k == 'default': |
| continue |
| msg = ('Keys of the tolerance dictionary need to be type ' |
| 'objects as `numpy.float32` and `cupy.float32` or ' |
| '`\'default\'` string.') |
| raise TypeError(msg) |
| _check(rtol) |
| _check(atol) |
|
|
|
|
| def _resolve_tolerance(type_check, result, rtol, atol): |
| def _resolve(dtype, tol): |
| if isinstance(tol, dict): |
| tol1 = tol.get(dtype.type) |
| if tol1 is None: |
| tol1 = tol.get('default') |
| if tol1 is None: |
| raise TypeError( |
| 'Can not find tolerance for {}'.format(dtype.type)) |
| return tol1 |
| else: |
| return tol |
|
|
| dtype = result.dtype |
| rtol1 = _resolve(dtype, rtol) |
| atol1 = _resolve(dtype, atol) |
| return rtol1, atol1 |
|
|
|
|
| def numpy_cupy_allclose(rtol=1e-7, atol=0, err_msg='', verbose=True, |
| name='xp', type_check=True, accept_error=False, |
| sp_name=None, scipy_name=None, contiguous_check=True, |
| *, _check_sparse_format=True): |
| """Decorator that checks NumPy results and CuPy ones are close. |
| |
| Args: |
| rtol(float or dict): Relative tolerance. Besides a float value, a |
| dictionary that maps a dtypes to a float value can be supplied to |
| adjust tolerance per dtype. If the dictionary has ``'default'`` |
| string as its key, its value is used as the default tolerance in |
| case any dtype keys do not match. |
| atol(float or dict): Absolute tolerance. Besides a float value, a |
| dictionary can be supplied as ``rtol``. |
| err_msg(str): The error message to be printed in case of failure. |
| verbose(bool): If ``True``, the conflicting values are |
| appended to the error message. |
| name(str): Argument name whose value is either |
| ``numpy`` or ``cupy`` module. |
| type_check(bool): If ``True``, consistency of dtype is also checked. |
| accept_error(bool, Exception or tuple of Exception): Specify |
| acceptable errors. When both NumPy test and CuPy test raises the |
| same type of errors, and the type of the errors is specified with |
| this argument, the errors are ignored and not raised. |
| If it is ``True`` all error types are acceptable. |
| If it is ``False`` no error is acceptable. |
| sp_name(str or None): Argument name whose value is either |
| ``scipy.sparse`` or ``cupyx.scipy.sparse`` module. If ``None``, no |
| argument is given for the modules. |
| scipy_name(str or None): Argument name whose value is either ``scipy`` |
| or ``cupyx.scipy`` module. If ``None``, no argument is given for |
| the modules. |
| contiguous_check(bool): If ``True``, consistency of contiguity is |
| also checked. |
| |
| Decorated test fixture is required to return the arrays whose values are |
| close between ``numpy`` case and ``cupy`` case. |
| For example, this test case checks ``numpy.zeros`` and ``cupy.zeros`` |
| should return same value. |
| |
| >>> import unittest |
| >>> from cupy import testing |
| >>> class TestFoo(unittest.TestCase): |
| ... |
| ... @testing.numpy_cupy_allclose() |
| ... def test_foo(self, xp): |
| ... # ... |
| ... # Prepare data with xp |
| ... # ... |
| ... |
| ... xp_result = xp.zeros(10) |
| ... return xp_result |
| |
| .. seealso:: :func:`cupy.testing.assert_allclose` |
| """ |
| _check_tolerance_keys(rtol, atol) |
|
|
| |
| |
| |
| if not type_check: |
| if isinstance(rtol, dict) or isinstance(atol, dict): |
| raise TypeError('When `type_check` is `False`, `rtol` and `atol` ' |
| 'must be supplied as float.') |
|
|
| def check_func(c, n): |
| rtol1, atol1 = _resolve_tolerance(type_check, c, rtol, atol) |
| _array.assert_allclose( |
| c, n, rtol1, atol1, err_msg=err_msg, verbose=verbose) |
| return _make_decorator(check_func, name, type_check, contiguous_check, |
| accept_error, sp_name, scipy_name, |
| _check_sparse_format) |
|
|
|
|
| def numpy_cupy_array_almost_equal(decimal=6, err_msg='', verbose=True, |
| name='xp', type_check=True, |
| accept_error=False, sp_name=None, |
| scipy_name=None): |
| """Decorator that checks NumPy results and CuPy ones are almost equal. |
| |
| Args: |
| decimal(int): Desired precision. |
| err_msg(str): The error message to be printed in case of failure. |
| verbose(bool): If ``True``, the conflicting values |
| are appended to the error message. |
| name(str): Argument name whose value is either |
| ``numpy`` or ``cupy`` module. |
| type_check(bool): If ``True``, consistency of dtype is also checked. |
| accept_error(bool, Exception or tuple of Exception): Specify |
| acceptable errors. When both NumPy test and CuPy test raises the |
| same type of errors, and the type of the errors is specified with |
| this argument, the errors are ignored and not raised. |
| If it is ``True`` all error types are acceptable. |
| If it is ``False`` no error is acceptable. |
| sp_name(str or None): Argument name whose value is either |
| ``scipy.sparse`` or ``cupyx.scipy.sparse`` module. If ``None``, no |
| argument is given for the modules. |
| scipy_name(str or None): Argument name whose value is either ``scipy`` |
| or ``cupyx.scipy`` module. If ``None``, no argument is given for |
| the modules. |
| |
| Decorated test fixture is required to return the same arrays |
| in the sense of :func:`cupy.testing.assert_array_almost_equal` |
| (except the type of array module) even if ``xp`` is ``numpy`` or ``cupy``. |
| |
| .. seealso:: :func:`cupy.testing.assert_array_almost_equal` |
| """ |
| def check_func(x, y): |
| _array.assert_array_almost_equal(x, y, decimal, err_msg, verbose) |
| return _make_decorator(check_func, name, type_check, False, |
| accept_error, sp_name, scipy_name) |
|
|
|
|
| def numpy_cupy_array_almost_equal_nulp(nulp=1, name='xp', type_check=True, |
| accept_error=False, sp_name=None, |
| scipy_name=None): |
| """Decorator that checks results of NumPy and CuPy are equal w.r.t. spacing. |
| |
| Args: |
| nulp(int): The maximum number of unit in the last place for tolerance. |
| name(str): Argument name whose value is either |
| ``numpy`` or ``cupy`` module. |
| type_check(bool): If ``True``, consistency of dtype is also checked. |
| accept_error(bool, Exception or tuple of Exception): Specify |
| acceptable errors. When both NumPy test and CuPy test raises the |
| same type of errors, and the type of the errors is specified with |
| this argument, the errors are ignored and not raised. |
| If it is ``True``, all error types are acceptable. |
| If it is ``False``, no error is acceptable. |
| sp_name(str or None): Argument name whose value is either |
| ``scipy.sparse`` or ``cupyx.scipy.sparse`` module. If ``None``, no |
| argument is given for the modules. |
| scipy_name(str or None): Argument name whose value is either ``scipy`` |
| or ``cupyx.scipy`` module. If ``None``, no argument is given for |
| the modules. |
| |
| Decorated test fixture is required to return the same arrays |
| in the sense of :func:`cupy.testing.assert_array_almost_equal_nulp` |
| (except the type of array module) even if ``xp`` is ``numpy`` or ``cupy``. |
| |
| .. seealso:: :func:`cupy.testing.assert_array_almost_equal_nulp` |
| """ |
| def check_func(x, y): |
| _array.assert_array_almost_equal_nulp(x, y, nulp) |
| return _make_decorator(check_func, name, type_check, False, |
| accept_error, sp_name, scipy_name=None) |
|
|
|
|
| def numpy_cupy_array_max_ulp(maxulp=1, dtype=None, name='xp', type_check=True, |
| accept_error=False, sp_name=None, |
| scipy_name=None): |
| """Decorator that checks results of NumPy and CuPy ones are equal w.r.t. ulp. |
| |
| Args: |
| maxulp(int): The maximum number of units in the last place |
| that elements of resulting two arrays can differ. |
| dtype(numpy.dtype): Data-type to convert the resulting |
| two array to if given. |
| name(str): Argument name whose value is either |
| ``numpy`` or ``cupy`` module. |
| type_check(bool): If ``True``, consistency of dtype is also checked. |
| accept_error(bool, Exception or tuple of Exception): Specify |
| acceptable errors. When both NumPy test and CuPy test raises the |
| same type of errors, and the type of the errors is specified with |
| this argument, the errors are ignored and not raised. |
| If it is ``True`` all error types are acceptable. |
| If it is ``False`` no error is acceptable. |
| sp_name(str or None): Argument name whose value is either |
| ``scipy.sparse`` or ``cupyx.scipy.sparse`` module. If ``None``, no |
| argument is given for the modules. |
| scipy_name(str or None): Argument name whose value is either ``scipy`` |
| or ``cupyx.scipy`` module. If ``None``, no argument is given for |
| the modules. |
| |
| Decorated test fixture is required to return the same arrays |
| in the sense of :func:`assert_array_max_ulp` |
| (except the type of array module) even if ``xp`` is ``numpy`` or ``cupy``. |
| |
| .. seealso:: :func:`cupy.testing.assert_array_max_ulp` |
| |
| """ |
| def check_func(x, y): |
| _array.assert_array_max_ulp(x, y, maxulp, dtype) |
| return _make_decorator(check_func, name, type_check, False, |
| accept_error, sp_name, scipy_name) |
|
|
|
|
| def numpy_cupy_array_equal(err_msg='', verbose=True, name='xp', |
| type_check=True, accept_error=False, sp_name=None, |
| scipy_name=None, strides_check=False): |
| """Decorator that checks NumPy results and CuPy ones are equal. |
| |
| Args: |
| err_msg(str): The error message to be printed in case of failure. |
| verbose(bool): If ``True``, the conflicting values are |
| appended to the error message. |
| name(str): Argument name whose value is either |
| ``numpy`` or ``cupy`` module. |
| type_check(bool): If ``True``, consistency of dtype is also checked. |
| accept_error(bool, Exception or tuple of Exception): Specify |
| acceptable errors. When both NumPy test and CuPy test raises the |
| same type of errors, and the type of the errors is specified with |
| this argument, the errors are ignored and not raised. |
| If it is ``True`` all error types are acceptable. |
| If it is ``False`` no error is acceptable. |
| sp_name(str or None): Argument name whose value is either |
| ``scipy.sparse`` or ``cupyx.scipy.sparse`` module. If ``None``, no |
| argument is given for the modules. |
| scipy_name(str or None): Argument name whose value is either ``scipy`` |
| or ``cupyx.scipy`` module. If ``None``, no argument is given for |
| the modules. |
| strides_check(bool): If ``True``, consistency of strides is also |
| checked. |
| |
| Decorated test fixture is required to return the same arrays |
| in the sense of :func:`numpy_cupy_array_equal` |
| (except the type of array module) even if ``xp`` is ``numpy`` or ``cupy``. |
| |
| .. seealso:: :func:`cupy.testing.assert_array_equal` |
| """ |
| def check_func(x, y): |
| _array.assert_array_equal( |
| x, y, err_msg, verbose, strides_check=strides_check) |
| return _make_decorator(check_func, name, type_check, False, |
| accept_error, sp_name, scipy_name) |
|
|
|
|
| def numpy_cupy_array_list_equal( |
| err_msg='', verbose=True, name='xp', sp_name=None, scipy_name=None): |
| """Decorator that checks the resulting lists of NumPy and CuPy's one are equal. |
| |
| Args: |
| err_msg(str): The error message to be printed in case of failure. |
| verbose(bool): If ``True``, the conflicting values are appended |
| to the error message. |
| name(str): Argument name whose value is either |
| ``numpy`` or ``cupy`` module. |
| sp_name(str or None): Argument name whose value is either |
| ``scipy.sparse`` or ``cupyx.scipy.sparse`` module. If ``None``, no |
| argument is given for the modules. |
| scipy_name(str or None): Argument name whose value is either ``scipy`` |
| or ``cupyx.scipy`` module. If ``None``, no argument is given for |
| the modules. |
| |
| Decorated test fixture is required to return the same list of arrays |
| (except the type of array module) even if ``xp`` is ``numpy`` or ``cupy``. |
| |
| .. seealso:: :func:`cupy.testing.assert_array_list_equal` |
| """ |
| warnings.warn( |
| 'numpy_cupy_array_list_equal is deprecated.' |
| ' Use numpy_cupy_array_equal instead.', |
| DeprecationWarning) |
|
|
| def check_func(x, y): |
| _array.assert_array_equal(x, y, err_msg, verbose) |
| return _make_decorator(check_func, name, False, False, |
| False, sp_name, scipy_name) |
|
|
|
|
| def numpy_cupy_array_less(err_msg='', verbose=True, name='xp', |
| type_check=True, accept_error=False, sp_name=None, |
| scipy_name=None): |
| """Decorator that checks the CuPy result is less than NumPy result. |
| |
| Args: |
| err_msg(str): The error message to be printed in case of failure. |
| verbose(bool): If ``True``, the conflicting values are |
| appended to the error message. |
| name(str): Argument name whose value is either |
| ``numpy`` or ``cupy`` module. |
| type_check(bool): If ``True``, consistency of dtype is also checked. |
| accept_error(bool, Exception or tuple of Exception): Specify |
| acceptable errors. When both NumPy test and CuPy test raises the |
| same type of errors, and the type of the errors is specified with |
| this argument, the errors are ignored and not raised. |
| If it is ``True`` all error types are acceptable. |
| If it is ``False`` no error is acceptable. |
| sp_name(str or None): Argument name whose value is either |
| ``scipy.sparse`` or ``cupyx.scipy.sparse`` module. If ``None``, no |
| argument is given for the modules. |
| scipy_name(str or None): Argument name whose value is either ``scipy`` |
| or ``cupyx.scipy`` module. If ``None``, no argument is given for |
| the modules. |
| |
| Decorated test fixture is required to return the smaller array |
| when ``xp`` is ``cupy`` than the one when ``xp`` is ``numpy``. |
| |
| .. seealso:: :func:`cupy.testing.assert_array_less` |
| """ |
| def check_func(x, y): |
| _array.assert_array_less(x, y, err_msg, verbose) |
| return _make_decorator(check_func, name, type_check, False, |
| accept_error, sp_name, scipy_name) |
|
|
|
|
| def numpy_cupy_equal(name='xp', sp_name=None, scipy_name=None): |
| """Decorator that checks NumPy results are equal to CuPy ones. |
| |
| Args: |
| name(str): Argument name whose value is either |
| ``numpy`` or ``cupy`` module. |
| sp_name(str or None): Argument name whose value is either |
| ``scipy.sparse`` or ``cupyx.scipy.sparse`` module. If ``None``, no |
| argument is given for the modules. |
| scipy_name(str or None): Argument name whose value is either ``scipy`` |
| or ``cupyx.scipy`` module. If ``None``, no argument is given for |
| the modules. |
| |
| Decorated test fixture is required to return the same results |
| even if ``xp`` is ``numpy`` or ``cupy``. |
| """ |
| def decorator(impl): |
| @_wraps_partial_xp(impl, name, sp_name, scipy_name) |
| def test_func(*args, **kw): |
| |
| ( |
| cupy_result, cupy_error, |
| numpy_result, numpy_error) = ( |
| _call_func_numpy_cupy( |
| impl, args, kw, name, sp_name, scipy_name)) |
|
|
| if cupy_error or numpy_error: |
| _check_cupy_numpy_error( |
| cupy_error, numpy_error, |
| accept_error=False) |
| return |
|
|
| if cupy_result != numpy_result: |
| message = '''Results are not equal: |
| cupy: %s |
| numpy: %s''' % (str(cupy_result), str(numpy_result)) |
| raise AssertionError(message) |
| return test_func |
| return decorator |
|
|
|
|
| def numpy_cupy_raises(name='xp', sp_name=None, scipy_name=None, |
| accept_error=Exception): |
| """Decorator that checks the NumPy and CuPy throw same errors. |
| |
| Args: |
| name(str): Argument name whose value is either |
| ``numpy`` or ``cupy`` module. |
| sp_name(str or None): Argument name whose value is either |
| ``scipy.sparse`` or ``cupyx.scipy.sparse`` module. If ``None``, no |
| argument is given for the modules. |
| scipy_name(str or None): Argument name whose value is either ``scipy`` |
| or ``cupyx.scipy`` module. If ``None``, no argument is given for |
| the modules. |
| accept_error(bool, Exception or tuple of Exception): Specify |
| acceptable errors. When both NumPy test and CuPy test raises the |
| same type of errors, and the type of the errors is specified with |
| this argument, the errors are ignored and not raised. |
| If it is ``True`` all error types are acceptable. |
| If it is ``False`` no error is acceptable. |
| |
| Decorated test fixture is required throw same errors |
| even if ``xp`` is ``numpy`` or ``cupy``. |
| """ |
| warnings.warn( |
| 'cupy.testing.numpy_cupy_raises is deprecated.', |
| DeprecationWarning) |
|
|
| def decorator(impl): |
| @_wraps_partial_xp(impl, name, sp_name, scipy_name) |
| def test_func(*args, **kw): |
| |
| ( |
| cupy_result, cupy_error, |
| numpy_result, numpy_error) = ( |
| _call_func_numpy_cupy( |
| impl, args, kw, name, sp_name, scipy_name)) |
|
|
| _check_cupy_numpy_error(cupy_error, |
| numpy_error, |
| accept_error=accept_error) |
| return test_func |
| return decorator |
|
|
|
|
| def for_dtypes(dtypes, name='dtype'): |
| """Decorator for parameterized dtype test. |
| |
| Args: |
| dtypes(list of dtypes): dtypes to be tested. |
| name(str): Argument name to which specified dtypes are passed. |
| |
| This decorator adds a keyword argument specified by ``name`` |
| to the test fixture. Then, it runs the fixtures in parallel |
| by passing the each element of ``dtypes`` to the named |
| argument. |
| """ |
| def decorator(impl): |
| @_wraps_partial(impl, name) |
| def test_func(*args, **kw): |
| for dtype in dtypes: |
| try: |
| kw[name] = numpy.dtype(dtype).type |
| impl(*args, **kw) |
| except _skip_classes as e: |
| print('skipped: {} = {} ({})'.format(name, dtype, e)) |
| except Exception: |
| print(name, 'is', dtype) |
| raise |
|
|
| return test_func |
| return decorator |
|
|
|
|
| _complex_dtypes = (numpy.complex64, numpy.complex128) |
| _regular_float_dtypes = (numpy.float64, numpy.float32) |
| _float_dtypes = _regular_float_dtypes + (numpy.float16,) |
| _signed_dtypes = tuple(numpy.dtype(i).type for i in 'bhilq') |
| _unsigned_dtypes = tuple(numpy.dtype(i).type for i in 'BHILQ') |
| _int_dtypes = _signed_dtypes + _unsigned_dtypes |
| _int_bool_dtypes = _int_dtypes + (numpy.bool_,) |
| _regular_dtypes = _regular_float_dtypes + _int_bool_dtypes |
| _dtypes = _float_dtypes + _int_bool_dtypes |
|
|
|
|
| def _make_all_dtypes(no_float16, no_bool, no_complex): |
| if no_float16: |
| dtypes = _regular_float_dtypes |
| else: |
| dtypes = _float_dtypes |
|
|
| if no_bool: |
| dtypes += _int_dtypes |
| else: |
| dtypes += _int_bool_dtypes |
|
|
| if not no_complex: |
| dtypes += _complex_dtypes |
|
|
| return dtypes |
|
|
|
|
| def for_all_dtypes(name='dtype', no_float16=False, no_bool=False, |
| no_complex=False): |
| """Decorator that checks the fixture with all dtypes. |
| |
| Args: |
| name(str): Argument name to which specified dtypes are passed. |
| no_float16(bool): If ``True``, ``numpy.float16`` is |
| omitted from candidate dtypes. |
| no_bool(bool): If ``True``, ``numpy.bool_`` is |
| omitted from candidate dtypes. |
| no_complex(bool): If ``True``, ``numpy.complex64`` and |
| ``numpy.complex128`` are omitted from candidate dtypes. |
| |
| dtypes to be tested: ``numpy.complex64`` (optional), |
| ``numpy.complex128`` (optional), |
| ``numpy.float16`` (optional), ``numpy.float32``, |
| ``numpy.float64``, ``numpy.dtype('b')``, ``numpy.dtype('h')``, |
| ``numpy.dtype('i')``, ``numpy.dtype('l')``, ``numpy.dtype('q')``, |
| ``numpy.dtype('B')``, ``numpy.dtype('H')``, ``numpy.dtype('I')``, |
| ``numpy.dtype('L')``, ``numpy.dtype('Q')``, and ``numpy.bool_`` (optional). |
| |
| The usage is as follows. |
| This test fixture checks if ``cPickle`` successfully reconstructs |
| :class:`cupy.ndarray` for various dtypes. |
| ``dtype`` is an argument inserted by the decorator. |
| |
| >>> import unittest |
| >>> from cupy import testing |
| >>> class TestNpz(unittest.TestCase): |
| ... |
| ... @testing.for_all_dtypes() |
| ... def test_pickle(self, dtype): |
| ... a = testing.shaped_arange((2, 3, 4), dtype=dtype) |
| ... s = pickle.dumps(a) |
| ... b = pickle.loads(s) |
| ... testing.assert_array_equal(a, b) |
| |
| Typically, we use this decorator in combination with |
| decorators that check consistency between NumPy and CuPy like |
| :func:`cupy.testing.numpy_cupy_allclose`. |
| The following is such an example. |
| |
| >>> import unittest |
| >>> from cupy import testing |
| >>> class TestMean(unittest.TestCase): |
| ... |
| ... @testing.for_all_dtypes() |
| ... @testing.numpy_cupy_allclose() |
| ... def test_mean_all(self, xp, dtype): |
| ... a = testing.shaped_arange((2, 3), xp, dtype) |
| ... return a.mean() |
| |
| .. seealso:: :func:`cupy.testing.for_dtypes` |
| """ |
| return for_dtypes(_make_all_dtypes(no_float16, no_bool, no_complex), |
| name=name) |
|
|
|
|
| def for_float_dtypes(name='dtype', no_float16=False): |
| """Decorator that checks the fixture with float dtypes. |
| |
| Args: |
| name(str): Argument name to which specified dtypes are passed. |
| no_float16(bool): If ``True``, ``numpy.float16`` is |
| omitted from candidate dtypes. |
| |
| dtypes to be tested are ``numpy.float16`` (optional), ``numpy.float32``, |
| and ``numpy.float64``. |
| |
| .. seealso:: :func:`cupy.testing.for_dtypes`, |
| :func:`cupy.testing.for_all_dtypes` |
| """ |
| if no_float16: |
| return for_dtypes(_regular_float_dtypes, name=name) |
| else: |
| return for_dtypes(_float_dtypes, name=name) |
|
|
|
|
| def for_signed_dtypes(name='dtype'): |
| """Decorator that checks the fixture with signed dtypes. |
| |
| Args: |
| name(str): Argument name to which specified dtypes are passed. |
| |
| dtypes to be tested are ``numpy.dtype('b')``, ``numpy.dtype('h')``, |
| ``numpy.dtype('i')``, ``numpy.dtype('l')``, and ``numpy.dtype('q')``. |
| |
| .. seealso:: :func:`cupy.testing.for_dtypes`, |
| :func:`cupy.testing.for_all_dtypes` |
| """ |
| return for_dtypes(_signed_dtypes, name=name) |
|
|
|
|
| def for_unsigned_dtypes(name='dtype'): |
| """Decorator that checks the fixture with unsigned dtypes. |
| |
| Args: |
| name(str): Argument name to which specified dtypes are passed. |
| |
| dtypes to be tested are ``numpy.dtype('B')``, ``numpy.dtype('H')``, |
| |
| ``numpy.dtype('I')``, ``numpy.dtype('L')``, and ``numpy.dtype('Q')``. |
| |
| .. seealso:: :func:`cupy.testing.for_dtypes`, |
| :func:`cupy.testing.for_all_dtypes` |
| """ |
| return for_dtypes(_unsigned_dtypes, name=name) |
|
|
|
|
| def for_int_dtypes(name='dtype', no_bool=False): |
| """Decorator that checks the fixture with integer and optionally bool dtypes. |
| |
| Args: |
| name(str): Argument name to which specified dtypes are passed. |
| no_bool(bool): If ``True``, ``numpy.bool_`` is |
| omitted from candidate dtypes. |
| |
| dtypes to be tested are ``numpy.dtype('b')``, ``numpy.dtype('h')``, |
| ``numpy.dtype('i')``, ``numpy.dtype('l')``, ``numpy.dtype('q')``, |
| ``numpy.dtype('B')``, ``numpy.dtype('H')``, ``numpy.dtype('I')``, |
| ``numpy.dtype('L')``, ``numpy.dtype('Q')``, and ``numpy.bool_`` (optional). |
| |
| .. seealso:: :func:`cupy.testing.for_dtypes`, |
| :func:`cupy.testing.for_all_dtypes` |
| """ |
| if no_bool: |
| return for_dtypes(_int_dtypes, name=name) |
| else: |
| return for_dtypes(_int_bool_dtypes, name=name) |
|
|
|
|
| def for_complex_dtypes(name='dtype'): |
| """Decorator that checks the fixture with complex dtypes. |
| |
| Args: |
| name(str): Argument name to which specified dtypes are passed. |
| |
| dtypes to be tested are ``numpy.complex64`` and ``numpy.complex128``. |
| |
| .. seealso:: :func:`cupy.testing.for_dtypes`, |
| :func:`cupy.testing.for_all_dtypes` |
| """ |
| return for_dtypes(_complex_dtypes, name=name) |
|
|
|
|
| def for_dtypes_combination(types, names=('dtype',), full=None): |
| """Decorator that checks the fixture with a product set of dtypes. |
| |
| Args: |
| types(list of dtypes): dtypes to be tested. |
| names(list of str): Argument names to which dtypes are passed. |
| full(bool): If ``True``, then all combinations |
| of dtypes will be tested. |
| Otherwise, the subset of combinations will be tested |
| (see the description below). |
| |
| Decorator adds the keyword arguments specified by ``names`` |
| to the test fixture. Then, it runs the fixtures in parallel |
| with passing (possibly a subset of) the product set of dtypes. |
| The range of dtypes is specified by ``types``. |
| |
| The combination of dtypes to be tested changes depending |
| on the option ``full``. If ``full`` is ``True``, |
| all combinations of ``types`` are tested. |
| Sometimes, such an exhaustive test can be costly. |
| So, if ``full`` is ``False``, only a subset of possible combinations |
| is randomly sampled. If ``full`` is ``None``, the behavior is |
| determined by an environment variable ``CUPY_TEST_FULL_COMBINATION``. |
| If the value is set to ``'1'``, it behaves as if ``full=True``, and |
| otherwise ``full=False``. |
| """ |
| types = list(types) |
|
|
| if len(types) == 1: |
| name, = names |
| return for_dtypes(types, name) |
|
|
| if full is None: |
| full = int(os.environ.get('CUPY_TEST_FULL_COMBINATION', '0')) != 0 |
|
|
| if full: |
| combination = _parameterized.product({name: types for name in names}) |
| else: |
| ts = [] |
| for _ in range(len(names)): |
| |
| shuffled_types = types[:] |
| random.shuffle(shuffled_types) |
| ts.append(types + shuffled_types) |
|
|
| combination = [tuple(zip(names, typs)) for typs in zip(*ts)] |
| |
| combination = [dict(assoc_list) for assoc_list in set(combination)] |
|
|
| def decorator(impl): |
| @_wraps_partial(impl, *names) |
| def test_func(*args, **kw): |
| for dtypes in combination: |
| kw_copy = kw.copy() |
| kw_copy.update(dtypes) |
|
|
| try: |
| impl(*args, **kw_copy) |
| except _skip_classes as e: |
| msg = ', '.join( |
| '{} = {}'.format(name, dtype) |
| for name, dtype in dtypes.items()) |
| print('skipped: {} ({})'.format(msg, e)) |
| except Exception: |
| print(dtypes) |
| raise |
|
|
| return test_func |
| return decorator |
|
|
|
|
| def for_all_dtypes_combination(names=('dtyes',), |
| no_float16=False, no_bool=False, full=None, |
| no_complex=False): |
| """Decorator that checks the fixture with a product set of all dtypes. |
| |
| Args: |
| names(list of str): Argument names to which dtypes are passed. |
| no_float16(bool): If ``True``, ``numpy.float16`` is |
| omitted from candidate dtypes. |
| no_bool(bool): If ``True``, ``numpy.bool_`` is |
| omitted from candidate dtypes. |
| full(bool): If ``True``, then all combinations of dtypes |
| will be tested. |
| Otherwise, the subset of combinations will be tested |
| (see description in :func:`cupy.testing.for_dtypes_combination`). |
| no_complex(bool): If, True, ``numpy.complex64`` and |
| ``numpy.complex128`` are omitted from candidate dtypes. |
| |
| .. seealso:: :func:`cupy.testing.for_dtypes_combination` |
| """ |
| types = _make_all_dtypes(no_float16, no_bool, no_complex) |
| return for_dtypes_combination(types, names, full) |
|
|
|
|
| def for_signed_dtypes_combination(names=('dtype',), full=None): |
| """Decorator for parameterized test w.r.t. the product set of signed dtypes. |
| |
| Args: |
| names(list of str): Argument names to which dtypes are passed. |
| full(bool): If ``True``, then all combinations of dtypes |
| will be tested. |
| Otherwise, the subset of combinations will be tested |
| (see description in :func:`cupy.testing.for_dtypes_combination`). |
| |
| .. seealso:: :func:`cupy.testing.for_dtypes_combination` |
| """ |
| return for_dtypes_combination(_signed_dtypes, names=names, full=full) |
|
|
|
|
| def for_unsigned_dtypes_combination(names=('dtype',), full=None): |
| """Decorator for parameterized test w.r.t. the product set of unsigned dtypes. |
| |
| Args: |
| names(list of str): Argument names to which dtypes are passed. |
| full(bool): If ``True``, then all combinations of dtypes |
| will be tested. |
| Otherwise, the subset of combinations will be tested |
| (see description in :func:`cupy.testing.for_dtypes_combination`). |
| |
| .. seealso:: :func:`cupy.testing.for_dtypes_combination` |
| """ |
| return for_dtypes_combination(_unsigned_dtypes, names=names, full=full) |
|
|
|
|
| def for_int_dtypes_combination(names=('dtype',), no_bool=False, full=None): |
| """Decorator for parameterized test w.r.t. the product set of int and boolean. |
| |
| Args: |
| names(list of str): Argument names to which dtypes are passed. |
| no_bool(bool): If ``True``, ``numpy.bool_`` is |
| omitted from candidate dtypes. |
| full(bool): If ``True``, then all combinations of dtypes |
| will be tested. |
| Otherwise, the subset of combinations will be tested |
| (see description in :func:`cupy.testing.for_dtypes_combination`). |
| |
| .. seealso:: :func:`cupy.testing.for_dtypes_combination` |
| """ |
| if no_bool: |
| types = _int_dtypes |
| else: |
| types = _int_bool_dtypes |
| return for_dtypes_combination(types, names, full) |
|
|
|
|
| def for_orders(orders, name='order'): |
| """Decorator to parameterize tests with order. |
| |
| Args: |
| orders(list of order): orders to be tested. |
| name(str): Argument name to which the specified order is passed. |
| |
| This decorator adds a keyword argument specified by ``name`` |
| to the test fixtures. Then, the fixtures run by passing each element of |
| ``orders`` to the named argument. |
| |
| """ |
| def decorator(impl): |
| @_wraps_partial(impl, name) |
| def test_func(*args, **kw): |
| for order in orders: |
| try: |
| kw[name] = order |
| impl(*args, **kw) |
| except Exception: |
| print(name, 'is', order) |
| raise |
|
|
| return test_func |
| return decorator |
|
|
|
|
| def for_CF_orders(name='order'): |
| """Decorator that checks the fixture with orders 'C' and 'F'. |
| |
| Args: |
| name(str): Argument name to which the specified order is passed. |
| |
| .. seealso:: :func:`cupy.testing.for_all_dtypes` |
| |
| """ |
| return for_orders([None, 'C', 'F', 'c', 'f'], name) |
|
|
|
|
| def for_contiguous_axes(name='axis'): |
| '''Decorator for parametrizing tests with possible contiguous axes. |
| |
| Args: |
| name(str): Argument name to which specified axis are passed. |
| |
| .. note:: |
| 1. Adapted from tests/cupy_tests/fft_tests/test_fft.py. |
| 2. Example: for ``shape = (1, 2, 3)``, the tested axes are |
| ``[(2,), (1, 2), (0, 1, 2)]`` for the C order, and |
| ``[(0,), (0, 1), (0, 1, 2)]`` for the F order. |
| ''' |
| def decorator(impl): |
| @_wraps_partial(impl, name) |
| def test_func(self, *args, **kw): |
| ndim = len(self.shape) |
| order = self.order |
| for i in range(ndim): |
| a = () |
| if order in ('c', 'C'): |
| for j in range(ndim-1, i-1, -1): |
| a = (j,) + a |
| elif order in ('f', 'F'): |
| for j in range(0, i+1): |
| a = a + (j,) |
| else: |
| raise ValueError('Please specify the array order.') |
| try: |
| kw[name] = a |
| impl(self, *args, **kw) |
| except Exception: |
| print(name, 'is', a, ', ndim is', ndim, ', shape is', |
| self.shape, ', order is', order) |
| raise |
| return test_func |
| return decorator |
|
|