| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- """
- Testing utilities.
- """
- import os
- import platform
- import re
- import struct
- import sys
- import functools
- import inspect
- from tempfile import NamedTemporaryFile
- import numpy as np
- from numpy import testing
- from numpy.testing import (
- TestCase,
- assert_,
- assert_warns,
- assert_no_warnings,
- assert_equal,
- assert_almost_equal,
- assert_array_equal,
- assert_allclose,
- assert_array_almost_equal,
- assert_array_almost_equal_nulp,
- assert_array_less,
- )
- from .. import data, io
- from ..data._fetchers import _fetch
- from ..util import img_as_uint, img_as_float, img_as_int, img_as_ubyte
- from ._warnings import expected_warnings
- from ._dependency_checks import is_wasm
- import pytest
- skipif = pytest.mark.skipif
- xfail = pytest.mark.xfail
- parametrize = pytest.mark.parametrize
- raises = pytest.raises
- fixture = pytest.fixture
- SKIP_RE = re.compile(r"(\s*>>>.*?)(\s*)#\s*skip\s+if\s+(.*)$")
- # true if python is running in 32bit mode
- # Calculate the size of a void * pointer in bits
- # https://docs.python.org/3/library/struct.html
- arch32 = struct.calcsize("P") * 8 == 32
- def assert_less(a, b, msg=None):
- message = f"{a!r} is not lower than {b!r}"
- if msg is not None:
- message += ": " + msg
- assert a < b, message
- def assert_greater(a, b, msg=None):
- message = f"{a!r} is not greater than {b!r}"
- if msg is not None:
- message += ": " + msg
- assert a > b, message
- def doctest_skip_parser(func):
- """Decorator replaces custom skip test markup in doctests
- Say a function has a docstring::
- >>> something, HAVE_AMODULE, HAVE_BMODULE = 0, False, False
- >>> something # skip if not HAVE_AMODULE
- 0
- >>> something # skip if HAVE_BMODULE
- 0
- This decorator will evaluate the expression after ``skip if``. If this
- evaluates to True, then the comment is replaced by ``# doctest: +SKIP``. If
- False, then the comment is just removed. The expression is evaluated in the
- ``globals`` scope of `func`.
- For example, if the module global ``HAVE_AMODULE`` is False, and module
- global ``HAVE_BMODULE`` is False, the returned function will have docstring::
- >>> something # doctest: +SKIP
- >>> something + else # doctest: +SKIP
- >>> something # doctest: +SKIP
- """
- lines = func.__doc__.split('\n')
- new_lines = []
- for line in lines:
- match = SKIP_RE.match(line)
- if match is None:
- new_lines.append(line)
- continue
- code, space, expr = match.groups()
- try:
- # Works as a function decorator
- if eval(expr, func.__globals__):
- code = code + space + "# doctest: +SKIP"
- except AttributeError:
- # Works as a class decorator
- if eval(expr, func.__init__.__globals__):
- code = code + space + "# doctest: +SKIP"
- new_lines.append(code)
- func.__doc__ = "\n".join(new_lines)
- return func
- def roundtrip(image, plugin, suffix):
- """Save and read an image using a specified plugin"""
- if '.' not in suffix:
- suffix = '.' + suffix
- with NamedTemporaryFile(suffix=suffix, delete=False) as temp_file:
- fname = temp_file.name
- io.imsave(fname, image, plugin=plugin)
- new = io.imread(fname, plugin=plugin)
- try:
- os.remove(fname)
- except Exception:
- pass
- return new
- def color_check(plugin, fmt='png'):
- """Check roundtrip behavior for color images.
- All major input types should be handled as ubytes and read
- back correctly.
- """
- img = img_as_ubyte(data.chelsea())
- r1 = roundtrip(img, plugin, fmt)
- testing.assert_allclose(img, r1)
- img2 = img > 128
- r2 = roundtrip(img2, plugin, fmt)
- testing.assert_allclose(img2, r2.astype(bool))
- img3 = img_as_float(img)
- r3 = roundtrip(img3, plugin, fmt)
- testing.assert_allclose(r3, img)
- img4 = img_as_int(img)
- if fmt.lower() in (('tif', 'tiff')):
- img4 -= 100
- r4 = roundtrip(img4, plugin, fmt)
- testing.assert_allclose(r4, img4)
- else:
- r4 = roundtrip(img4, plugin, fmt)
- testing.assert_allclose(r4, img_as_ubyte(img4))
- img5 = img_as_uint(img)
- r5 = roundtrip(img5, plugin, fmt)
- testing.assert_allclose(r5, img)
- def mono_check(plugin, fmt='png'):
- """Check the roundtrip behavior for images that support most types.
- All major input types should be handled.
- """
- img = img_as_ubyte(data.moon())
- r1 = roundtrip(img, plugin, fmt)
- testing.assert_allclose(img, r1)
- img2 = img > 128
- r2 = roundtrip(img2, plugin, fmt)
- testing.assert_allclose(img2, r2.astype(bool))
- img3 = img_as_float(img)
- r3 = roundtrip(img3, plugin, fmt)
- if r3.dtype.kind == 'f':
- testing.assert_allclose(img3, r3)
- else:
- testing.assert_allclose(r3, img_as_uint(img))
- img4 = img_as_int(img)
- if fmt.lower() in (('tif', 'tiff')):
- img4 -= 100
- r4 = roundtrip(img4, plugin, fmt)
- testing.assert_allclose(r4, img4)
- else:
- r4 = roundtrip(img4, plugin, fmt)
- testing.assert_allclose(r4, img_as_uint(img4))
- img5 = img_as_uint(img)
- r5 = roundtrip(img5, plugin, fmt)
- testing.assert_allclose(r5, img5)
- def fetch(data_filename, prefix=None):
- """Attempt to fetch data, but if unavailable, skip the tests.
- Parameters
- ----------
- data_filename : str
- File path in the scikit-image repo tree, e.g.,
- 'restoration/camera_rl.npy', possibly pointing to a remote location.
- prefix : str, optional
- If None, `data_filename` is prefixed by 'src/skimage'
- If 'tests', `data_filename` is prefixed by 'tests/skimage'.
- Returns
- -------
- file_path : str
- Path of the local file, possibly pointing to a remote location.
- """
- try:
- return _fetch(data_filename, prefix=prefix)
- except (ConnectionError, ModuleNotFoundError):
- pytest.skip(f'Unable to download {data_filename}', allow_module_level=True)
- # Ref: about the lack of threading support in WASM, please see
- # https://github.com/pyodide/pyodide/issues/237
- def run_in_parallel(workers=2, warnings_matching=None):
- """Decorator to run the same function multiple times in parallel.
- This decorator is useful to ensure that separate threads execute
- concurrently and correctly while releasing the GIL.
- It is currently skipped when running on WASM-based platforms, as
- the threading module is not supported.
- Parameters
- ----------
- workers : int, optional
- The number of times the function is run in parallel.
- warnings_matching : list or None
- This parameter is passed on to `expected_warnings` so as not to have
- race conditions with the warnings filters. A single
- `expected_warnings` context manager is used for all threads.
- If None, then no warnings are checked.
- """
- assert workers > 0
- def wrapper(func):
- if is_wasm:
- # Threading isn't supported on WASM, return early
- return func
- import threading
- @functools.wraps(func)
- def inner(*args, **kwargs):
- with expected_warnings(warnings_matching):
- threads = []
- for i in range(workers - 1):
- thread = threading.Thread(target=func, args=args, kwargs=kwargs)
- threads.append(thread)
- for thread in threads:
- thread.start()
- func(*args, **kwargs)
- for thread in threads:
- thread.join()
- return inner
- return wrapper
- def assert_stacklevel(warnings, *, offset=-1):
- """Assert correct stacklevel of captured warnings.
- When scikit-image raises warnings, the stacklevel should ideally be set
- so that the origin of the warnings will point to the public function
- that was called by the user and not necessarily the very place where the
- warnings were emitted (which may be inside some internal function).
- This utility function helps with checking that
- the stacklevel was set correctly on warnings captured by `pytest.warns`.
- Parameters
- ----------
- warnings : collections.abc.Iterable[warning.WarningMessage]
- Warnings that were captured by `pytest.warns`.
- offset : int, optional
- Offset from the line this function is called to the line were the
- warning is supposed to originate from. For multiline calls, the
- first line is relevant. Defaults to -1 which corresponds to the line
- right above the one where this function is called.
- Raises
- ------
- AssertionError
- If a warning in `warnings` does not match the expected line number or
- file name.
- Examples
- --------
- >>> def test_something():
- ... with pytest.warns(UserWarning, match="some message") as record:
- ... something_raising_a_warning()
- ... assert_stacklevel(record)
- ...
- >>> def test_another_thing():
- ... with pytest.warns(UserWarning, match="some message") as record:
- ... iam_raising_many_warnings(
- ... "A long argument that forces the call to wrap."
- ... )
- ... assert_stacklevel(record, offset=-3)
- """
- __tracebackhide__ = True # Hide traceback for py.test
- frame = inspect.stack()[1].frame # 0 is current frame, 1 is outer frame
- line_number = frame.f_lineno + offset
- filename = frame.f_code.co_filename
- expected = f"{filename}:{line_number}"
- for warning in warnings:
- actual = f"{warning.filename}:{warning.lineno}"
- msg = (
- "Warning with wrong stacklevel:\n"
- f" Expected: {expected}\n"
- f" Actual: {actual}\n"
- f" {warning.category.__name__}: {warning.message}"
- )
- assert actual == expected, msg
|