|
9 | 9 | import shutil |
10 | 10 | from copy import deepcopy |
11 | 11 | from pathlib import Path |
12 | | - |
13 | | -# When no longer supporting Python <3.9 this should be collections.abc.Iterable |
14 | 12 | from typing import TYPE_CHECKING, Callable |
| 13 | +from unittest.mock import patch |
15 | 14 |
|
16 | 15 | import cv2 |
17 | 16 | import glymur |
|
27 | 26 |
|
28 | 27 | from tiatoolbox import cli, utils |
29 | 28 | from tiatoolbox.annotation import SQLiteStore |
30 | | -from tiatoolbox.utils import imread |
| 29 | +from tiatoolbox.utils import imread, tiff_to_fsspec |
31 | 30 | from tiatoolbox.utils.exceptions import FileNotSupportedError |
32 | 31 | from tiatoolbox.utils.magic import is_sqlite3 |
33 | 32 | from tiatoolbox.utils.transforms import imresize, locsize2bounds |
|
37 | 36 | AnnotationStoreReader, |
38 | 37 | ArrayView, |
39 | 38 | DICOMWSIReader, |
| 39 | + FsspecJsonWSIReader, |
40 | 40 | JP2WSIReader, |
41 | 41 | NGFFWSIReader, |
42 | 42 | OpenSlideWSIReader, |
@@ -221,6 +221,43 @@ def read_bounds_level_consistency(wsi: WSIReader, bounds: IntBounds) -> None: |
221 | 221 | # Utility Test Classes & Functions |
222 | 222 | # ------------------------------------------------------------------------------------- |
223 | 223 |
|
| 224 | +_FSSPEC_WSI_CACHE = {} |
| 225 | + |
| 226 | + |
| 227 | +def fsspec_wsi(sample_svs: Path, tmp_path: Path) -> FsspecJsonWSIReader: |
| 228 | + """Returns cached FsspecJsonWSIReader instance. |
| 229 | +
|
| 230 | + The reader instance opens CMU-1-Small-Region.svs image. |
| 231 | +
|
| 232 | + It's cached so the reader can be reused, |
| 233 | +
|
| 234 | + since loading the whole image using HTTP range requests from: |
| 235 | +
|
| 236 | + https://tiatoolbox.dcs.warwick.ac.uk/sample_wsis/CMU-1-Small-Region.svs |
| 237 | +
|
| 238 | + takes about 20 seconds. |
| 239 | +
|
| 240 | + """ |
| 241 | + cache_key = "sample_svs" |
| 242 | + |
| 243 | + if cache_key in _FSSPEC_WSI_CACHE: |
| 244 | + return _FSSPEC_WSI_CACHE[cache_key] # Return cached instance |
| 245 | + |
| 246 | + file_types = ("*.svs",) |
| 247 | + files_all = utils.misc.grab_files_from_dir( |
| 248 | + input_path=Path(sample_svs).parent, |
| 249 | + file_types=file_types, |
| 250 | + ) |
| 251 | + svs_file_path = str(files_all[0]) |
| 252 | + json_file_path = str(tmp_path / "fsspec.json") |
| 253 | + final_url = ( |
| 254 | + "https://tiatoolbox.dcs.warwick.ac.uk/sample_wsis/CMU-1-Small-Region.svs" |
| 255 | + ) |
| 256 | + tiff_to_fsspec.main(svs_file_path, json_file_path, final_url) |
| 257 | + |
| 258 | + _FSSPEC_WSI_CACHE[cache_key] = wsireader.FsspecJsonWSIReader(json_file_path) |
| 259 | + return _FSSPEC_WSI_CACHE[cache_key] |
| 260 | + |
224 | 261 |
|
225 | 262 | class DummyMutableOpenSlideObject: |
226 | 263 | """Dummy OpenSlide object with mutable properties.""" |
@@ -2812,3 +2849,133 @@ def test_read_multi_channel(source_image: Path) -> None: |
2812 | 2849 | assert region.shape == (100, 50, (new_img_array.shape[-1])) |
2813 | 2850 | assert np.abs(np.median(region.astype(int) - target.astype(int))) == 0 |
2814 | 2851 | assert np.abs(np.mean(region.astype(int) - target.astype(int))) < 0.2 |
| 2852 | + |
| 2853 | + |
| 2854 | +def test_fsspec_json_wsi_reader_instantiation() -> None: |
| 2855 | + """Test if FsspecJsonWSIReader is instantiated. |
| 2856 | +
|
| 2857 | + In case json is passed to WSIReader.open, FsspecJsonWSIReader |
| 2858 | + should be instantiated. |
| 2859 | + """ |
| 2860 | + input_path = "mock_path.json" |
| 2861 | + mpp = None |
| 2862 | + power = None |
| 2863 | + |
| 2864 | + with ( |
| 2865 | + patch( |
| 2866 | + "tiatoolbox.wsicore.wsireader.FsspecJsonWSIReader.is_valid_zarr_fsspec", |
| 2867 | + return_value=True, |
| 2868 | + ), |
| 2869 | + patch("tiatoolbox.wsicore.wsireader.FsspecJsonWSIReader") as mock_reader, |
| 2870 | + ): |
| 2871 | + WSIReader.open(input_path, mpp, power) |
| 2872 | + mock_reader.assert_called_once_with(input_path, mpp=mpp, power=power) |
| 2873 | + |
| 2874 | + |
| 2875 | +def test_generate_fsspec_json_file_and_validate( |
| 2876 | + sample_svs: Path, tmp_path: Path |
| 2877 | +) -> None: |
| 2878 | + """Test generate fsspec json file and validate it.""" |
| 2879 | + file_types = ("*.svs",) |
| 2880 | + |
| 2881 | + files_all = utils.misc.grab_files_from_dir( |
| 2882 | + input_path=Path(sample_svs).parent, |
| 2883 | + file_types=file_types, |
| 2884 | + ) |
| 2885 | + |
| 2886 | + svs_file_path = str(files_all[0]) |
| 2887 | + json_file_path = str(tmp_path / "fsspec.json") |
| 2888 | + final_url = "https://example.com/some_id" |
| 2889 | + |
| 2890 | + tiff_to_fsspec.main(svs_file_path, json_file_path, final_url) |
| 2891 | + |
| 2892 | + assert Path(json_file_path).exists(), "Output JSON file was not created." |
| 2893 | + |
| 2894 | + assert FsspecJsonWSIReader.is_valid_zarr_fsspec(json_file_path), ( |
| 2895 | + "FSSPEC JSON file is invalid." |
| 2896 | + ) |
| 2897 | + |
| 2898 | + |
| 2899 | +def test_fsspec_wsireader_info_read(sample_svs: Path, tmp_path: Path) -> None: |
| 2900 | + """Test info read of the FsspecJsonWSIReader. |
| 2901 | +
|
| 2902 | + Generate fsspec json file and load image from: |
| 2903 | +
|
| 2904 | + https://tiatoolbox.dcs.warwick.ac.uk/sample_wsis/CMU-1-Small-Region.svs |
| 2905 | +
|
| 2906 | + """ |
| 2907 | + wsi = fsspec_wsi(sample_svs, tmp_path) |
| 2908 | + info = wsi.info |
| 2909 | + |
| 2910 | + assert info is not None, "info should not be None." |
| 2911 | + |
| 2912 | + |
| 2913 | +def test_read_bounds_fsspec_reader_baseline(sample_svs: Path, tmp_path: Path) -> None: |
| 2914 | + """Test FsspecJsonWSIReader read bounds at baseline. |
| 2915 | +
|
| 2916 | + Location coordinate is in baseline (level 0) reference frame. |
| 2917 | +
|
| 2918 | + """ |
| 2919 | + wsi = fsspec_wsi(sample_svs, tmp_path) |
| 2920 | + |
| 2921 | + bounds = SVS_TEST_TISSUE_BOUNDS |
| 2922 | + size = SVS_TEST_TISSUE_SIZE |
| 2923 | + im_region = wsi.read_bounds(bounds, resolution=0, units="level") |
| 2924 | + |
| 2925 | + assert isinstance(im_region, np.ndarray) |
| 2926 | + assert im_region.dtype == "uint8" |
| 2927 | + assert im_region.shape == (*size[::-1], 3) |
| 2928 | + |
| 2929 | + |
| 2930 | +def test_read_rect_fsspec_reader_baseline(sample_svs: Path, tmp_path: Path) -> None: |
| 2931 | + """Test FsspecJsonWSIReader read rect at baseline. |
| 2932 | +
|
| 2933 | + Location coordinate is in baseline (level 0) reference frame. |
| 2934 | +
|
| 2935 | + """ |
| 2936 | + wsi = fsspec_wsi(sample_svs, tmp_path) |
| 2937 | + |
| 2938 | + location = SVS_TEST_TISSUE_LOCATION |
| 2939 | + size = SVS_TEST_TISSUE_SIZE |
| 2940 | + im_region = wsi.read_rect(location, size, resolution=0, units="level") |
| 2941 | + |
| 2942 | + assert isinstance(im_region, np.ndarray) |
| 2943 | + assert im_region.dtype == "uint8" |
| 2944 | + assert im_region.shape == (*size[::-1], 3) |
| 2945 | + |
| 2946 | + |
| 2947 | +def test_fsspec_reader_open_invalid_json_file(tmp_path: Path) -> None: |
| 2948 | + """Ensure JSONDecodeError is handled properly. |
| 2949 | +
|
| 2950 | + Pass invalid JSON to FsspecJsonWSIReader.is_valid_zarr_fsspec. |
| 2951 | + """ |
| 2952 | + json_path = tmp_path / "invalid.json" |
| 2953 | + json_path.write_text("{invalid json}") # Corrupt JSON |
| 2954 | + |
| 2955 | + assert not FsspecJsonWSIReader.is_valid_zarr_fsspec(str(json_path)) |
| 2956 | + |
| 2957 | + |
| 2958 | +def test_fsspec_reader_open_oserror_handling() -> None: |
| 2959 | + """Ensure OSError is handled properly. |
| 2960 | +
|
| 2961 | + Pass non existent JSON to FsspecJsonWSIReader.is_valid_zarr_fsspec. |
| 2962 | +
|
| 2963 | + """ |
| 2964 | + with patch("builtins.open", side_effect=OSError("File not found")): |
| 2965 | + result = FsspecJsonWSIReader.is_valid_zarr_fsspec("non_existent.json") |
| 2966 | + |
| 2967 | + assert result is False, "Function should return False for OSError" |
| 2968 | + |
| 2969 | + |
| 2970 | +def test_fsspec_reader_open_pass_empty_json(tmp_path: Path) -> None: |
| 2971 | + """Ensure empty JSON is handled properly. |
| 2972 | +
|
| 2973 | + Pass empty JSON to FsspecJsonWSIReader.is_valid_zarr_fsspec and |
| 2974 | +
|
| 2975 | + verify that it's not valid. |
| 2976 | +
|
| 2977 | + """ |
| 2978 | + json_path = tmp_path / "empty.json" |
| 2979 | + json_path.write_text("{}") |
| 2980 | + |
| 2981 | + assert not FsspecJsonWSIReader.is_valid_zarr_fsspec(str(json_path)) |
0 commit comments