Coverage for nilearn/_utils/testing.py: 28%
75 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
1"""Utilities for testing nilearn."""
3import gc
4import os
5import sys
6import tempfile
7import warnings
8from pathlib import Path
10import pytest
11from numpy import __version__ as np_version
13from nilearn._utils import compare_version
14from nilearn._utils.helpers import OPTIONAL_MATPLOTLIB_MIN_VERSION
16try:
17 from matplotlib import __version__ as mpl_version
18except ImportError:
19 mpl_version = OPTIONAL_MATPLOTLIB_MIN_VERSION
22# we use memory_profiler library for memory consumption checks
23try:
24 from memory_profiler import memory_usage
26 def with_memory_profiler(func):
27 """Use as a decorator to skip tests requiring memory_profiler."""
28 return func
30 def memory_used(func, *args, **kwargs):
31 """Compute memory usage when executing func."""
33 def func_3_times(*args, **kwargs):
34 for _ in range(3):
35 func(*args, **kwargs)
37 gc.collect()
38 mem_use = memory_usage((func_3_times, args, kwargs), interval=0.001)
39 return max(mem_use) - min(mem_use)
41except ImportError:
43 def with_memory_profiler(func): # noqa: ARG001
44 """Use as a decorator to skip tests requiring memory_profiler."""
46 def dummy_func():
47 pytest.skip("Test requires memory_profiler.")
49 return dummy_func
51 memory_usage = memory_used = None # type: ignore[assignment]
54def is_64bit() -> bool:
55 """Return True if python is run on 64bits."""
56 return sys.maxsize > 2**32
59def assert_memory_less_than(
60 memory_limit, tolerance, callable_obj, *args, **kwargs
61):
62 """Check memory consumption of a callable stays below a given limit.
64 Parameters
65 ----------
66 memory_limit : int
67 The expected memory limit in MiB.
69 tolerance : float
70 As memory_profiler results have some variability, this adds some
71 tolerance around memory_limit. Accepted values are in range [0.0, 1.0].
73 callable_obj : callable
74 The function to be called to check memory consumption.
76 """
77 mem_used = memory_used(callable_obj, *args, **kwargs)
79 if mem_used > memory_limit * (1 + tolerance):
80 raise ValueError(
81 f"Memory consumption measured ({mem_used:.2f} MiB) is "
82 f"greater than required memory limit ({memory_limit} MiB) within "
83 f"accepted tolerance ({tolerance * 100:.2f}%)."
84 )
86 # We are confident in memory_profiler measures above 100MiB.
87 # We raise an error if the measure is below the limit of 50MiB to avoid
88 # false positive.
89 if mem_used < 50:
90 raise ValueError(
91 "Memory profiler measured an untrustable memory "
92 f"consumption ({mem_used:.2f} MiB). The expected memory "
93 f"limit was {memory_limit:.2f} MiB. Try to bench with larger "
94 "objects (at least 100MiB in memory)."
95 )
98def serialize_niimg(img, gzipped=True):
99 """Serialize a Nifti1Image to nifti.
101 Serialize to .nii.gz if gzipped, else to .nii Returns a `bytes` object.
103 """
104 with tempfile.TemporaryDirectory() as tmp_dir:
105 tmp_dir = Path(tmp_dir)
106 file_path = tmp_dir / f"img.nii{'.gz' if gzipped else ''}"
107 img.to_filename(file_path)
108 with file_path.open("rb") as f:
109 return f.read()
112def write_imgs_to_path(
113 *imgs, file_path=None, create_files=True, use_wildcards=False
114):
115 """Write Nifti images on disk.
117 Write nifti images in a specified location.
119 Parameters
120 ----------
121 imgs : Nifti1Image
122 Several Nifti images. Every format understood by nibabel.save is
123 accepted.
125 file_path: pathlib.Path
126 Output directory
128 create_files : bool
129 If True, imgs are written on disk and filenames are returned. If
130 False, nothing is written, and imgs is returned as output. This is
131 useful to test the two cases (filename / Nifti1Image) in the same
132 loop.
134 use_wildcards : bool
135 If True, and create_files is True, imgs are written on disk and a
136 matching glob is returned.
138 Returns
139 -------
140 filenames : string or list of strings
141 Filename(s) where input images have been written. If a single image
142 has been given as input, a single string is returned. Otherwise, a
143 list of string is returned.
145 """
146 if file_path is None:
147 file_path = Path.cwd()
149 if create_files:
150 filenames = []
151 prefix = "nilearn_"
152 suffix = ".nii"
154 with warnings.catch_warnings():
155 warnings.simplefilter("ignore", RuntimeWarning)
156 for i, img in enumerate(imgs):
157 filename = file_path / (prefix + str(i) + suffix)
158 filenames.append(str(filename))
159 img.to_filename(filename)
160 del img
162 if use_wildcards:
163 return str(file_path / f"{prefix}*{suffix}")
164 if len(filenames) == 1:
165 return filenames[0]
166 return filenames
168 elif len(imgs) == 1:
169 return imgs[0]
170 else:
171 return imgs
174def are_tests_running():
175 """Return whether we are running the pytest test loader."""
176 # https://docs.pytest.org/en/stable/example/simple.html#detect-if-running-from-within-a-pytest-run
177 return os.environ.get("PYTEST_VERSION") is not None
180def skip_if_running_tests(msg=""):
181 """Raise a SkipTest if we appear to be running the pytest test loader.
183 Parameters
184 ----------
185 msg : string, optional
186 The message issued when a test is skipped.
188 """
189 if are_tests_running():
190 pytest.skip(msg, allow_module_level=True)
193def on_windows_with_old_mpl_and_new_numpy():
194 return (
195 compare_version(np_version, ">", "1.26.4")
196 and compare_version(mpl_version, "<", "3.8.0")
197 and os.name == "nt"
198 )