diff --git a/pyproject.toml b/pyproject.toml index d0072d0..d6f222e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -215,13 +215,21 @@ convention = "google" # Flag errors (`C901`) whenever the complexity level exceeds 5. max-complexity = 20 +[tool.pytest.ini_options] +filterwarnings = [ + "ignore::DeprecationWarning", + "ignore::FutureWarning", +] + [tool.coverage.report] exclude_lines = [ "pragma: no cover", "if __name__ == .__main__.:", "def __repr__", "except", - "warnings.warn", + "import warnings", + "warnings\\.warn", + "warnings\\.filter", "print\\(f?\"\\[!\\].*", "def __str__(self):", "def __hash__(self) -> int:", diff --git a/unit_tests/test_nii.py b/unit_tests/test_nii.py index f8d1d3f..d7601f7 100755 --- a/unit_tests/test_nii.py +++ b/unit_tests/test_nii.py @@ -6,9 +6,7 @@ import operator import random -import sys import unittest -from pathlib import Path import nibabel as nib import numpy as np @@ -46,8 +44,6 @@ class TestNII_MathOperators(unittest.TestCase): def make_nii(shape=(8, 9, 10), seed=0, dtype=float): rng = np.random.default_rng(seed) arr = rng.normal(size=shape) if dtype is float else rng.integers(0, 8, size=shape, dtype=dtype) - import nibabel as nib - nii = NII((arr, np.eye(4), nib.nifti1.Nifti1Header())) return nii diff --git a/unit_tests/test_nii_extended.py b/unit_tests/test_nii_extended.py index aafbfe2..b5bf4c2 100644 --- a/unit_tests/test_nii_extended.py +++ b/unit_tests/test_nii_extended.py @@ -384,10 +384,86 @@ def test_rescale_round_trip_shape(self): arr[3:7, 3:9, 3:11] = 1 nii = _make_nii(arr, zoom=(1.0, 1.0, 1.0)) nii2 = nii.rescale((2.0, 2.0, 2.0)).rescale((1.0, 1.0, 1.0)) - # shape should approximately return to original after double rescale for orig_s, new_s in zip(nii.shape, nii2.shape): self.assertAlmostEqual(orig_s, new_s, delta=2) +class Test_NII_MorphologyStd(unittest.TestCase): + """Tests for standard (non-Euclidean) NII.erode_msk and NII.dilate_msk.""" + + @staticmethod + def _make_cube_seg(cube_size=8, shape=(20, 20, 20)): + arr = np.zeros(shape, dtype=np.uint8) + c = shape[0] // 2 + h = cube_size // 2 + arr[c - h : c + h, c - h : c + h, c - h : c + h] = 1 + return _make_nii(arr) + + def test_erode_reduces_volume(self): + nii = self._make_cube_seg() + eroded = nii.erode_msk(n_pixel=1) + self.assertLess(int((eroded.get_array() > 0).sum()), int((nii.get_array() > 0).sum())) + + def test_dilate_increases_volume(self): + nii = self._make_cube_seg() + dilated = nii.dilate_msk(n_pixel=1) + self.assertGreater(int((dilated.get_array() > 0).sum()), int((nii.get_array() > 0).sum())) + + def test_erode_inplace(self): + nii = self._make_cube_seg() + vol_before = int((nii.get_array() > 0).sum()) + nii.erode_msk_(n_pixel=1) + self.assertLess(int((nii.get_array() > 0).sum()), vol_before) + + def test_dilate_inplace(self): + nii = self._make_cube_seg() + vol_before = int((nii.get_array() > 0).sum()) + nii.dilate_msk_(n_pixel=1) + self.assertGreater(int((nii.get_array() > 0).sum()), vol_before) + + +class Test_NII_FillHoles(unittest.TestCase): + """Tests for NII.fill_holes and NII.fill_holes_.""" + + def test_hollow_cube_filled(self): + arr = np.zeros((15, 15, 15), dtype=np.uint8) + arr[2:13, 2:13, 2:13] = 1 + arr[5:10, 5:10, 5:10] = 0 + nii = _make_nii(arr) + filled = nii.fill_holes() + self.assertEqual(filled.get_array()[7, 7, 7], 1) + + def test_no_holes_volume_unchanged(self): + arr = np.zeros((10, 10, 10), dtype=np.uint8) + arr[2:8, 2:8, 2:8] = 1 + nii = _make_nii(arr) + vol_before = int((nii.get_array() > 0).sum()) + filled = nii.fill_holes() + self.assertGreaterEqual(int((filled.get_array() > 0).sum()), vol_before) + + def test_fill_holes_inplace(self): + arr = np.zeros((12, 12, 12), dtype=np.uint8) + arr[1:11, 1:11, 1:11] = 1 + arr[4:8, 4:8, 4:8] = 0 + nii = _make_nii(arr) + nii.fill_holes_() + self.assertEqual(nii.get_array()[6, 6, 6], 1) + + +class Test_NII_GetSegArray(unittest.TestCase): + """Tests for NII.get_seg_array, including the warning path for non-seg NIIs.""" + + def test_returns_correct_array_when_seg(self): + arr = np.array([[[0, 1], [2, 3]]], dtype=np.int16) + nii = _make_nii(arr, seg=True) + np.testing.assert_array_equal(nii.get_seg_array(), arr) + + def test_returns_array_when_not_seg(self): + arr = np.ones((4, 4, 4), dtype=np.float32) + nii = _make_nii(arr, seg=False) + result = nii.get_seg_array() + self.assertEqual(result.shape, arr.shape) + + if __name__ == "__main__": unittest.main() diff --git a/unit_tests/test_nii_io.py b/unit_tests/test_nii_io.py new file mode 100644 index 0000000..0c5c5f6 --- /dev/null +++ b/unit_tests/test_nii_io.py @@ -0,0 +1,105 @@ +"""Round-trip I/O tests for NII.load, NII.save, and NII.from_numpy.""" + +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path + +import numpy as np + +from TPTBox import NII + + +def _make_nii(arr: np.ndarray, seg: bool = True, zoom=(1.0, 1.0, 1.0)) -> NII: + affine = np.diag([*zoom, 1.0]) + return NII.from_numpy(arr, affine=affine, seg=seg) + + +class Test_NII_IO(unittest.TestCase): + """Verify that NII survives a save→load round-trip with consistent shape and metadata.""" + + def test_save_load_roundtrip_shape(self): + arr = np.zeros((10, 12, 14), dtype=np.uint8) + arr[3:7, 3:9, 3:11] = 1 + nii = _make_nii(arr, zoom=(1.0, 2.0, 3.0)) + with tempfile.NamedTemporaryFile(suffix=".nii.gz", delete=False) as f: + path = Path(f.name) + try: + nii.save(path, verbose=False) + loaded = NII.load(path, seg=True) + self.assertEqual(loaded.shape, nii.shape) + finally: + path.unlink(missing_ok=True) + + def test_save_load_roundtrip_data(self): + arr = np.arange(27, dtype=np.uint8).reshape(3, 3, 3) + nii = _make_nii(arr, seg=True) + with tempfile.NamedTemporaryFile(suffix=".nii.gz", delete=False) as f: + path = Path(f.name) + try: + nii.save(path, verbose=False) + loaded = NII.load(path, seg=True) + np.testing.assert_array_equal(loaded.get_array(), arr) + finally: + path.unlink(missing_ok=True) + + def test_save_load_preserves_zoom(self): + arr = np.zeros((5, 6, 7), dtype=np.uint8) + nii = _make_nii(arr, zoom=(1.5, 2.5, 3.5)) + with tempfile.NamedTemporaryFile(suffix=".nii.gz", delete=False) as f: + path = Path(f.name) + try: + nii.save(path, verbose=False) + loaded = NII.load(path, seg=True) + for orig, restored in zip(nii.zoom, loaded.zoom): + self.assertAlmostEqual(orig, restored, places=4) + finally: + path.unlink(missing_ok=True) + + def test_save_load_seg_flag(self): + arr = np.zeros((4, 4, 4), dtype=np.uint8) + arr[1:3, 1:3, 1:3] = 1 + nii = _make_nii(arr, seg=True) + with tempfile.NamedTemporaryFile(suffix=".nii.gz", delete=False) as f: + path = Path(f.name) + try: + nii.save(path, verbose=False) + loaded = NII.load(path, seg=True) + self.assertTrue(loaded.seg) + finally: + path.unlink(missing_ok=True) + + +class Test_NII_FromNumpy(unittest.TestCase): + """Tests for NII.from_numpy factory method.""" + + def test_shape(self): + arr = np.zeros((8, 9, 10), dtype=np.uint8) + nii = NII.from_numpy(arr, affine=np.eye(4), seg=True) + self.assertEqual(nii.shape, (8, 9, 10)) + + def test_seg_flag_true(self): + arr = np.zeros((4, 4, 4), dtype=np.uint8) + self.assertTrue(NII.from_numpy(arr, affine=np.eye(4), seg=True).seg) + + def test_seg_flag_false(self): + arr = np.zeros((4, 4, 4), dtype=np.uint8) + self.assertFalse(NII.from_numpy(arr, affine=np.eye(4), seg=False).seg) + + def test_affine_zoom_extracted(self): + arr = np.zeros((5, 5, 5), dtype=np.uint8) + affine = np.diag([2.0, 3.0, 4.0, 1.0]) + nii = NII.from_numpy(arr, affine=affine, seg=True) + self.assertAlmostEqual(nii.zoom[0], 2.0) + self.assertAlmostEqual(nii.zoom[1], 3.0) + self.assertAlmostEqual(nii.zoom[2], 4.0) + + def test_data_round_trip(self): + arr = np.arange(24, dtype=np.int16).reshape(2, 3, 4) + nii = NII.from_numpy(arr, affine=np.eye(4), seg=False) + np.testing.assert_array_equal(nii.get_array(), arr) + + +if __name__ == "__main__": + unittest.main() diff --git a/unit_tests/test_nputils.py b/unit_tests/test_nputils.py index bc5cdb5..4477540 100755 --- a/unit_tests/test_nputils.py +++ b/unit_tests/test_nputils.py @@ -1,21 +1,12 @@ -# Call 'python -m unittest' on this folder -# coverage run -m unittest -# coverage report -# coverage html from __future__ import annotations -import sys -from pathlib import Path +import random +import unittest -file = Path(__file__).resolve() -sys.path.append(str(file.parents[2])) -import random # noqa: E402 -import unittest # noqa: E402 +import numpy as np -import numpy as np # noqa: E402 - -from TPTBox.core import np_utils # noqa: E402 -from TPTBox.tests.test_utils import get_nii, repeats # noqa: E402 +from TPTBox.core import np_utils +from TPTBox.tests.test_utils import get_nii, repeats def make_test_array_repeating(shape=(4, 4), labels=(0, 1, 2, 3)) -> np.ndarray: diff --git a/unit_tests/test_nputils_extended.py b/unit_tests/test_nputils_extended.py index b8e0814..6f6e81b 100644 --- a/unit_tests/test_nputils_extended.py +++ b/unit_tests/test_nputils_extended.py @@ -271,5 +271,207 @@ def test_no_overlap(self): self.assertNotIn((1, 2), result) +class Test_np_bbox_binary(unittest.TestCase): + """Tests for np_bbox_binary — tight bounding box of all non-zero voxels.""" + + def test_basic_cube(self): + arr = np.zeros((10, 10, 10), dtype=np.uint8) + arr[2:6, 3:7, 1:5] = 1 + slices = np_utils.np_bbox_binary(arr) + self.assertEqual(slices[0].start, 2) + self.assertEqual(slices[0].stop, 6) + self.assertEqual(slices[1].start, 3) + self.assertEqual(slices[1].stop, 7) + self.assertEqual(slices[2].start, 1) + self.assertEqual(slices[2].stop, 5) + + def test_single_voxel(self): + arr = np.zeros((8, 8, 8), dtype=np.uint8) + arr[4, 5, 6] = 1 + slices = np_utils.np_bbox_binary(arr) + self.assertEqual(slices[0].start, 4) + self.assertEqual(slices[0].stop, 5) + self.assertEqual(slices[1].start, 5) + self.assertEqual(slices[1].stop, 6) + + def test_empty_raises(self): + arr = np.zeros((5, 5, 5), dtype=np.uint8) + with self.assertRaises(ValueError): + np_utils.np_bbox_binary(arr, raise_error=True) + + def test_with_padding_expands_box(self): + arr = np.zeros((10, 10, 10), dtype=np.uint8) + arr[4:6, 4:6, 4:6] = 1 + no_pad = np_utils.np_bbox_binary(arr, px_dist=0) + with_pad = np_utils.np_bbox_binary(arr, px_dist=1) + self.assertLessEqual(with_pad[0].start, no_pad[0].start) + self.assertGreaterEqual(with_pad[0].stop, no_pad[0].stop) + + +class Test_np_point_coordinates(unittest.TestCase): + """Tests for np_point_coordinates — non-zero voxel coordinates in 3D.""" + + def test_single_point(self): + arr = np.zeros((5, 5, 5), dtype=np.uint8) + arr[2, 3, 4] = 1 + coords = np_utils.np_point_coordinates(arr) + self.assertEqual(len(coords), 1) + self.assertEqual(coords[0], (2, 3, 4)) + + def test_multiple_points(self): + arr = np.zeros((5, 5, 5), dtype=np.uint8) + arr[1, 1, 1] = 1 + arr[3, 3, 3] = 1 + coords = np_utils.np_point_coordinates(arr) + self.assertEqual(len(coords), 2) + self.assertIn((1, 1, 1), coords) + self.assertIn((3, 3, 3), coords) + + def test_empty_array(self): + arr = np.zeros((4, 4, 4), dtype=np.uint8) + coords = np_utils.np_point_coordinates(arr) + self.assertEqual(len(coords), 0) + + def test_requires_3d(self): + arr = np.zeros((4, 4), dtype=np.uint8) + with self.assertRaises(AssertionError): + np_utils.np_point_coordinates(arr) + + +class Test_np_translate_to_center(unittest.TestCase): + """Tests for np_translate_to_center_of_array — moves content toward array center.""" + + def test_output_shape_preserved(self): + arr = np.zeros((10, 10, 10), dtype=np.uint8) + arr[1, 1, 1] = 1 + out = np_utils.np_translate_to_center_of_array(arr) + self.assertEqual(out.shape, arr.shape) + + def test_sum_preserved(self): + arr = np.zeros((12, 12, 12), dtype=np.uint8) + arr[1:4, 1:4, 1:4] = 1 + out = np_utils.np_translate_to_center_of_array(arr) + self.assertEqual(int(out.sum()), int(arr.sum())) + + def test_content_moves_toward_center(self): + arr = np.zeros((20, 20, 20), dtype=np.uint8) + arr[0, 0, 0] = 1 + out = np_utils.np_translate_to_center_of_array(arr) + xs, ys, zs = np.where(out) + if len(xs) > 0: + self.assertGreater(xs[0], 0) + + +class Test_np_calc_convex_hull(unittest.TestCase): + """Tests for np_calc_convex_hull — fills the convex hull of non-zero voxels.""" + + def test_2d_output_shape(self): + arr = np.zeros((10, 10), dtype=np.uint8) + arr[2:8, 2:8] = 1 + hull = np_utils.np_calc_convex_hull(arr) + self.assertEqual(hull.shape, arr.shape) + + def test_hull_contains_original(self): + # need > 3 non-zero points so ConvexHull can construct a hull + arr = np.zeros((12, 12), dtype=np.uint8) + arr[1, 6] = 1 + arr[6, 1] = 1 + arr[6, 11] = 1 + arr[11, 6] = 1 + hull = np_utils.np_calc_convex_hull(arr) + self.assertEqual(hull.shape, arr.shape) + self.assertTrue(np.all(hull[arr > 0] > 0)) + + def test_3d_output_same_shape(self): + arr = np.zeros((8, 8, 8), dtype=np.uint8) + arr[2:6, 2:6, 2:6] = 1 + hull = np_utils.np_calc_convex_hull(arr) + self.assertEqual(hull.shape, arr.shape) + + def test_hull_not_smaller_than_input(self): + arr = np.zeros((10, 10), dtype=np.uint8) + arr[2:8, 2:8] = 1 + hull = np_utils.np_calc_convex_hull(arr) + self.assertGreaterEqual(int(hull.sum()), int(arr.sum())) + + +class Test_np_betti_numbers(unittest.TestCase): + """Tests for np_betti_numbers — topological descriptors B0, B1, B2.""" + + def test_single_ball_b0_is_1(self): + arr = np.zeros((10, 10, 10), dtype=np.uint8) + arr[2:8, 2:8, 2:8] = 1 + b0, _b1, _b2 = np_utils.np_betti_numbers(arr) + self.assertEqual(b0, 1) + + def test_two_components_b0_is_2(self): + arr = np.zeros((15, 15, 15), dtype=np.uint8) + arr[1:4, 1:4, 1:4] = 1 + arr[10:13, 10:13, 10:13] = 1 + b0, _b1, _b2 = np_utils.np_betti_numbers(arr) + self.assertEqual(b0, 2) + + def test_empty_b0_is_0(self): + arr = np.zeros((6, 6, 6), dtype=np.uint8) + b0, _b1, _b2 = np_utils.np_betti_numbers(arr) + self.assertEqual(b0, 0) + + def test_returns_three_ints(self): + arr = np.zeros((8, 8, 8), dtype=np.uint8) + arr[2:6, 2:6, 2:6] = 1 + result = np_utils.np_betti_numbers(arr) + self.assertEqual(len(result), 3) + for val in result: + self.assertIsInstance(val, int) + + +class Test_np_majority_label_overlap(unittest.TestCase): + """Tests for np_map_labels_based_on_majority_label_mask_overlap.""" + + def test_simple_remap(self): + arr = np.zeros((10, 10, 10), dtype=np.uint8) + arr[3:7, 3:7, 3:7] = 1 + label_mask = np.zeros_like(arr) + label_mask[2:8, 2:8, 2:8] = 5 + result = np_utils.np_map_labels_based_on_majority_label_mask_overlap(arr, label_mask) + self.assertIn(5, np_utils.np_unique_withoutzero(result)) + self.assertNotIn(1, np_utils.np_unique(result)) + + def test_no_overlap_maps_to_zero(self): + arr = np.zeros((10, 10, 10), dtype=np.uint8) + arr[1:3, 1:3, 1:3] = 1 + label_mask = np.zeros_like(arr) + result = np_utils.np_map_labels_based_on_majority_label_mask_overlap(arr, label_mask, no_match_label=0) + self.assertNotIn(1, np_utils.np_unique(result)) + + def test_inplace(self): + arr = np.zeros((10, 10, 10), dtype=np.uint8) + arr[3:7, 3:7, 3:7] = 2 + label_mask = np.zeros_like(arr) + label_mask[2:8, 2:8, 2:8] = 9 + result = np_utils.np_map_labels_based_on_majority_label_mask_overlap(arr, label_mask, inplace=True) + self.assertIs(result, arr) + + +class Test_np_find_index_of_k_max_values(unittest.TestCase): + """Tests for np_find_index_of_k_max_values — indices of k largest values.""" + + def test_top1(self): + arr = np.array([3.0, 1.0, 9.0, 5.0]) + idx = np_utils.np_find_index_of_k_max_values(arr, k=1) + self.assertEqual(idx[0], 2) + + def test_top2_order(self): + arr = np.array([1.0, 7.0, 3.0, 9.0, 2.0]) + idx = np_utils.np_find_index_of_k_max_values(arr, k=2) + self.assertEqual(idx[0], 3) + self.assertEqual(idx[1], 1) + + def test_default_k_returns_two(self): + arr = np.array([5.0, 1.0, 3.0]) + idx = np_utils.np_find_index_of_k_max_values(arr) + self.assertEqual(len(idx), 2) + + if __name__ == "__main__": unittest.main() diff --git a/unit_tests/test_vertconstants.py b/unit_tests/test_vertconstants.py index c1b655a..77b13ac 100644 --- a/unit_tests/test_vertconstants.py +++ b/unit_tests/test_vertconstants.py @@ -1,19 +1,11 @@ -# Call 'python -m unittest' on this folder -# coverage run -m unittest -# coverage report -# coverage html from __future__ import annotations -import sys import unittest -from pathlib import Path -sys.path.append(str(Path(__file__).resolve().parents[2])) import numpy as np from TPTBox import Vertebra_Instance - -structures = ["rib", "ivd", "endplate"] +from TPTBox.core.vert_constants import Location, vert_subreg_labels class Test_Locations(unittest.TestCase): @@ -67,3 +59,106 @@ def test_vertebra_instance_correctness(self): ]: with self.assertRaises(AssertionError): self.assertIsNone(i.RIB) + + +class Test_Vertebra_Regions(unittest.TestCase): + """Tests for Vertebra_Instance region accessors and class-level queries.""" + + def test_cervical_count(self): + self.assertEqual(len(Vertebra_Instance.cervical()), 7) + + def test_thoracic_count(self): + self.assertEqual(len(Vertebra_Instance.thoracic()), 13) + + def test_lumbar_count(self): + self.assertEqual(len(Vertebra_Instance.lumbar()), 6) + + def test_sacrum_includes_cocc(self): + self.assertIn(Vertebra_Instance.COCC, Vertebra_Instance.sacrum()) + + def test_order_starts_c1_ends_cocc(self): + order = Vertebra_Instance.order() + self.assertEqual(order[0], Vertebra_Instance.C1) + self.assertEqual(order[-1], Vertebra_Instance.COCC) + + def test_order_dict_length(self): + od = Vertebra_Instance.order_dict() + self.assertEqual(len(od), len(Vertebra_Instance.order())) + + def test_is_sacrum_true(self): + self.assertTrue(Vertebra_Instance.is_sacrum(Vertebra_Instance.S1.value)) + self.assertTrue(Vertebra_Instance.is_sacrum(Vertebra_Instance.COCC.value)) + + def test_is_sacrum_false(self): + self.assertFalse(Vertebra_Instance.is_sacrum(Vertebra_Instance.L5.value)) + self.assertFalse(Vertebra_Instance.is_sacrum(Vertebra_Instance.T1.value)) + + def test_rib_label_nonempty(self): + self.assertGreater(len(Vertebra_Instance.rib_label()), 0) + + def test_endplate_label_nonempty(self): + self.assertGreater(len(Vertebra_Instance.endplate_label()), 0) + + def test_ivd_and_endplate_offsets(self): + v = Vertebra_Instance.L3 # no rib, has IVD + self.assertEqual(v.IVD, v.value + 100) + self.assertEqual(v.ENDPLATE, v.value + 200) + + def test_vertebra_label_without_sacrum(self): + labels = Vertebra_Instance.vertebra_label_without_sacrum() + self.assertNotIn(Vertebra_Instance.S1.value, labels) + self.assertNotIn(Vertebra_Instance.COCC.value, labels) + + +class Test_Vertebra_Navigation(unittest.TestCase): + """Tests for get_next_poi and get_previous_poi navigation helpers.""" + + def test_get_next_poi_finds_next(self): + labels = [Vertebra_Instance.L3.value, Vertebra_Instance.L5.value] + nxt = Vertebra_Instance.L3.get_next_poi(labels) + self.assertEqual(nxt, Vertebra_Instance.L5) + + def test_get_next_poi_returns_none_at_end(self): + labels = [Vertebra_Instance.COCC.value] + self.assertIsNone(Vertebra_Instance.COCC.get_next_poi(labels)) + + def test_get_previous_poi_finds_prev(self): + labels = [Vertebra_Instance.T10.value, Vertebra_Instance.T12.value] + prev = Vertebra_Instance.T12.get_previous_poi(labels) + self.assertEqual(prev, Vertebra_Instance.T10) + + def test_get_previous_poi_returns_none_at_start(self): + labels = [Vertebra_Instance.C1.value] + self.assertIsNone(Vertebra_Instance.C1.get_previous_poi(labels)) + + def test_get_next_skips_absent(self): + # T1 and T3 present, T2 missing — next from T1 should be T3 + labels = [Vertebra_Instance.T1.value, Vertebra_Instance.T3.value] + nxt = Vertebra_Instance.T1.get_next_poi(labels) + self.assertEqual(nxt, Vertebra_Instance.T3) + + +class Test_Location_Enum(unittest.TestCase): + """Tests for the Location enum and vert_subreg_labels utility.""" + + def test_vert_subreg_labels_nonempty(self): + self.assertGreater(len(vert_subreg_labels()), 0) + + def test_vert_subreg_labels_contains_corpus(self): + self.assertIn(Location.Vertebra_Corpus, vert_subreg_labels()) + + def test_vert_subreg_labels_with_border_not_smaller(self): + with_border = vert_subreg_labels(with_border=True) + without_border = vert_subreg_labels(with_border=False) + self.assertGreaterEqual(len(with_border), len(without_border)) + + def test_location_save_as_name_false(self): + self.assertFalse(Location.save_as_name()) + + def test_location_get_id_from_value(self): + val = Location.Vertebra_Corpus.value + self.assertEqual(Location._get_id(val), val) + + def test_location_unique_values(self): + values = [loc.value for loc in Location] + self.assertEqual(len(values), len(set(values)), "Location enum has duplicate values")