From a29f32cd4647a440f4ac7d78f2c379e5e0819e61 Mon Sep 17 00:00:00 2001 From: Alex Valcourt Caron Date: Mon, 19 Dec 2022 14:03:04 -0500 Subject: [PATCH 01/18] Make apply-mocks fixture usable by all tests. Start hooks and fixtures modules for pytest, loaded using conftest.py in the script tests directory. --- scilpy/tests/__init__.py | 1 + scilpy/tests/checks.py | 15 +++++++++++++++ scilpy/tests/fixtures.py | 6 ++++++ scilpy/tests/hooks.py | 10 ++++++++++ scripts/tests/conftest.py | 6 ++++++ 5 files changed, 38 insertions(+) create mode 100644 scilpy/tests/__init__.py create mode 100644 scilpy/tests/checks.py create mode 100644 scilpy/tests/fixtures.py create mode 100644 scilpy/tests/hooks.py create mode 100644 scripts/tests/conftest.py diff --git a/scilpy/tests/__init__.py b/scilpy/tests/__init__.py new file mode 100644 index 000000000..991aa1a51 --- /dev/null +++ b/scilpy/tests/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scilpy/tests/checks.py b/scilpy/tests/checks.py new file mode 100644 index 000000000..59e2ce570 --- /dev/null +++ b/scilpy/tests/checks.py @@ -0,0 +1,15 @@ +import numpy as np + + +def assert_images_close(img1, img2): + dtype = img1.header.get_data_dtype() + + assert np.allclose(img1.affine, img2.affine), "Images affines don't match" + + assert np.allclose( + img1.get_fdata(dtype=dtype), img2.get_fdata(dtype=dtype)), \ + "Images data don't match. MSE : {} | max SE : {}".format( + np.mean((img1.get_fdata(dtype=dtype) - + img2.get_fdata(dtype=dtype)) ** 2.), + np.max((img1.get_fdata(dtype=dtype) - + img2.get_fdata(dtype=dtype)) ** 2.)) diff --git a/scilpy/tests/fixtures.py b/scilpy/tests/fixtures.py new file mode 100644 index 000000000..b3f1bffc3 --- /dev/null +++ b/scilpy/tests/fixtures.py @@ -0,0 +1,6 @@ +import pytest + + +@pytest.fixture(scope="session") +def apply_mocks(request): + return request.config.getoption("--apply-mocks") diff --git a/scilpy/tests/hooks.py b/scilpy/tests/hooks.py new file mode 100644 index 000000000..005b9b8be --- /dev/null +++ b/scilpy/tests/hooks.py @@ -0,0 +1,10 @@ +import pytest + + +def pytest_addoption(parser): + parser.addoption( + "--apply-mocks", + action="store_true", + help="Apply mocks to accelerate tests and " + "prevent testing external dependencies" + ) diff --git a/scripts/tests/conftest.py b/scripts/tests/conftest.py new file mode 100644 index 000000000..556fecf63 --- /dev/null +++ b/scripts/tests/conftest.py @@ -0,0 +1,6 @@ +import pytest + +pytest_plugins = [ + "scilpy.tests.fixtures", + "scilpy.tests.hooks" +] From b6c5f9afdfdd043aa4dac7dae6189707596ada4e Mon Sep 17 00:00:00 2001 From: Alex Valcourt Caron Date: Mon, 16 Jan 2023 14:35:12 -0500 Subject: [PATCH 02/18] Remove debug lines --- ...execute_angle_aware_bilateral_filtering.py | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 scripts/tests/test_execute_angle_aware_bilateral_filtering.py diff --git a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py new file mode 100644 index 000000000..518429315 --- /dev/null +++ b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import nibabel as nib +import numpy as np +import os +import pytest +import tempfile + +from scilpy.io.fetcher import get_testing_files_dict, fetch_data, get_home +from scilpy.tests.checks import assert_images_close + + +# If they already exist, this only takes 5 seconds (check md5sum) +fetch_data(get_testing_files_dict(), keys=['fodf_filtering.zip']) +data_path = os.path.join(get_home(), 'fodf_filtering') +tmp_dir = tempfile.TemporaryDirectory() + + +@pytest.fixture(scope='function') +def filter_mock(mocker, apply_mocks, out_fodf): + if apply_mocks: + def _mock_side_effect(*args, **kwargs): + img = nib.load(out_fodf) + return img.get_fdata(dtype=np.float32) + + return mocker.patch( + "{}.{}".format( + "scripts.scil_execute_angle_aware_bilateral_filtering", + "angle_aware_bilateral_filtering"), + side_effect=_mock_side_effect, create=True) + + return None + + +def test_help_option(script_runner): + ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', + '--help') + assert ret.success + + +@pytest.mark.parametrize("in_fodf,out_fodf", + [[os.path.join(data_path, 'fodf_descoteaux07_sub.nii.gz'), + os.path.join(data_path, 'fodf_descoteaux07_sub_full.nii.gz')]], + scope='function') +def test_asym_basis_output( + script_runner, filter_mock, apply_mocks, in_fodf, out_fodf): + os.chdir(os.path.expanduser(tmp_dir.name)) + + ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', + in_fodf, + 'out_fodf1.nii.gz', + '--sphere', 'repulsion100', + '--sigma_angular', '1.0', + '--sigma_spatial', '1.0', + '--sigma_range', '1.0', + '--sh_basis', 'descoteaux07', + '--processes', '1', '-f', + print_result=True, shell=True) + + assert ret.success + + if apply_mocks: + filter_mock.assert_called_once() + + assert_images_close(nib.load(out_fodf), nib.load("out_fodf1.nii.gz")) + + +@pytest.mark.parametrize("in_fodf,out_fodf,sym_fodf", + [[os.path.join(data_path, "fodf_descoteaux07_sub.nii.gz"), + os.path.join(data_path, "fodf_descoteaux07_sub_full.nii.gz"), + os.path.join(data_path, "fodf_descoteaux07_sub_sym.nii.gz")]], + scope='function') +def test_sym_basis_output( + script_runner, filter_mock, apply_mocks, in_fodf, out_fodf, sym_fodf): + os.chdir(os.path.expanduser(tmp_dir.name)) + + ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', + in_fodf, + 'out_fodf2.nii.gz', + '--out_sym', 'out_sym.nii.gz', + '--sphere', 'repulsion100', + '--sigma_angular', '1.0', + '--sigma_spatial', '1.0', + '--sigma_range', '1.0', + '--sh_basis', 'descoteaux07', + '--processes', '1', '-f', + print_result=True, shell=True) + + assert ret.success + + if apply_mocks: + filter_mock.assert_called_once() + + assert_images_close(nib.load(sym_fodf), nib.load("out_sym.nii.gz")) + + +@pytest.mark.parametrize("in_fodf,out_fodf", + [[os.path.join(data_path, "fodf_descoteaux07_sub_full.nii.gz"), + os.path.join(data_path, "fodf_descoteaux07_sub_twice.nii.gz")]], + scope='function') +def test_asym_input(script_runner, filter_mock, apply_mocks, in_fodf, out_fodf): + os.chdir(os.path.expanduser(tmp_dir.name)) + + ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', + in_fodf, + 'out_fodf3.nii.gz', + '--sphere', 'repulsion100', + '--sigma_angular', '1.0', + '--sigma_spatial', '1.0', + '--sigma_range', '1.0', + '--sh_basis', 'descoteaux07', + '--processes', '1', '-f', + print_result=True, shell=True) + + assert ret.success + + if apply_mocks: + filter_mock.assert_called_once() + + assert_images_close(nib.load(out_fodf), nib.load("out_fodf3.nii.gz")) From 5effa905914815b01f1e069269c14e76e8572106 Mon Sep 17 00:00:00 2001 From: Alex Valcourt Caron Date: Mon, 19 Dec 2022 14:03:04 -0500 Subject: [PATCH 03/18] Make apply-mocks fixture usable by all tests. Start hooks and fixtures modules for pytest, loaded using conftest.py in the script tests directory. --- scripts/tests/test_execute_angle_aware_bilateral_filtering.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py index 518429315..ad64819f0 100644 --- a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py +++ b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py @@ -6,9 +6,11 @@ import os import pytest import tempfile +from shutil import copyfile from scilpy.io.fetcher import get_testing_files_dict, fetch_data, get_home from scilpy.tests.checks import assert_images_close +from scilpy.tests.checks import assert_images_close # If they already exist, this only takes 5 seconds (check md5sum) From ebf2a16648d025a3159f45ac4e540d39c4c6ce82 Mon Sep 17 00:00:00 2001 From: Alex Valcourt Caron Date: Mon, 16 Jan 2023 14:35:12 -0500 Subject: [PATCH 04/18] Remove debug lines --- scripts/tests/test_execute_angle_aware_bilateral_filtering.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py index ad64819f0..556d4b511 100644 --- a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py +++ b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py @@ -6,7 +6,6 @@ import os import pytest import tempfile -from shutil import copyfile from scilpy.io.fetcher import get_testing_files_dict, fetch_data, get_home from scilpy.tests.checks import assert_images_close From 6fbe06f31ecb6bbedf3daaaa63614efed8bf5e46 Mon Sep 17 00:00:00 2001 From: Alex Valcourt Caron Date: Tue, 21 Mar 2023 15:44:31 -0400 Subject: [PATCH 05/18] Push addoption inside local pytest plugin, making it accessible to all tests --- scilpy/tests/fixtures.py | 6 ------ scilpy/tests/{hooks.py => pytest_plugin.py} | 5 +++++ scripts/tests/conftest.py | 6 ------ setup.py | 3 ++- 4 files changed, 7 insertions(+), 13 deletions(-) delete mode 100644 scilpy/tests/fixtures.py rename scilpy/tests/{hooks.py => pytest_plugin.py} (67%) delete mode 100644 scripts/tests/conftest.py diff --git a/scilpy/tests/fixtures.py b/scilpy/tests/fixtures.py deleted file mode 100644 index b3f1bffc3..000000000 --- a/scilpy/tests/fixtures.py +++ /dev/null @@ -1,6 +0,0 @@ -import pytest - - -@pytest.fixture(scope="session") -def apply_mocks(request): - return request.config.getoption("--apply-mocks") diff --git a/scilpy/tests/hooks.py b/scilpy/tests/pytest_plugin.py similarity index 67% rename from scilpy/tests/hooks.py rename to scilpy/tests/pytest_plugin.py index 005b9b8be..454506c5f 100644 --- a/scilpy/tests/hooks.py +++ b/scilpy/tests/pytest_plugin.py @@ -8,3 +8,8 @@ def pytest_addoption(parser): help="Apply mocks to accelerate tests and " "prevent testing external dependencies" ) + + +@pytest.fixture(scope="session") +def apply_mocks(request): + return request.config.getoption("--apply-mocks") \ No newline at end of file diff --git a/scripts/tests/conftest.py b/scripts/tests/conftest.py deleted file mode 100644 index 556fecf63..000000000 --- a/scripts/tests/conftest.py +++ /dev/null @@ -1,6 +0,0 @@ -import pytest - -pytest_plugins = [ - "scilpy.tests.fixtures", - "scilpy.tests.hooks" -] diff --git a/setup.py b/setup.py index f8127082b..a9905c0ac 100644 --- a/setup.py +++ b/setup.py @@ -101,7 +101,8 @@ def run(self): 'console_scripts': ["{}=scripts.{}:main".format( os.path.basename(s), os.path.basename(s).split(".")[0]) for s in SCRIPTS] + - entry_point_legacy + entry_point_legacy, + 'pytest11': ["scilpy-testing=scilpy.tests.pytest_plugin"] }, data_files=[('data/LUT', ["data/LUT/freesurfer_desikan_killiany.json", From 96044fb2d68684f6a85162ef8bda916003e4f40b Mon Sep 17 00:00:00 2001 From: Alex Valcourt Caron Date: Mon, 23 Oct 2023 13:29:27 -0400 Subject: [PATCH 06/18] Add mocking util function. Pass mocking to jenkins --- scilpy/tests/mocking.py | 14 ++++++++++++++ scilpy/tests/pytest_plugin.py | 4 +++- ..._execute_angle_aware_bilateral_filtering.py | 18 +++++++----------- 3 files changed, 24 insertions(+), 12 deletions(-) create mode 100644 scilpy/tests/mocking.py diff --git a/scilpy/tests/mocking.py b/scilpy/tests/mocking.py new file mode 100644 index 000000000..61ce872dc --- /dev/null +++ b/scilpy/tests/mocking.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +import pytest + + +def create_mock(module_name, object_name, mocker, apply_mocks, + side_effect=None): + + if apply_mocks: + return mocker.patch( + "{}.{}".format(module_name, object_name), + side_effect=side_effect, create=True) + + return None diff --git a/scilpy/tests/pytest_plugin.py b/scilpy/tests/pytest_plugin.py index 454506c5f..d4926592b 100644 --- a/scilpy/tests/pytest_plugin.py +++ b/scilpy/tests/pytest_plugin.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + import pytest @@ -12,4 +14,4 @@ def pytest_addoption(parser): @pytest.fixture(scope="session") def apply_mocks(request): - return request.config.getoption("--apply-mocks") \ No newline at end of file + return request.config.getoption("--apply-mocks") diff --git a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py index 556d4b511..1cdfbd115 100644 --- a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py +++ b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py @@ -10,6 +10,7 @@ from scilpy.io.fetcher import get_testing_files_dict, fetch_data, get_home from scilpy.tests.checks import assert_images_close from scilpy.tests.checks import assert_images_close +from scilpy.tests.mocking import create_mock # If they already exist, this only takes 5 seconds (check md5sum) @@ -20,18 +21,13 @@ @pytest.fixture(scope='function') def filter_mock(mocker, apply_mocks, out_fodf): - if apply_mocks: - def _mock_side_effect(*args, **kwargs): - img = nib.load(out_fodf) - return img.get_fdata(dtype=np.float32) - - return mocker.patch( - "{}.{}".format( - "scripts.scil_execute_angle_aware_bilateral_filtering", - "angle_aware_bilateral_filtering"), - side_effect=_mock_side_effect, create=True) + def _mock_side_effect(*args, **kwargs): + img = nib.load(out_fodf) + return img.get_fdata(dtype=np.float32) - return None + return create_mock("scripts.scil_execute_angle_aware_bilateral_filtering", + "angle_aware_bilateral_filtering", mocker, apply_mocks, + side_effect=_mock_side_effect) def test_help_option(script_runner): From 3dee48a023907eef4e4307c18dfb8440f2e32f08 Mon Sep 17 00:00:00 2001 From: AlexVCaron Date: Thu, 14 Dec 2023 10:58:01 -0500 Subject: [PATCH 07/18] change mock infra, clean plugin --- scilpy/denoise/tests/fixtures/mocks.py | 18 ++++ scilpy/tests/mocking.py | 14 --- scilpy/tests/plugin.py | 90 +++++++++++++++++++ scilpy/tests/pytest_plugin.py | 17 ---- ...execute_angle_aware_bilateral_filtering.py | 34 +++---- setup.py | 2 +- 6 files changed, 121 insertions(+), 54 deletions(-) create mode 100644 scilpy/denoise/tests/fixtures/mocks.py delete mode 100644 scilpy/tests/mocking.py create mode 100644 scilpy/tests/plugin.py delete mode 100644 scilpy/tests/pytest_plugin.py diff --git a/scilpy/denoise/tests/fixtures/mocks.py b/scilpy/denoise/tests/fixtures/mocks.py new file mode 100644 index 000000000..9bb2a5f89 --- /dev/null +++ b/scilpy/denoise/tests/fixtures/mocks.py @@ -0,0 +1,18 @@ + +import nibabel as nib +import numpy as np +import pytest + + +@pytest.fixture(scope='function') +def bilateral_filtering(mock_creator, expected_results=None): + def _mock_side_effect(*args, **kwargs): + if expected_results is None or len(expected_results) == 0: + return None + + _out_odf_fname = expected_results[0] + return nib.load(_out_odf_fname).get_fdata(dtype=np.float32) + + return mock_creator("scripts.scil_execute_angle_aware_bilateral_filtering", + "angle_aware_bilateral_filtering", + side_effect=_mock_side_effect) diff --git a/scilpy/tests/mocking.py b/scilpy/tests/mocking.py deleted file mode 100644 index 61ce872dc..000000000 --- a/scilpy/tests/mocking.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- - -import pytest - - -def create_mock(module_name, object_name, mocker, apply_mocks, - side_effect=None): - - if apply_mocks: - return mocker.patch( - "{}.{}".format(module_name, object_name), - side_effect=side_effect, create=True) - - return None diff --git a/scilpy/tests/plugin.py b/scilpy/tests/plugin.py new file mode 100644 index 000000000..8626a758c --- /dev/null +++ b/scilpy/tests/plugin.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- + +from glob import glob +from os.path import realpath +import pytest + + +# Load mock modules from all library tests + +MOCK_MODULES = list(_module.replace("/", ".").replace(".py", "") + for _module in glob("**/tests/fixtures/mocks.py", + root_dir=realpath('.'), + recursive=True)) + +MOCK_NAMES = list(m.split(".")[-1] for m in MOCK_MODULES) + +LOADED_MOCK_MODULES = [] + + +# Helper function to select mocks + +def _get_active_mocks(include_list=None, exclude_list=None): + """ + Returns a list of all active mocks in the current session + + Parameters + ---------- + include_list: iterable or None + List of mocks to consider + + exclude_list: iterable or None + List of mocks to exclude from consideration + + Returns + ------- + list: list of modules containing mocks + """ + + def _active_names(): + def _exclude(_l): + if exclude_list is None: + return _l + return filter(lambda _i: _i not in exclude_list, _l) + + if include_list is None or len(include_list) == 0: + return [] + + if "all" in include_list: + return _exclude(MOCK_NAMES) + + return _exclude([_m for _m in include_list if _m in MOCK_NAMES]) + + return list(map(lambda _m: _m[1], + filter(lambda _m: _m[0] in _active_names(), + zip(MOCK_NAMES, MOCK_MODULES)))) + + +# Create hooks and fixtures to handle mocking from pytest command line + +def pytest_addoption(parser): + parser.addoption( + "--mocks", + nargs='+', + choices=["all"] + MOCK_NAMES, + help="Apply mocks to accelerate tests and " + "prevent testing external dependencies") + + +def pytest_configure(config): + _toggle_mocks = config.getoption("--mocks") + for _mock_mod in _get_active_mocks(_toggle_mocks): + config.pluginmanager.import_plugin(_mock_mod) + LOADED_MOCK_MODULES.append(_mock_mod) + + +@pytest.fixture +def mock_collector(request): + def _collector(mock_name): + _fixture = request(mock_name) + print(_fixture) + return _fixture.getfixturevalue() + return _collector + + +@pytest.fixture +def mock_creator(module_name, object_name, mocker, + side_effect=None): + + return mocker.patch("{}.{}".format(module_name, object_name), + side_effect=side_effect, create=True) diff --git a/scilpy/tests/pytest_plugin.py b/scilpy/tests/pytest_plugin.py deleted file mode 100644 index d4926592b..000000000 --- a/scilpy/tests/pytest_plugin.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- - -import pytest - - -def pytest_addoption(parser): - parser.addoption( - "--apply-mocks", - action="store_true", - help="Apply mocks to accelerate tests and " - "prevent testing external dependencies" - ) - - -@pytest.fixture(scope="session") -def apply_mocks(request): - return request.config.getoption("--apply-mocks") diff --git a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py index 1cdfbd115..b13c43fc1 100644 --- a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py +++ b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py @@ -9,8 +9,6 @@ from scilpy.io.fetcher import get_testing_files_dict, fetch_data, get_home from scilpy.tests.checks import assert_images_close -from scilpy.tests.checks import assert_images_close -from scilpy.tests.mocking import create_mock # If they already exist, this only takes 5 seconds (check md5sum) @@ -19,17 +17,6 @@ tmp_dir = tempfile.TemporaryDirectory() -@pytest.fixture(scope='function') -def filter_mock(mocker, apply_mocks, out_fodf): - def _mock_side_effect(*args, **kwargs): - img = nib.load(out_fodf) - return img.get_fdata(dtype=np.float32) - - return create_mock("scripts.scil_execute_angle_aware_bilateral_filtering", - "angle_aware_bilateral_filtering", mocker, apply_mocks, - side_effect=_mock_side_effect) - - def test_help_option(script_runner): ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', '--help') @@ -41,7 +28,7 @@ def test_help_option(script_runner): os.path.join(data_path, 'fodf_descoteaux07_sub_full.nii.gz')]], scope='function') def test_asym_basis_output( - script_runner, filter_mock, apply_mocks, in_fodf, out_fodf): + script_runner, in_fodf, out_fodf, mock_collector): os.chdir(os.path.expanduser(tmp_dir.name)) ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', @@ -57,8 +44,9 @@ def test_asym_basis_output( assert ret.success - if apply_mocks: - filter_mock.assert_called_once() + _mock = mock_collector("bilateral_filtering") + if _mock: + _mock.assert_called_once() assert_images_close(nib.load(out_fodf), nib.load("out_fodf1.nii.gz")) @@ -69,7 +57,7 @@ def test_asym_basis_output( os.path.join(data_path, "fodf_descoteaux07_sub_sym.nii.gz")]], scope='function') def test_sym_basis_output( - script_runner, filter_mock, apply_mocks, in_fodf, out_fodf, sym_fodf): + script_runner, in_fodf, out_fodf, sym_fodf, mock_collector): os.chdir(os.path.expanduser(tmp_dir.name)) ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', @@ -86,8 +74,9 @@ def test_sym_basis_output( assert ret.success - if apply_mocks: - filter_mock.assert_called_once() + _mock = mock_collector("bilateral_filtering") + if _mock: + _mock.assert_called_once() assert_images_close(nib.load(sym_fodf), nib.load("out_sym.nii.gz")) @@ -96,7 +85,7 @@ def test_sym_basis_output( [[os.path.join(data_path, "fodf_descoteaux07_sub_full.nii.gz"), os.path.join(data_path, "fodf_descoteaux07_sub_twice.nii.gz")]], scope='function') -def test_asym_input(script_runner, filter_mock, apply_mocks, in_fodf, out_fodf): +def test_asym_input(script_runner, in_fodf, out_fodf, mock_collector): os.chdir(os.path.expanduser(tmp_dir.name)) ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', @@ -112,7 +101,8 @@ def test_asym_input(script_runner, filter_mock, apply_mocks, in_fodf, out_fodf): assert ret.success - if apply_mocks: - filter_mock.assert_called_once() + _mock = mock_collector("bilateral_filtering") + if _mock: + _mock.assert_called_once() assert_images_close(nib.load(out_fodf), nib.load("out_fodf3.nii.gz")) diff --git a/setup.py b/setup.py index a9905c0ac..e1cc27000 100644 --- a/setup.py +++ b/setup.py @@ -102,7 +102,7 @@ def run(self): os.path.basename(s), os.path.basename(s).split(".")[0]) for s in SCRIPTS] + entry_point_legacy, - 'pytest11': ["scilpy-testing=scilpy.tests.pytest_plugin"] + 'pytest11': ["scilpy-test=scilpy.tests.plugin"] }, data_files=[('data/LUT', ["data/LUT/freesurfer_desikan_killiany.json", From 4c26d494585cdeff45fc412bd45281d62a680b18 Mon Sep 17 00:00:00 2001 From: AlexVCaron Date: Thu, 14 Dec 2023 12:18:15 -0500 Subject: [PATCH 08/18] Fix mock collection so they are accessible anywhere. Still need to try merging lib and scripts mocks together --- scilpy/denoise/tests/fixtures/mocks.py | 20 +++++++++++--- scilpy/tests/plugin.py | 18 ++++++++----- ...execute_angle_aware_bilateral_filtering.py | 26 +++++++++++-------- 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/scilpy/denoise/tests/fixtures/mocks.py b/scilpy/denoise/tests/fixtures/mocks.py index 9bb2a5f89..d935f268c 100644 --- a/scilpy/denoise/tests/fixtures/mocks.py +++ b/scilpy/denoise/tests/fixtures/mocks.py @@ -5,14 +5,26 @@ @pytest.fixture(scope='function') -def bilateral_filtering(mock_creator, expected_results=None): +def bilateral_filtering(mock_creator, expected_results): def _mock_side_effect(*args, **kwargs): if expected_results is None or len(expected_results) == 0: return None - _out_odf_fname = expected_results[0] - return nib.load(_out_odf_fname).get_fdata(dtype=np.float32) + return nib.load(expected_results).get_fdata(dtype=np.float32) - return mock_creator("scripts.scil_execute_angle_aware_bilateral_filtering", + return mock_creator("scilpy.denoise.bilateral_filtering", "angle_aware_bilateral_filtering", side_effect=_mock_side_effect) + + +@pytest.fixture(scope='function') +def bilateral_filtering_script(mock_creator, expected_results): + def _mock_side_effect(*args, **kwargs): + if expected_results is None or len(expected_results) == 0: + return None + + return nib.load(expected_results).get_fdata(dtype=np.float32) + + return mock_creator("scripts.scil_execute_angle_aware_bilateral_filtering", + "angle_aware_bilateral_filtering", + side_effect=_mock_side_effect) \ No newline at end of file diff --git a/scilpy/tests/plugin.py b/scilpy/tests/plugin.py index 8626a758c..35430b855 100644 --- a/scilpy/tests/plugin.py +++ b/scilpy/tests/plugin.py @@ -3,6 +3,7 @@ from glob import glob from os.path import realpath import pytest +import warnings # Load mock modules from all library tests @@ -76,15 +77,18 @@ def pytest_configure(config): @pytest.fixture def mock_collector(request): def _collector(mock_name): - _fixture = request(mock_name) - print(_fixture) - return _fixture.getfixturevalue() + try: + return request.getfixturevalue(mock_name) + except pytest.FixtureLookupError: + warnings.warn(f"Fixture {mock_name} not found.") + return None return _collector @pytest.fixture -def mock_creator(module_name, object_name, mocker, - side_effect=None): +def mock_creator(mocker): + def _mocker(module_name, object_name, side_effect=None): + return mocker.patch("{}.{}".format(module_name, object_name), + side_effect=side_effect, create=True) - return mocker.patch("{}.{}".format(module_name, object_name), - side_effect=side_effect, create=True) + return _mocker diff --git a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py index b13c43fc1..9aa293c6e 100644 --- a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py +++ b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py @@ -23,14 +23,17 @@ def test_help_option(script_runner): assert ret.success -@pytest.mark.parametrize("in_fodf,out_fodf", +@pytest.mark.parametrize("in_fodf,expected_results", [[os.path.join(data_path, 'fodf_descoteaux07_sub.nii.gz'), os.path.join(data_path, 'fodf_descoteaux07_sub_full.nii.gz')]], scope='function') def test_asym_basis_output( - script_runner, in_fodf, out_fodf, mock_collector): + script_runner, in_fodf, expected_results, mock_collector, request): + os.chdir(os.path.expanduser(tmp_dir.name)) + _mock = mock_collector("bilateral_filtering_script") + print(request.fixturenames) ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', in_fodf, 'out_fodf1.nii.gz', @@ -44,21 +47,22 @@ def test_asym_basis_output( assert ret.success - _mock = mock_collector("bilateral_filtering") if _mock: _mock.assert_called_once() - assert_images_close(nib.load(out_fodf), nib.load("out_fodf1.nii.gz")) + assert_images_close(nib.load(expected_results), nib.load("out_fodf1.nii.gz")) -@pytest.mark.parametrize("in_fodf,out_fodf,sym_fodf", +@pytest.mark.parametrize("in_fodf,expected_results,sym_fodf", [[os.path.join(data_path, "fodf_descoteaux07_sub.nii.gz"), os.path.join(data_path, "fodf_descoteaux07_sub_full.nii.gz"), os.path.join(data_path, "fodf_descoteaux07_sub_sym.nii.gz")]], scope='function') def test_sym_basis_output( - script_runner, in_fodf, out_fodf, sym_fodf, mock_collector): + script_runner, in_fodf, expected_results, sym_fodf, mock_collector): + os.chdir(os.path.expanduser(tmp_dir.name)) + _mock = mock_collector("bilateral_filtering_script") ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', in_fodf, @@ -74,19 +78,20 @@ def test_sym_basis_output( assert ret.success - _mock = mock_collector("bilateral_filtering") if _mock: _mock.assert_called_once() assert_images_close(nib.load(sym_fodf), nib.load("out_sym.nii.gz")) -@pytest.mark.parametrize("in_fodf,out_fodf", +@pytest.mark.parametrize("in_fodf,expected_results", [[os.path.join(data_path, "fodf_descoteaux07_sub_full.nii.gz"), os.path.join(data_path, "fodf_descoteaux07_sub_twice.nii.gz")]], scope='function') -def test_asym_input(script_runner, in_fodf, out_fodf, mock_collector): +def test_asym_input(script_runner, in_fodf, expected_results, mock_collector): + os.chdir(os.path.expanduser(tmp_dir.name)) + _mock = mock_collector("bilateral_filtering_script") ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', in_fodf, @@ -101,8 +106,7 @@ def test_asym_input(script_runner, in_fodf, out_fodf, mock_collector): assert ret.success - _mock = mock_collector("bilateral_filtering") if _mock: _mock.assert_called_once() - assert_images_close(nib.load(out_fodf), nib.load("out_fodf3.nii.gz")) + assert_images_close(nib.load(expected_results), nib.load("out_fodf3.nii.gz")) From 9f536213ee556525ea3e1e9ec0e2c142bfc0c213 Mon Sep 17 00:00:00 2001 From: AlexVCaron Date: Thu, 14 Dec 2023 13:28:07 -0500 Subject: [PATCH 09/18] Add setuptools to install importlib --- Jenkinsfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index b558ebc80..193eb2d73 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -19,6 +19,7 @@ pipeline { pip3 install numpy==1.23.* pip3 install Cython==0.29.* pip3 install packaging==23.* + pip3 install setuptools pip3 install -e . ''' } @@ -35,6 +36,7 @@ pipeline { pip3 install wheel==0.38.* pip3 install numpy==1.23.* pip3 install packaging==23.* + pip3 install setuptools pip3 install -e . export MPLBACKEND="agg" export OPENBLAS_NUM_THREADS=1 From 9c85fda20c23941065cfcb218b1170e3477f14cc Mon Sep 17 00:00:00 2001 From: AlexVCaron Date: Thu, 14 Dec 2023 13:32:53 -0500 Subject: [PATCH 10/18] add setuptools to build requirements --- Jenkinsfile | 4 +--- scilpy/version.py | 1 - setup.py | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 193eb2d73..7855f6f0f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -19,7 +19,6 @@ pipeline { pip3 install numpy==1.23.* pip3 install Cython==0.29.* pip3 install packaging==23.* - pip3 install setuptools pip3 install -e . ''' } @@ -36,11 +35,10 @@ pipeline { pip3 install wheel==0.38.* pip3 install numpy==1.23.* pip3 install packaging==23.* - pip3 install setuptools pip3 install -e . export MPLBACKEND="agg" export OPENBLAS_NUM_THREADS=1 - pytest --cov-report term-missing:skip-covered + pytest --cov-report term-missing:skip-covered --mocks all ''' } discoverGitReferenceBuild() diff --git a/scilpy/version.py b/scilpy/version.py index 4927cb187..796436505 100644 --- a/scilpy/version.py +++ b/scilpy/version.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -import itertools import glob import os diff --git a/setup.py b/setup.py index e1cc27000..d21c091e4 100644 --- a/setup.py +++ b/setup.py @@ -95,7 +95,7 @@ def run(self): }, ext_modules=get_extensions(), python_requires=PYTHON_VERSION, - setup_requires=['cython', 'numpy'], + setup_requires=['cython', 'numpy', 'setuptools'], install_requires=external_dependencies, entry_points={ 'console_scripts': ["{}=scripts.{}:main".format( From e00c7a503dd0b280422db835f91a31cb77d71602 Mon Sep 17 00:00:00 2001 From: AlexVCaron Date: Thu, 14 Dec 2023 13:38:10 -0500 Subject: [PATCH 11/18] update pip and setuptools --- Jenkinsfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 7855f6f0f..a96d86f5b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -19,6 +19,8 @@ pipeline { pip3 install numpy==1.23.* pip3 install Cython==0.29.* pip3 install packaging==23.* + pip3 install -U pip + pip3 install -U setuptools pip3 install -e . ''' } @@ -35,6 +37,8 @@ pipeline { pip3 install wheel==0.38.* pip3 install numpy==1.23.* pip3 install packaging==23.* + pip3 install -U pip + pip3 install -U setuptools pip3 install -e . export MPLBACKEND="agg" export OPENBLAS_NUM_THREADS=1 From 166696acb1d397fb2d5ece9d7b37d90a668f4d1a Mon Sep 17 00:00:00 2001 From: AlexVCaron Date: Thu, 14 Dec 2023 13:41:33 -0500 Subject: [PATCH 12/18] remove importlib from requirements --- Jenkinsfile | 4 ---- setup.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index a96d86f5b..7855f6f0f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -19,8 +19,6 @@ pipeline { pip3 install numpy==1.23.* pip3 install Cython==0.29.* pip3 install packaging==23.* - pip3 install -U pip - pip3 install -U setuptools pip3 install -e . ''' } @@ -37,8 +35,6 @@ pipeline { pip3 install wheel==0.38.* pip3 install numpy==1.23.* pip3 install packaging==23.* - pip3 install -U pip - pip3 install -U setuptools pip3 install -e . export MPLBACKEND="agg" export OPENBLAS_NUM_THREADS=1 diff --git a/setup.py b/setup.py index d21c091e4..e1cc27000 100644 --- a/setup.py +++ b/setup.py @@ -95,7 +95,7 @@ def run(self): }, ext_modules=get_extensions(), python_requires=PYTHON_VERSION, - setup_requires=['cython', 'numpy', 'setuptools'], + setup_requires=['cython', 'numpy'], install_requires=external_dependencies, entry_points={ 'console_scripts': ["{}=scripts.{}:main".format( From 09c82131b4dfd20fb4b13d1cc1a907de4050d483 Mon Sep 17 00:00:00 2001 From: AlexVCaron Date: Thu, 14 Dec 2023 14:56:25 -0500 Subject: [PATCH 13/18] working mocks easy to use ! --- scilpy/denoise/tests/fixtures/mocks.py | 17 ++------------- scilpy/tests/plugin.py | 17 +++++++++------ ...execute_angle_aware_bilateral_filtering.py | 21 +++++++++++-------- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/scilpy/denoise/tests/fixtures/mocks.py b/scilpy/denoise/tests/fixtures/mocks.py index d935f268c..63d0aef88 100644 --- a/scilpy/denoise/tests/fixtures/mocks.py +++ b/scilpy/denoise/tests/fixtures/mocks.py @@ -13,18 +13,5 @@ def _mock_side_effect(*args, **kwargs): return nib.load(expected_results).get_fdata(dtype=np.float32) return mock_creator("scilpy.denoise.bilateral_filtering", - "angle_aware_bilateral_filtering", - side_effect=_mock_side_effect) - - -@pytest.fixture(scope='function') -def bilateral_filtering_script(mock_creator, expected_results): - def _mock_side_effect(*args, **kwargs): - if expected_results is None or len(expected_results) == 0: - return None - - return nib.load(expected_results).get_fdata(dtype=np.float32) - - return mock_creator("scripts.scil_execute_angle_aware_bilateral_filtering", - "angle_aware_bilateral_filtering", - side_effect=_mock_side_effect) \ No newline at end of file + "angle_aware_bilateral_filtering", + side_effect=_mock_side_effect) diff --git a/scilpy/tests/plugin.py b/scilpy/tests/plugin.py index 35430b855..5da360614 100644 --- a/scilpy/tests/plugin.py +++ b/scilpy/tests/plugin.py @@ -76,19 +76,24 @@ def pytest_configure(config): @pytest.fixture def mock_collector(request): - def _collector(mock_name): + def _collector(mock_names, patch_path): try: - return request.getfixturevalue(mock_name) + return {_name: request.getfixturevalue(_name)(patch_path) + for _name in mock_names} except pytest.FixtureLookupError: - warnings.warn(f"Fixture {mock_name} not found.") + warnings.warn(f"Some fixtures in {mock_names} cannot be found.") return None return _collector @pytest.fixture def mock_creator(mocker): - def _mocker(module_name, object_name, side_effect=None): - return mocker.patch("{}.{}".format(module_name, object_name), - side_effect=side_effect, create=True) + def _mocker(base_module, object_name, side_effect=None): + def _patcher(module_name=None): + _base = base_module if module_name is None else module_name + return mocker.patch("{}.{}".format(_base, object_name), + side_effect=side_effect, create=True) + + return _patcher return _mocker diff --git a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py index 9aa293c6e..f5e2ceeef 100644 --- a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py +++ b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py @@ -31,7 +31,8 @@ def test_asym_basis_output( script_runner, in_fodf, expected_results, mock_collector, request): os.chdir(os.path.expanduser(tmp_dir.name)) - _mock = mock_collector("bilateral_filtering_script") + _mocks = mock_collector(["bilateral_filtering"], + "scripts.scil_execute_angle_aware_bilateral_filtering") print(request.fixturenames) ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', @@ -47,8 +48,8 @@ def test_asym_basis_output( assert ret.success - if _mock: - _mock.assert_called_once() + if _mocks["bilateral_filtering"]: + _mocks["bilateral_filtering"].assert_called_once() assert_images_close(nib.load(expected_results), nib.load("out_fodf1.nii.gz")) @@ -62,7 +63,8 @@ def test_sym_basis_output( script_runner, in_fodf, expected_results, sym_fodf, mock_collector): os.chdir(os.path.expanduser(tmp_dir.name)) - _mock = mock_collector("bilateral_filtering_script") + _mocks = mock_collector(["bilateral_filtering"], + "scripts.scil_execute_angle_aware_bilateral_filtering") ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', in_fodf, @@ -78,8 +80,8 @@ def test_sym_basis_output( assert ret.success - if _mock: - _mock.assert_called_once() + if _mocks["bilateral_filtering"]: + _mocks["bilateral_filtering"].assert_called_once() assert_images_close(nib.load(sym_fodf), nib.load("out_sym.nii.gz")) @@ -91,7 +93,8 @@ def test_sym_basis_output( def test_asym_input(script_runner, in_fodf, expected_results, mock_collector): os.chdir(os.path.expanduser(tmp_dir.name)) - _mock = mock_collector("bilateral_filtering_script") + _mocks = mock_collector(["bilateral_filtering"], + "scripts.scil_execute_angle_aware_bilateral_filtering") ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', in_fodf, @@ -106,7 +109,7 @@ def test_asym_input(script_runner, in_fodf, expected_results, mock_collector): assert ret.success - if _mock: - _mock.assert_called_once() + if _mocks["bilateral_filtering"]: + _mocks["bilateral_filtering"].assert_called_once() assert_images_close(nib.load(expected_results), nib.load("out_fodf3.nii.gz")) From 09ef70cd67bab50c7ff47c592ec480e272085dcd Mon Sep 17 00:00:00 2001 From: AlexVCaron Date: Fri, 15 Dec 2023 12:22:55 -0500 Subject: [PATCH 14/18] Document pytest plugin and mocking. Mock NODDI --- scilpy/denoise/tests/fixtures/mocks.py | 5 + .../denoise/tests/test_bilateral_filtering.py | 54 +++++++++ scilpy/reconst/noddi.py | 31 +++++ scilpy/reconst/tests/fixtures/mocks.py | 14 +++ scilpy/tests/arrays.py | 2 + scilpy/tests/checks.py | 2 + scilpy/tests/dict.py | 1 + scilpy/tests/plugin.py | 107 +++++++++++++++--- scripts/scil_NODDI_maps.py | 51 +++------ scripts/tests/test_NODDI_maps.py | 10 +- ...execute_angle_aware_bilateral_filtering.py | 6 +- 11 files changed, 231 insertions(+), 52 deletions(-) create mode 100644 scilpy/denoise/tests/test_bilateral_filtering.py create mode 100644 scilpy/reconst/noddi.py create mode 100644 scilpy/reconst/tests/fixtures/mocks.py diff --git a/scilpy/denoise/tests/fixtures/mocks.py b/scilpy/denoise/tests/fixtures/mocks.py index 63d0aef88..811560598 100644 --- a/scilpy/denoise/tests/fixtures/mocks.py +++ b/scilpy/denoise/tests/fixtures/mocks.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import nibabel as nib import numpy as np @@ -6,6 +7,10 @@ @pytest.fixture(scope='function') def bilateral_filtering(mock_creator, expected_results): + """ + Mock to patch the angle aware bilateral filtering function. + Needs to be namespace patched by scripts. + """ def _mock_side_effect(*args, **kwargs): if expected_results is None or len(expected_results) == 0: return None diff --git a/scilpy/denoise/tests/test_bilateral_filtering.py b/scilpy/denoise/tests/test_bilateral_filtering.py new file mode 100644 index 000000000..f1b995a19 --- /dev/null +++ b/scilpy/denoise/tests/test_bilateral_filtering.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +import numpy as np + +from scilpy.denoise.bilateral_filtering import \ + angle_aware_bilateral_filtering_cpu +from scilpy.reconst.utils import get_sh_order_and_fullness +from scilpy.tests.arrays import ( + fodf_3x3_order8_descoteaux07, fodf_3x3_order8_descoteaux07_filtered) + + +def _call_angle_aware_bilateral_filtering_cpu_n_processes(n_processes): + """ Call angle_aware_bilateral_filtering_cpu on a simple 3x3 grid + using an arbitrary number of processes. + """ + + in_sh = fodf_3x3_order8_descoteaux07 + sh_order = 8 + sh_basis = 'descoteaux07' + in_full_basis = False + sphere_str = 'repulsion100' + sigma_spatial = 1.0 + sigma_angular = 1.0 + sigma_range = 1.0 + nbr_processes = n_processes + + sh_order, full_basis = get_sh_order_and_fullness(in_sh.shape[-1]) + out = angle_aware_bilateral_filtering_cpu(in_sh, sh_order, + sh_basis, in_full_basis, + sphere_str, sigma_spatial, + sigma_angular, sigma_range, + nbr_processes) + + return out + + +def test_angle_aware_bilateral_filtering_cpu_one_process(): + """ Test angle_aware_bilateral_filtering_cpu on a simple 3x3 grid + using one process. + """ + + out = _call_angle_aware_bilateral_filtering_cpu_n_processes(1) + + assert np.allclose(out, fodf_3x3_order8_descoteaux07_filtered) + + +def test_angle_aware_bilateral_filtering_cpu_four_processes(): + """ Test angle_aware_bilateral_filtering_cpu on a simple 3x3 grid + using four processes. + """ + + out = _call_angle_aware_bilateral_filtering_cpu_n_processes(4) + + assert np.allclose(out, fodf_3x3_order8_descoteaux07_filtered) diff --git a/scilpy/reconst/noddi.py b/scilpy/reconst/noddi.py new file mode 100644 index 000000000..db15a2d6a --- /dev/null +++ b/scilpy/reconst/noddi.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +import amico +from os.path import join +import tempfile + + +def get_evaluator(dwi, scheme_filename, mask, para_diff, iso_diff, + lambda1, lambda2, intra_vol_fraction, intra_orientation_dist, + kernels_dir=None): + + with tempfile.TemporaryDirectory() as tmp_dir: + # Setup AMICO + amico.core.setup() + ae = amico.Evaluation('.', '.') + ae.load_data(dwi, scheme_filename, mask_filename=mask) + # Compute the response functions + ae.set_model("NODDI") + + ae.model.set(para_diff, iso_diff, intra_vol_fraction, + intra_orientation_dist, False) + + ae.set_solver(lambda1=lambda1, lambda2=lambda2) + + ae.set_config('ATOMS_path', + kernels_dir or join(tmp_dir.name, 'kernels', + ae.model.id)) + + ae.generate_kernels(regenerate=not kernels_dir) + + return ae diff --git a/scilpy/reconst/tests/fixtures/mocks.py b/scilpy/reconst/tests/fixtures/mocks.py new file mode 100644 index 000000000..a8b2ec790 --- /dev/null +++ b/scilpy/reconst/tests/fixtures/mocks.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +import pytest + + +@pytest.fixture(scope='function') +def amico_evaluator(mock_creator): + """ + Mock to patch amico's kernel generation and fitting. + Does not need to be namespace patched by scripts. + """ + return mock_creator("amico", "Evaluation", + mock_attributes=["fit", "generate_kernels", + "load_kernels", "save_results"]) diff --git a/scilpy/tests/arrays.py b/scilpy/tests/arrays.py index 45021ce41..54be7c9d6 100644 --- a/scilpy/tests/arrays.py +++ b/scilpy/tests/arrays.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + import copy import numpy as np diff --git a/scilpy/tests/checks.py b/scilpy/tests/checks.py index 59e2ce570..141d87873 100644 --- a/scilpy/tests/checks.py +++ b/scilpy/tests/checks.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + import numpy as np diff --git a/scilpy/tests/dict.py b/scilpy/tests/dict.py index fa8a533e2..af66fdcd3 100644 --- a/scilpy/tests/dict.py +++ b/scilpy/tests/dict.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- dict_to_average = { "sub-01": { diff --git a/scilpy/tests/plugin.py b/scilpy/tests/plugin.py index 5da360614..9cf188df9 100644 --- a/scilpy/tests/plugin.py +++ b/scilpy/tests/plugin.py @@ -2,10 +2,74 @@ from glob import glob from os.path import realpath +from unittest.mock import DEFAULT import pytest import warnings +""" +Scilpy Pytest plugin. As of now, this plugin is only used to handle mocking, +but it can be extended to hook into other parts of the testing process. + +Mocking interface +----------------- + +Writing mocking fixtures is long and tedious. The interface provided by +unittest.mock, and the pytest-mock overhead, is cumbersome. Inasmuch, with +the default pytest framework, it is impossible to share mocks between tests +that are not located within the same module. + +This plugin registers early into pytest and thus, can investiguate the modules' +structure and load stuff into the tests namespaces before they are executed. + +- It first hooks into pytest_addoption to add a new command line option to + load mocks for any or all modules in the scilpy package, --mocks. + +- It then hooks into pytest_configure to load the mocks activated by the user + into the test session. Note that this way, all mocks associated with a module + gets injected into pytest's namespace, which might not be granular enough for + some use cases (see below). + +To make mock creation and collection in test instances, and to allow for a more +granular selection of mocks from mocking modules, this plugin provides two +fixtures: + +- mock_creator : the mock_creator fixture exposes the bases interface of + unittest.mock patchers, but with a more convenient syntax. It + is able to patch mutliple attributes at once, and can be + configured to create the patched object if it does not exist. + +- mock_collector : the mock_collector fixture is a helper function that is used + to collect specific mocks from mocking modules. It is also + used to modify the namespace into which the mocked objects + get patched. This is required for some mocks to be used with + scripts, when their import is relative (e.g. from . import). + +Creating a new mock is done using the mock_creator fixture. All mocks must be +placed inside the scilpy library, in the tests directories of their respective +modules, in fixtures/mocks.py. This is own they get discovered by the plugin. + +- A mock fixture must have a relevant name (e.g. amico_evaluator patches several + parts of the amico.Evaluation class). Its return value is the result of + calling the mock_creator fixture. + +- The mock_creator fixture does not need to be imported, it is provided + automatically by the pytest framework. Simply add mock_creator as a parameter + to the mock fixture function. + +Using mocks in tests is done using the mock_collector fixture. Like the +mock_creator, it is provided automatically by the pytest framework. Simply +add mock_collector as a parameter to the test function that requires mocking. To +use the mocks, call the mock_collector fixture with the list of mock names to +use. Additionally, the mock_collector fixture can be used to modify the +namespace into which the mocks are injected, by providing a patch_path argument +as a second parameter. The returned dictionary indexes loaded mocks by their +name and can be used to assert their usage throughout the test case. +""" + + +AUTOMOCK = DEFAULT + # Load mock modules from all library tests MOCK_MODULES = list(_module.replace("/", ".").replace(".py", "") @@ -13,28 +77,26 @@ root_dir=realpath('.'), recursive=True)) -MOCK_NAMES = list(m.split(".")[-1] for m in MOCK_MODULES) - -LOADED_MOCK_MODULES = [] +MOCK_PACKAGES = list(m.split(".")[-4] for m in MOCK_MODULES) # Helper function to select mocks def _get_active_mocks(include_list=None, exclude_list=None): """ - Returns a list of all active mocks in the current session + Returns a list of all packages with active mocks in the current session Parameters ---------- include_list: iterable or None - List of mocks to consider + List of scilpy packages to consider exclude_list: iterable or None - List of mocks to exclude from consideration + List of scilpy packages to exclude from consideration Returns ------- - list: list of modules containing mocks + list: list of packages with active mocks """ def _active_names(): @@ -47,13 +109,13 @@ def _exclude(_l): return [] if "all" in include_list: - return _exclude(MOCK_NAMES) + return _exclude(MOCK_PACKAGES) - return _exclude([_m for _m in include_list if _m in MOCK_NAMES]) + return _exclude([_m for _m in include_list if _m in MOCK_PACKAGES]) return list(map(lambda _m: _m[1], filter(lambda _m: _m[0] in _active_names(), - zip(MOCK_NAMES, MOCK_MODULES)))) + zip(MOCK_PACKAGES, MOCK_MODULES)))) # Create hooks and fixtures to handle mocking from pytest command line @@ -62,21 +124,23 @@ def pytest_addoption(parser): parser.addoption( "--mocks", nargs='+', - choices=["all"] + MOCK_NAMES, - help="Apply mocks to accelerate tests and " - "prevent testing external dependencies") + choices=["all"] + MOCK_PACKAGES, + help="Load mocks for scilpy packages to accelerate" + "tests and prevent testing external dependencies") def pytest_configure(config): _toggle_mocks = config.getoption("--mocks") for _mock_mod in _get_active_mocks(_toggle_mocks): config.pluginmanager.import_plugin(_mock_mod) - LOADED_MOCK_MODULES.append(_mock_mod) @pytest.fixture def mock_collector(request): - def _collector(mock_names, patch_path): + """ + Pytest fixture to collect a specific set of mocks for a test case + """ + def _collector(mock_names, patch_path=None): try: return {_name: request.getfixturevalue(_name)(patch_path) for _name in mock_names} @@ -88,9 +152,20 @@ def _collector(mock_names, patch_path): @pytest.fixture def mock_creator(mocker): - def _mocker(base_module, object_name, side_effect=None): + """ + Pytest fixture to create a namespace patchable mock + """ + def _mocker(base_module, object_name, side_effect=None, + mock_attributes=None): + def _patcher(module_name=None): _base = base_module if module_name is None else module_name + + if mock_attributes is not None: + return mocker.patch.multiple("{}.{}".format(_base, object_name), + **{a: AUTOMOCK + for a in mock_attributes}) + return mocker.patch("{}.{}".format(_base, object_name), side_effect=side_effect, create=True) diff --git a/scripts/scil_NODDI_maps.py b/scripts/scil_NODDI_maps.py index bb720a85c..f679dfd0f 100755 --- a/scripts/scil_NODDI_maps.py +++ b/scripts/scil_NODDI_maps.py @@ -16,7 +16,6 @@ import sys import tempfile -import amico from dipy.io.gradients import read_bvals_bvecs import numpy as np @@ -27,6 +26,7 @@ assert_output_dirs_exist_and_empty, redirect_stdout_c) from scilpy.gradients.bvec_bval_tools import fsl2mrtrix, identify_shells +from scilpy.reconst.noddi import get_evaluator EPILOG = """ Reference: @@ -126,48 +126,33 @@ def main(): 'at {}.'.format(len(shells_centroids), shells_centroids)) with redirected_stdout: - # Load the data - amico.core.setup() - ae = amico.Evaluation('.', '.') - ae.load_data(args.in_dwi, - tmp_scheme_filename, - mask_filename=args.mask) - # Compute the response functions - ae.set_model("NODDI") - intra_vol_frac = np.linspace(0.1, 0.99, 12) intra_orient_distr = np.hstack((np.array([0.03, 0.06]), np.linspace(0.09, 0.99, 10))) - ae.model.set(args.para_diff, args.iso_diff, - intra_vol_frac, intra_orient_distr, - False) - ae.set_solver(lambda1=args.lambda1, lambda2=args.lambda2) - - # The kernels are, by default, set to be in the current directory - # Depending on the choice, manually change the saving location - if args.save_kernels: - kernels_dir = os.path.join(args.save_kernels) - regenerate_kernels = True - elif args.load_kernels: - kernels_dir = os.path.join(args.load_kernels) - regenerate_kernels = False - else: - kernels_dir = os.path.join(tmp_dir.name, 'kernels', ae.model.id) - regenerate_kernels = True - - ae.set_config('ATOMS_path', kernels_dir) - ae.set_config('OUTPUT_path', args.out_dir) - ae.generate_kernels(regenerate=regenerate_kernels) + # One of those is going to be None, or have a value.If it is a valid + # kernels path, then everything will work in get_evaluator. + kernels_dir = (args.save_kernels or + args.load_kernels or + os.path.join(tmp_dir.name, 'kernels')) + + # Load the data + amico = get_evaluator(args.in_dwi, tmp_scheme_filename, args.mask, + args.para_diff, args.iso_diff, + args.lambda1,args.lambda2, + intra_vol_frac, intra_orient_distr, + kernels_dir=kernels_dir) + if args.compute_only: return - ae.load_kernels() + amico.load_kernels() # Model fit - ae.fit() + amico.fit() + # Save the results - ae.save_results() + amico.save_results() tmp_dir.cleanup() diff --git a/scripts/tests/test_NODDI_maps.py b/scripts/tests/test_NODDI_maps.py index 946656adc..fdff945d4 100644 --- a/scripts/tests/test_NODDI_maps.py +++ b/scripts/tests/test_NODDI_maps.py @@ -16,8 +16,10 @@ def test_help_option(script_runner): assert ret.success -def test_execution_commit_amico(script_runner): +def test_execution_commit_amico(script_runner, mock_collector): os.chdir(os.path.expanduser(tmp_dir.name)) + _mocks = mock_collector(["amico_evaluator"]) + in_dwi = os.path.join(get_home(), 'commit_amico', 'dwi.nii.gz') in_bval = os.path.join(get_home(), 'commit_amico', @@ -32,4 +34,10 @@ def test_execution_commit_amico(script_runner): '--para_diff', '0.0017', '--iso_diff', '0.003', '--lambda1', '0.5', '--lambda2', '0.001', '--processes', '1') + assert ret.success + + amico_mock = _mocks["amico_evaluator"] + assert amico_mock["fit"].called_once() + assert amico_mock["generate_kernels"].called_once() + assert amico_mock["save_results"].called_once() diff --git a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py index f5e2ceeef..6c527e39c 100644 --- a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py +++ b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py @@ -51,7 +51,8 @@ def test_asym_basis_output( if _mocks["bilateral_filtering"]: _mocks["bilateral_filtering"].assert_called_once() - assert_images_close(nib.load(expected_results), nib.load("out_fodf1.nii.gz")) + assert_images_close(nib.load(expected_results), + nib.load("out_fodf1.nii.gz")) @pytest.mark.parametrize("in_fodf,expected_results,sym_fodf", @@ -112,4 +113,5 @@ def test_asym_input(script_runner, in_fodf, expected_results, mock_collector): if _mocks["bilateral_filtering"]: _mocks["bilateral_filtering"].assert_called_once() - assert_images_close(nib.load(expected_results), nib.load("out_fodf3.nii.gz")) + assert_images_close(nib.load(expected_results), + nib.load("out_fodf3.nii.gz")) From dbac2f30186f7399b76fcc7d02a0f718d9431a55 Mon Sep 17 00:00:00 2001 From: AlexVCaron Date: Fri, 15 Dec 2023 12:31:27 -0500 Subject: [PATCH 15/18] pep8 again --- scilpy/reconst/noddi.py | 2 +- scilpy/tests/__init__.py | 1 - scilpy/tests/plugin.py | 21 +++++++++--------- scripts/scil_NODDI_maps.py | 4 ++-- scripts/tests/test_NODDI_maps.py | 22 +++++++++---------- ...execute_angle_aware_bilateral_filtering.py | 21 +++++++++--------- setup.py | 19 +++++++++------- 7 files changed, 45 insertions(+), 45 deletions(-) diff --git a/scilpy/reconst/noddi.py b/scilpy/reconst/noddi.py index db15a2d6a..1c909262a 100644 --- a/scilpy/reconst/noddi.py +++ b/scilpy/reconst/noddi.py @@ -18,7 +18,7 @@ def get_evaluator(dwi, scheme_filename, mask, para_diff, iso_diff, ae.set_model("NODDI") ae.model.set(para_diff, iso_diff, intra_vol_fraction, - intra_orientation_dist, False) + intra_orientation_dist, False) ae.set_solver(lambda1=lambda1, lambda2=lambda2) diff --git a/scilpy/tests/__init__.py b/scilpy/tests/__init__.py index 991aa1a51..e69de29bb 100644 --- a/scilpy/tests/__init__.py +++ b/scilpy/tests/__init__.py @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/scilpy/tests/plugin.py b/scilpy/tests/plugin.py index 9cf188df9..6ed7dff50 100644 --- a/scilpy/tests/plugin.py +++ b/scilpy/tests/plugin.py @@ -34,7 +34,7 @@ granular selection of mocks from mocking modules, this plugin provides two fixtures: -- mock_creator : the mock_creator fixture exposes the bases interface of +- mock_creator : the mock_creator fixture exposes the bases interface of unittest.mock patchers, but with a more convenient syntax. It is able to patch mutliple attributes at once, and can be configured to create the patched object if it does not exist. @@ -49,9 +49,9 @@ placed inside the scilpy library, in the tests directories of their respective modules, in fixtures/mocks.py. This is own they get discovered by the plugin. -- A mock fixture must have a relevant name (e.g. amico_evaluator patches several - parts of the amico.Evaluation class). Its return value is the result of - calling the mock_creator fixture. +- A mock fixture must have a relevant name (e.g. amico_evaluator patches + several parts of the amico.Evaluation class). Its return value is the result + of calling the mock_creator fixture. - The mock_creator fixture does not need to be imported, it is provided automatically by the pytest framework. Simply add mock_creator as a parameter @@ -59,9 +59,9 @@ Using mocks in tests is done using the mock_collector fixture. Like the mock_creator, it is provided automatically by the pytest framework. Simply -add mock_collector as a parameter to the test function that requires mocking. To -use the mocks, call the mock_collector fixture with the list of mock names to -use. Additionally, the mock_collector fixture can be used to modify the +add mock_collector as a parameter to the test function that requires mocking. +To use the mocks, call the mock_collector fixture with the list of mock names +to use. Additionally, the mock_collector fixture can be used to modify the namespace into which the mocks are injected, by providing a patch_path argument as a second parameter. The returned dictionary indexes loaded mocks by their name and can be used to assert their usage throughout the test case. @@ -160,14 +160,15 @@ def _mocker(base_module, object_name, side_effect=None, def _patcher(module_name=None): _base = base_module if module_name is None else module_name + _mock_target = "{}.{}".format(_base, object_name) if mock_attributes is not None: - return mocker.patch.multiple("{}.{}".format(_base, object_name), + return mocker.patch.multiple(_mock_target, **{a: AUTOMOCK for a in mock_attributes}) - return mocker.patch("{}.{}".format(_base, object_name), - side_effect=side_effect, create=True) + return mocker.patch(_mock_target, side_effect=side_effect, + create=True) return _patcher diff --git a/scripts/scil_NODDI_maps.py b/scripts/scil_NODDI_maps.py index f679dfd0f..07bf5d700 100755 --- a/scripts/scil_NODDI_maps.py +++ b/scripts/scil_NODDI_maps.py @@ -132,14 +132,14 @@ def main(): # One of those is going to be None, or have a value.If it is a valid # kernels path, then everything will work in get_evaluator. - kernels_dir = (args.save_kernels or + kernels_dir = (args.save_kernels or args.load_kernels or os.path.join(tmp_dir.name, 'kernels')) # Load the data amico = get_evaluator(args.in_dwi, tmp_scheme_filename, args.mask, args.para_diff, args.iso_diff, - args.lambda1,args.lambda2, + args.lambda1, args.lambda2, intra_vol_frac, intra_orient_distr, kernels_dir=kernels_dir) diff --git a/scripts/tests/test_NODDI_maps.py b/scripts/tests/test_NODDI_maps.py index fdff945d4..59c29b014 100644 --- a/scripts/tests/test_NODDI_maps.py +++ b/scripts/tests/test_NODDI_maps.py @@ -20,14 +20,11 @@ def test_execution_commit_amico(script_runner, mock_collector): os.chdir(os.path.expanduser(tmp_dir.name)) _mocks = mock_collector(["amico_evaluator"]) - in_dwi = os.path.join(get_home(), 'commit_amico', - 'dwi.nii.gz') - in_bval = os.path.join(get_home(), 'commit_amico', - 'dwi.bval') - in_bvec = os.path.join(get_home(), 'commit_amico', - 'dwi.bvec') - mask = os.path.join(get_home(), 'commit_amico', - 'mask.nii.gz') + in_dwi = os.path.join(get_home(), 'commit_amico', 'dwi.nii.gz') + in_bval = os.path.join(get_home(), 'commit_amico', 'dwi.bval') + in_bvec = os.path.join(get_home(), 'commit_amico', 'dwi.bvec') + mask = os.path.join(get_home(), 'commit_amico', 'mask.nii.gz') + ret = script_runner.run('scil_NODDI_maps.py', in_dwi, in_bval, in_bvec, '--mask', mask, '--out_dir', 'noddi', '--b_thr', '30', @@ -37,7 +34,8 @@ def test_execution_commit_amico(script_runner, mock_collector): assert ret.success - amico_mock = _mocks["amico_evaluator"] - assert amico_mock["fit"].called_once() - assert amico_mock["generate_kernels"].called_once() - assert amico_mock["save_results"].called_once() + if _mocks["amico_evaluator"]: + amico_mock = _mocks["amico_evaluator"] + assert amico_mock["fit"].called_once() + assert amico_mock["generate_kernels"].called_once() + assert amico_mock["save_results"].called_once() diff --git a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py index 6c527e39c..7b5dfa6a2 100644 --- a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py +++ b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py @@ -27,14 +27,13 @@ def test_help_option(script_runner): [[os.path.join(data_path, 'fodf_descoteaux07_sub.nii.gz'), os.path.join(data_path, 'fodf_descoteaux07_sub_full.nii.gz')]], scope='function') -def test_asym_basis_output( - script_runner, in_fodf, expected_results, mock_collector, request): +def test_asym_basis_output(script_runner, in_fodf, expected_results, + mock_collector): os.chdir(os.path.expanduser(tmp_dir.name)) - _mocks = mock_collector(["bilateral_filtering"], - "scripts.scil_execute_angle_aware_bilateral_filtering") + _namespace = "scripts.scil_execute_angle_aware_bilateral_filtering" + _mocks = mock_collector(["bilateral_filtering"], _namespace) - print(request.fixturenames) ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', in_fodf, 'out_fodf1.nii.gz', @@ -60,12 +59,12 @@ def test_asym_basis_output( os.path.join(data_path, "fodf_descoteaux07_sub_full.nii.gz"), os.path.join(data_path, "fodf_descoteaux07_sub_sym.nii.gz")]], scope='function') -def test_sym_basis_output( - script_runner, in_fodf, expected_results, sym_fodf, mock_collector): +def test_sym_basis_output(script_runner, in_fodf, expected_results, sym_fodf, + mock_collector): os.chdir(os.path.expanduser(tmp_dir.name)) - _mocks = mock_collector(["bilateral_filtering"], - "scripts.scil_execute_angle_aware_bilateral_filtering") + _namespace = "scripts.scil_execute_angle_aware_bilateral_filtering" + _mocks = mock_collector(["bilateral_filtering"], _namespace) ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', in_fodf, @@ -94,8 +93,8 @@ def test_sym_basis_output( def test_asym_input(script_runner, in_fodf, expected_results, mock_collector): os.chdir(os.path.expanduser(tmp_dir.name)) - _mocks = mock_collector(["bilateral_filtering"], - "scripts.scil_execute_angle_aware_bilateral_filtering") + _namespace = "scripts.scil_execute_angle_aware_bilateral_filtering" + _mocks = mock_collector(["bilateral_filtering"], _namespace) ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', in_fodf, diff --git a/setup.py b/setup.py index e1cc27000..df9a0ee87 100644 --- a/setup.py +++ b/setup.py @@ -70,11 +70,17 @@ def run(self): with open(ver_file) as f: exec(f.read()) -entry_point_legacy = [] + +def _format_entry_point(_script, _base): + return "{}={}.{}:main".format(os.path.basename(_script), _base, + os.path.basename(_script).split(".")[0]) + + +entry_points = [_format_entry_point(s, "scripts") for s in SCRIPTS] + if os.getenv('SCILPY_LEGACY') != 'False': - entry_point_legacy = ["{}=scripts.legacy.{}:main".format( - os.path.basename(s), - os.path.basename(s).split(".")[0]) for s in LEGACY_SCRIPTS] + entry_points += [_format_entry_point(s, "scripts.legacy") + for s in LEGACY_SCRIPTS] opts = dict(name=NAME, maintainer=MAINTAINER, @@ -98,10 +104,7 @@ def run(self): setup_requires=['cython', 'numpy'], install_requires=external_dependencies, entry_points={ - 'console_scripts': ["{}=scripts.{}:main".format( - os.path.basename(s), - os.path.basename(s).split(".")[0]) for s in SCRIPTS] + - entry_point_legacy, + 'console_scripts': entry_points, 'pytest11': ["scilpy-test=scilpy.tests.plugin"] }, data_files=[('data/LUT', From be122ae2f3ce01e970126a940a7f4ae8b4b602c5 Mon Sep 17 00:00:00 2001 From: AlexVCaron Date: Mon, 15 Jan 2024 15:14:10 -0500 Subject: [PATCH 16/18] Move test to correct module. --- scilpy/tests/checks.py | 34 ++++- ...execute_angle_aware_bilateral_filtering.py | 116 ------------------ scripts/tests/test_sh_to_aodf.py | 97 +++++++-------- 3 files changed, 77 insertions(+), 170 deletions(-) delete mode 100644 scripts/tests/test_execute_angle_aware_bilateral_filtering.py diff --git a/scilpy/tests/checks.py b/scilpy/tests/checks.py index 141d87873..6c9e07d3a 100644 --- a/scilpy/tests/checks.py +++ b/scilpy/tests/checks.py @@ -3,15 +3,37 @@ import numpy as np +def _nd_array_match(_arr1, _arr2, _rtol=1E-05, _atol=1E-8): + return np.allclose(_arr1, _arr2, rtol=_rtol, atol=_atol) + + +def _mse_metrics(_arr1, _arr2): + _mse = (_arr1 - _arr2) ** 2. + return np.mean(_mse), np.max(_mse) + + def assert_images_close(img1, img2): dtype = img1.header.get_data_dtype() assert np.allclose(img1.affine, img2.affine), "Images affines don't match" - assert np.allclose( - img1.get_fdata(dtype=dtype), img2.get_fdata(dtype=dtype)), \ + assert _nd_array_match(img1.get_fdata(dtype=dtype), + img2.get_fdata(dtype=dtype)), \ "Images data don't match. MSE : {} | max SE : {}".format( - np.mean((img1.get_fdata(dtype=dtype) - - img2.get_fdata(dtype=dtype)) ** 2.), - np.max((img1.get_fdata(dtype=dtype) - - img2.get_fdata(dtype=dtype)) ** 2.)) + *_mse_metrics(img1.get_fdata(dtype=dtype), + img2.get_fdata(dtype=dtype))) + + + +def assert_images_not_close(img1, img2, affine_must_match=True): + dtype = img1.header.get_data_dtype() + + if affine_must_match: + assert np.allclose(img1.affine, img2.affine), \ + "Images affines don't match" + + assert not _nd_array_match(img1.get_fdata(dtype=dtype), + img2.get_fdata(dtype=dtype)), \ + "Images data should not match. MSE : {} | max SE : {}".format( + *_mse_metrics(img1.get_fdata(dtype=dtype), + img2.get_fdata(dtype=dtype))) diff --git a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py b/scripts/tests/test_execute_angle_aware_bilateral_filtering.py deleted file mode 100644 index 7b5dfa6a2..000000000 --- a/scripts/tests/test_execute_angle_aware_bilateral_filtering.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import nibabel as nib -import numpy as np -import os -import pytest -import tempfile - -from scilpy.io.fetcher import get_testing_files_dict, fetch_data, get_home -from scilpy.tests.checks import assert_images_close - - -# If they already exist, this only takes 5 seconds (check md5sum) -fetch_data(get_testing_files_dict(), keys=['fodf_filtering.zip']) -data_path = os.path.join(get_home(), 'fodf_filtering') -tmp_dir = tempfile.TemporaryDirectory() - - -def test_help_option(script_runner): - ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', - '--help') - assert ret.success - - -@pytest.mark.parametrize("in_fodf,expected_results", - [[os.path.join(data_path, 'fodf_descoteaux07_sub.nii.gz'), - os.path.join(data_path, 'fodf_descoteaux07_sub_full.nii.gz')]], - scope='function') -def test_asym_basis_output(script_runner, in_fodf, expected_results, - mock_collector): - - os.chdir(os.path.expanduser(tmp_dir.name)) - _namespace = "scripts.scil_execute_angle_aware_bilateral_filtering" - _mocks = mock_collector(["bilateral_filtering"], _namespace) - - ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', - in_fodf, - 'out_fodf1.nii.gz', - '--sphere', 'repulsion100', - '--sigma_angular', '1.0', - '--sigma_spatial', '1.0', - '--sigma_range', '1.0', - '--sh_basis', 'descoteaux07', - '--processes', '1', '-f', - print_result=True, shell=True) - - assert ret.success - - if _mocks["bilateral_filtering"]: - _mocks["bilateral_filtering"].assert_called_once() - - assert_images_close(nib.load(expected_results), - nib.load("out_fodf1.nii.gz")) - - -@pytest.mark.parametrize("in_fodf,expected_results,sym_fodf", - [[os.path.join(data_path, "fodf_descoteaux07_sub.nii.gz"), - os.path.join(data_path, "fodf_descoteaux07_sub_full.nii.gz"), - os.path.join(data_path, "fodf_descoteaux07_sub_sym.nii.gz")]], - scope='function') -def test_sym_basis_output(script_runner, in_fodf, expected_results, sym_fodf, - mock_collector): - - os.chdir(os.path.expanduser(tmp_dir.name)) - _namespace = "scripts.scil_execute_angle_aware_bilateral_filtering" - _mocks = mock_collector(["bilateral_filtering"], _namespace) - - ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', - in_fodf, - 'out_fodf2.nii.gz', - '--out_sym', 'out_sym.nii.gz', - '--sphere', 'repulsion100', - '--sigma_angular', '1.0', - '--sigma_spatial', '1.0', - '--sigma_range', '1.0', - '--sh_basis', 'descoteaux07', - '--processes', '1', '-f', - print_result=True, shell=True) - - assert ret.success - - if _mocks["bilateral_filtering"]: - _mocks["bilateral_filtering"].assert_called_once() - - assert_images_close(nib.load(sym_fodf), nib.load("out_sym.nii.gz")) - - -@pytest.mark.parametrize("in_fodf,expected_results", - [[os.path.join(data_path, "fodf_descoteaux07_sub_full.nii.gz"), - os.path.join(data_path, "fodf_descoteaux07_sub_twice.nii.gz")]], - scope='function') -def test_asym_input(script_runner, in_fodf, expected_results, mock_collector): - - os.chdir(os.path.expanduser(tmp_dir.name)) - _namespace = "scripts.scil_execute_angle_aware_bilateral_filtering" - _mocks = mock_collector(["bilateral_filtering"], _namespace) - - ret = script_runner.run('scil_execute_angle_aware_bilateral_filtering.py', - in_fodf, - 'out_fodf3.nii.gz', - '--sphere', 'repulsion100', - '--sigma_angular', '1.0', - '--sigma_spatial', '1.0', - '--sigma_range', '1.0', - '--sh_basis', 'descoteaux07', - '--processes', '1', '-f', - print_result=True, shell=True) - - assert ret.success - - if _mocks["bilateral_filtering"]: - _mocks["bilateral_filtering"].assert_called_once() - - assert_images_close(nib.load(expected_results), - nib.load("out_fodf3.nii.gz")) diff --git a/scripts/tests/test_sh_to_aodf.py b/scripts/tests/test_sh_to_aodf.py index a5d9eb469..de6eb52b3 100644 --- a/scripts/tests/test_sh_to_aodf.py +++ b/scripts/tests/test_sh_to_aodf.py @@ -8,6 +8,7 @@ import tempfile from scilpy.io.fetcher import get_testing_files_dict, fetch_data, get_home +from scilpy.tests.checks import assert_images_close, assert_images_not_close # If they already exist, this only takes 5 seconds (check md5sum) @@ -16,28 +17,20 @@ tmp_dir = tempfile.TemporaryDirectory() -@pytest.fixture -def mock_filtering(mocker, out_fodf): - def _mock(*args, **kwargs): - img = nib.load(out_fodf) - return img.get_fdata().astype(np.float32) - - script = 'scil_sh_to_aodf' - filtering_fn = "angle_aware_bilateral_filtering" - return mocker.patch("scripts.{}.{}".format(script, filtering_fn), - side_effect=_mock, create=True) - - def test_help_option(script_runner): ret = script_runner.run('scil_sh_to_aodf.py', '--help') assert ret.success -@pytest.mark.parametrize("in_fodf,out_fodf", +@pytest.mark.parametrize("in_fodf,expected_results", [[os.path.join(data_path, 'fodf_descoteaux07_sub.nii.gz'), - os.path.join(data_path, 'fodf_descoteaux07_sub_full.nii.gz')]]) -def test_asym_basis_output(script_runner, mock_filtering, in_fodf, out_fodf): + os.path.join(data_path, 'fodf_descoteaux07_sub_full.nii.gz')]], + scope='function') +def test_asym_basis_output(script_runner, in_fodf, expected_results, + mock_collector): + os.chdir(os.path.expanduser(tmp_dir.name)) + _mocks = mock_collector(["bilateral_filtering"], "scripts.scil_sh_to_aodf") ret = script_runner.run('scil_sh_to_aodf.py', in_fodf, 'out_fodf1.nii.gz', @@ -45,24 +38,29 @@ def test_asym_basis_output(script_runner, mock_filtering, in_fodf, out_fodf): '--sigma_angular', '1.0', '--sigma_spatial', '1.0', '--sigma_range', '1.0', - '--sh_basis', 'descoteaux07', '-f', + '--sh_basis', 'descoteaux07', + '--processes', '1', '-f', print_result=True, shell=True) assert ret.success - mock_filtering.assert_called_once() - ret_fodf = nib.load("out_fodf1.nii.gz") - test_fodf = nib.load(out_fodf) - assert np.allclose(ret_fodf.get_fdata(), test_fodf.get_fdata()) + if _mocks["bilateral_filtering"]: + _mocks["bilateral_filtering"].assert_called_once() + + assert_images_close(nib.load(expected_results), + nib.load("out_fodf1.nii.gz")) -@pytest.mark.parametrize("in_fodf,out_fodf,sym_fodf", +@pytest.mark.parametrize("in_fodf,expected_results,sym_fodf", [[os.path.join(data_path, "fodf_descoteaux07_sub.nii.gz"), os.path.join(data_path, "fodf_descoteaux07_sub_full.nii.gz"), - os.path.join(data_path, "fodf_descoteaux07_sub_sym.nii.gz")]]) -def test_sym_basis_output( - script_runner, mock_filtering, in_fodf, out_fodf, sym_fodf): + os.path.join(data_path, "fodf_descoteaux07_sub_sym.nii.gz")]], + scope='function') +def test_sym_basis_output(script_runner, in_fodf, expected_results, sym_fodf, + mock_collector): + os.chdir(os.path.expanduser(tmp_dir.name)) + _mocks = mock_collector(["bilateral_filtering"], "scripts.scil_sh_to_aodf") ret = script_runner.run('scil_sh_to_aodf.py', in_fodf, @@ -72,22 +70,26 @@ def test_sym_basis_output( '--sigma_angular', '1.0', '--sigma_spatial', '1.0', '--sigma_range', '1.0', - '--sh_basis', 'descoteaux07', '-f', + '--sh_basis', 'descoteaux07', + '--processes', '1', '-f', print_result=True, shell=True) assert ret.success - mock_filtering.assert_called_once() - ret_sym_fodf = nib.load("out_sym.nii.gz") - test_sym_fodf = nib.load(sym_fodf) - assert np.allclose(ret_sym_fodf.get_fdata(), test_sym_fodf.get_fdata()) + if _mocks["bilateral_filtering"]: + _mocks["bilateral_filtering"].assert_called_once() + + assert_images_close(nib.load(sym_fodf), nib.load("out_sym.nii.gz")) -@pytest.mark.parametrize("in_fodf,out_fodf", +@pytest.mark.parametrize("in_fodf,expected_results", [[os.path.join(data_path, "fodf_descoteaux07_sub_full.nii.gz"), - os.path.join(data_path, "fodf_descoteaux07_sub_twice.nii.gz")]]) -def test_asym_input(script_runner, mock_filtering, in_fodf, out_fodf): + os.path.join(data_path, "fodf_descoteaux07_sub_twice.nii.gz")]], + scope='function') +def test_asym_input(script_runner, in_fodf, expected_results, mock_collector): + os.chdir(os.path.expanduser(tmp_dir.name)) + _mocks = mock_collector(["bilateral_filtering"], "scripts.scil_sh_to_aodf") ret = script_runner.run('scil_sh_to_aodf.py', in_fodf, @@ -96,39 +98,38 @@ def test_asym_input(script_runner, mock_filtering, in_fodf, out_fodf): '--sigma_angular', '1.0', '--sigma_spatial', '1.0', '--sigma_range', '1.0', - '--sh_basis', 'descoteaux07', '-f', + '--sh_basis', 'descoteaux07', + '--processes', '1', '-f', print_result=True, shell=True) assert ret.success - mock_filtering.assert_called_once() - - ret_fodf = nib.load("out_fodf3.nii.gz") - test_fodf = nib.load(out_fodf) - assert np.allclose(ret_fodf.get_fdata(), test_fodf.get_fdata()) + if _mocks["bilateral_filtering"]: + _mocks["bilateral_filtering"].assert_called_once() + + assert_images_close(nib.load(expected_results), + nib.load("out_fodf3.nii.gz")) -@pytest.mark.parametrize("in_fodf,out_fodf", + +@pytest.mark.parametrize("in_fodf,expected_results", [[os.path.join(data_path, 'fodf_descoteaux07_sub.nii.gz'), os.path.join(data_path, 'fodf_descoteaux07_sub_full.nii.gz')]]) -def test_cosine_method(script_runner, mock_filtering, in_fodf, out_fodf): +def test_cosine_method(script_runner, in_fodf, expected_results): + os.chdir(os.path.expanduser(tmp_dir.name)) + # method cosine is fast and not mocked ret = script_runner.run('scil_sh_to_aodf.py', in_fodf, 'out_fodf1.nii.gz', '--sphere', 'repulsion100', '--method', 'cosine', '--sh_basis', 'descoteaux07', - '-f', + '--processes', '1', '-f', print_result=True, shell=True) assert ret.success - # method cosine is fast and not mocked - mock_filtering.assert_not_called() - - ret_fodf = nib.load("out_fodf1.nii.gz") - test_fodf = nib.load(out_fodf) - # We expect the output to be different from the # one obtained with angle-aware bilateral filtering - assert not np.allclose(ret_fodf.get_fdata(), test_fodf.get_fdata()) + assert_images_not_close(nib.load(expected_results), + nib.load("out_fodf1.nii.gz")) From e5d6a1f2a7dffb6245f294ede770a924a25173e4 Mon Sep 17 00:00:00 2001 From: AlexVCaron Date: Mon, 15 Jan 2024 15:19:12 -0500 Subject: [PATCH 17/18] remove old library test --- .../denoise/tests/test_bilateral_filtering.py | 54 ------------------- 1 file changed, 54 deletions(-) delete mode 100644 scilpy/denoise/tests/test_bilateral_filtering.py diff --git a/scilpy/denoise/tests/test_bilateral_filtering.py b/scilpy/denoise/tests/test_bilateral_filtering.py deleted file mode 100644 index f1b995a19..000000000 --- a/scilpy/denoise/tests/test_bilateral_filtering.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- - -import numpy as np - -from scilpy.denoise.bilateral_filtering import \ - angle_aware_bilateral_filtering_cpu -from scilpy.reconst.utils import get_sh_order_and_fullness -from scilpy.tests.arrays import ( - fodf_3x3_order8_descoteaux07, fodf_3x3_order8_descoteaux07_filtered) - - -def _call_angle_aware_bilateral_filtering_cpu_n_processes(n_processes): - """ Call angle_aware_bilateral_filtering_cpu on a simple 3x3 grid - using an arbitrary number of processes. - """ - - in_sh = fodf_3x3_order8_descoteaux07 - sh_order = 8 - sh_basis = 'descoteaux07' - in_full_basis = False - sphere_str = 'repulsion100' - sigma_spatial = 1.0 - sigma_angular = 1.0 - sigma_range = 1.0 - nbr_processes = n_processes - - sh_order, full_basis = get_sh_order_and_fullness(in_sh.shape[-1]) - out = angle_aware_bilateral_filtering_cpu(in_sh, sh_order, - sh_basis, in_full_basis, - sphere_str, sigma_spatial, - sigma_angular, sigma_range, - nbr_processes) - - return out - - -def test_angle_aware_bilateral_filtering_cpu_one_process(): - """ Test angle_aware_bilateral_filtering_cpu on a simple 3x3 grid - using one process. - """ - - out = _call_angle_aware_bilateral_filtering_cpu_n_processes(1) - - assert np.allclose(out, fodf_3x3_order8_descoteaux07_filtered) - - -def test_angle_aware_bilateral_filtering_cpu_four_processes(): - """ Test angle_aware_bilateral_filtering_cpu on a simple 3x3 grid - using four processes. - """ - - out = _call_angle_aware_bilateral_filtering_cpu_n_processes(4) - - assert np.allclose(out, fodf_3x3_order8_descoteaux07_filtered) From 011d0fb37b568e916840caf9ddd0aeecdbe95a73 Mon Sep 17 00:00:00 2001 From: AlexVCaron Date: Mon, 15 Jan 2024 16:31:52 -0500 Subject: [PATCH 18/18] fix --- scripts/tests/test_sh_to_aodf.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/scripts/tests/test_sh_to_aodf.py b/scripts/tests/test_sh_to_aodf.py index de6eb52b3..4f9384202 100644 --- a/scripts/tests/test_sh_to_aodf.py +++ b/scripts/tests/test_sh_to_aodf.py @@ -38,8 +38,7 @@ def test_asym_basis_output(script_runner, in_fodf, expected_results, '--sigma_angular', '1.0', '--sigma_spatial', '1.0', '--sigma_range', '1.0', - '--sh_basis', 'descoteaux07', - '--processes', '1', '-f', + '--sh_basis', 'descoteaux07', '-f', print_result=True, shell=True) assert ret.success @@ -70,8 +69,7 @@ def test_sym_basis_output(script_runner, in_fodf, expected_results, sym_fodf, '--sigma_angular', '1.0', '--sigma_spatial', '1.0', '--sigma_range', '1.0', - '--sh_basis', 'descoteaux07', - '--processes', '1', '-f', + '--sh_basis', 'descoteaux07', '-f', print_result=True, shell=True) assert ret.success @@ -98,8 +96,7 @@ def test_asym_input(script_runner, in_fodf, expected_results, mock_collector): '--sigma_angular', '1.0', '--sigma_spatial', '1.0', '--sigma_range', '1.0', - '--sh_basis', 'descoteaux07', - '--processes', '1', '-f', + '--sh_basis', 'descoteaux07', '-f', print_result=True, shell=True) assert ret.success @@ -123,8 +120,7 @@ def test_cosine_method(script_runner, in_fodf, expected_results): in_fodf, 'out_fodf1.nii.gz', '--sphere', 'repulsion100', '--method', 'cosine', - '--sh_basis', 'descoteaux07', - '--processes', '1', '-f', + '--sh_basis', 'descoteaux07', '-f', print_result=True, shell=True) assert ret.success