Skip to content

Commit 1cea0bd

Browse files
authored
Merge branch 'dev' into test/remove-duplicate-test-cases
2 parents 737bbbd + 87060c4 commit 1cea0bd

6 files changed

Lines changed: 88 additions & 85 deletions

File tree

monai/apps/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,13 @@
1313

1414
from .datasets import CrossValidation, DecathlonDataset, MedNISTDataset, TciaDataset
1515
from .mmars import MODEL_DESC, RemoteMMARKeys, download_mmar, get_model_spec, load_from_mmar
16-
from .utils import SUPPORTED_HASH_TYPES, check_hash, download_and_extract, download_url, extractall, get_logger, logger
16+
from .utils import (
17+
SUPPORTED_HASH_TYPES,
18+
HashCheckError,
19+
check_hash,
20+
download_and_extract,
21+
download_url,
22+
extractall,
23+
get_logger,
24+
logger,
25+
)

monai/apps/utils.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,24 @@
4242
else:
4343
tqdm, has_tqdm = optional_import("tqdm", "4.47.0", min_version, "tqdm")
4444

45-
__all__ = ["check_hash", "download_url", "extractall", "download_and_extract", "get_logger", "SUPPORTED_HASH_TYPES"]
45+
__all__ = [
46+
"HashCheckError",
47+
"check_hash",
48+
"download_url",
49+
"extractall",
50+
"download_and_extract",
51+
"get_logger",
52+
"SUPPORTED_HASH_TYPES",
53+
]
4654

4755
DEFAULT_FMT = "%(asctime)s - %(levelname)s - %(message)s"
4856
SUPPORTED_HASH_TYPES = {"md5": hashlib.md5, "sha1": hashlib.sha1, "sha256": hashlib.sha256, "sha512": hashlib.sha512}
4957

5058

59+
class HashCheckError(ValueError):
60+
pass
61+
62+
5163
def get_logger(
5264
module_name: str = "monai.apps",
5365
fmt: str = DEFAULT_FMT,
@@ -220,18 +232,15 @@ def download_url(
220232
HTTPError: See urllib.request.urlretrieve.
221233
ContentTooShortError: See urllib.request.urlretrieve.
222234
IOError: See urllib.request.urlretrieve.
223-
RuntimeError: When the hash validation of the ``url`` downloaded file fails.
224-
235+
HashCheckError: When the hash validation of the ``url`` downloaded file fails.
225236
"""
226237
if not filepath:
227238
filepath = Path(".", _basename(url)).resolve()
228239
logger.info(f"Default downloading to '{filepath}'")
229240
filepath = Path(filepath)
230241
if filepath.exists():
231242
if not check_hash(filepath, hash_val, hash_type):
232-
raise RuntimeError(
233-
f"{hash_type} check of existing file failed: filepath={filepath}, expected {hash_type}={hash_val}."
234-
)
243+
raise HashCheckError(f"{hash_type} hash check of existing file failed: {filepath=}, expected {hash_type=}.")
235244
logger.info(f"File exists: {filepath}, skipped downloading.")
236245
return
237246
try:
@@ -260,18 +269,20 @@ def download_url(
260269
raise RuntimeError(
261270
f"Download of file from {url} to {filepath} failed due to network issue or denied permission."
262271
)
272+
if not check_hash(tmp_name, hash_val, hash_type):
273+
raise HashCheckError(
274+
f"{hash_type} hash check of downloaded file failed: {url=}, "
275+
f"{filepath=}, expected {hash_type}={hash_val}, "
276+
f"The file may be corrupted or tampered with. "
277+
"Please retry the download or verify the source."
278+
)
263279
file_dir = filepath.parent
264280
if file_dir:
265281
os.makedirs(file_dir, exist_ok=True)
266282
shutil.move(f"{tmp_name}", f"{filepath}") # copy the downloaded to a user-specified cache.
267283
except (PermissionError, NotADirectoryError): # project-monai/monai issue #3613 #3757 for windows
268284
pass
269285
logger.info(f"Downloaded: {filepath}")
270-
if not check_hash(filepath, hash_val, hash_type):
271-
raise RuntimeError(
272-
f"{hash_type} check of downloaded file failed: URL={url}, "
273-
f"filepath={filepath}, expected {hash_type}={hash_val}."
274-
)
275286

276287

277288
def _extract_zip(filepath, output_dir):
@@ -325,10 +336,15 @@ def extractall(
325336
be False.
326337
327338
Raises:
328-
RuntimeError: When the hash validation of the ``filepath`` compressed file fails.
339+
HashCheckError: When the hash validation of the ``filepath`` compressed file fails.
329340
NotImplementedError: When the ``filepath`` file extension is not one of [zip", "tar.gz", "tar"].
330341
331342
"""
343+
filepath = Path(filepath)
344+
if hash_val and not check_hash(filepath, hash_val, hash_type):
345+
raise HashCheckError(
346+
f"{hash_type} hash check of compressed file failed: " f"{filepath=}, expected {hash_type}={hash_val}."
347+
)
332348
if has_base:
333349
# the extracted files will be in this folder
334350
cache_dir = Path(output_dir, _basename(filepath).split(".")[0])
@@ -337,11 +353,6 @@ def extractall(
337353
if cache_dir.exists() and next(cache_dir.iterdir(), None) is not None:
338354
logger.info(f"Non-empty folder exists in {cache_dir}, skipped extracting.")
339355
return
340-
filepath = Path(filepath)
341-
if hash_val and not check_hash(filepath, hash_val, hash_type):
342-
raise RuntimeError(
343-
f"{hash_type} check of compressed file failed: " f"filepath={filepath}, expected {hash_type}={hash_val}."
344-
)
345356
logger.info(f"Writing into directory: {output_dir}.")
346357
_file_type = file_type.lower().strip()
347358
if filepath.name.endswith("zip") or _file_type == "zip":

monai/bundle/utils.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
from monai.utils import optional_import
2222

2323
yaml, _ = optional_import("yaml")
24-
2524
__all__ = [
2625
"ID_REF_KEY",
2726
"ID_SEP_KEY",
@@ -39,7 +38,6 @@
3938
MERGE_KEY = "+" # prefix indicating merge instead of override in case of multiple configs.
4039

4140
_conf_values = get_config_values()
42-
4341
DEFAULT_METADATA = {
4442
"version": "0.0.1",
4543
"changelog": {"0.0.1": "Initial version"},
@@ -211,20 +209,15 @@ def load_bundle_config(bundle_path: str, *config_names: str, **load_kw_args: Any
211209
name, _ = os.path.splitext(os.path.basename(bundle_path))
212210

213211
archive = zipfile.ZipFile(bundle_path, "r")
214-
215212
all_files = archive.namelist()
216-
217213
zip_meta_name = f"{name}/configs/metadata.json"
218-
219214
if zip_meta_name in all_files:
220215
prefix = f"{name}/configs/" # zipped directory location for files
221216
else:
222217
zip_meta_name = f"{name}/extra/metadata.json"
223218
prefix = f"{name}/extra/" # Torchscript location for files
224-
225219
meta_json = json.loads(archive.read(zip_meta_name))
226220
parser.read_meta(f=meta_json)
227-
228221
for cname in config_names:
229222
full_cname = prefix + cname
230223
if full_cname not in all_files:

tests/apps/test_download_and_extract.py

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,49 +16,68 @@
1616
import unittest
1717
import zipfile
1818
from pathlib import Path
19-
from urllib.error import ContentTooShortError, HTTPError
2019

2120
from parameterized import parameterized
2221

2322
from monai.apps import download_and_extract, download_url, extractall
23+
from monai.apps.utils import HashCheckError
2424
from tests.test_utils import SkipIfNoModule, skip_if_downloading_fails, skip_if_quick, testing_data_config
2525

2626

2727
@SkipIfNoModule("requests")
2828
class TestDownloadAndExtract(unittest.TestCase):
29+
def setUp(self):
30+
self.testing_dir = Path(__file__).parents[1] / "testing_data"
31+
self.config = testing_data_config("images", "mednist")
32+
self.url = self.config["url"]
33+
self.hash_val = self.config["hash_val"]
34+
self.hash_type = self.config["hash_type"]
35+
2936
@skip_if_quick
30-
def test_actions(self):
31-
testing_dir = Path(__file__).parents[1] / "testing_data"
32-
config_dict = testing_data_config("images", "mednist")
33-
url = config_dict["url"]
34-
filepath = Path(testing_dir) / "MedNIST.tar.gz"
35-
output_dir = Path(testing_dir)
36-
hash_val, hash_type = config_dict["hash_val"], config_dict["hash_type"]
37+
def test_download_and_extract_success(self):
38+
"""End-to-end: download and extract should succeed with correct hash."""
39+
filepath = self.testing_dir / "MedNIST.tar.gz"
40+
output_dir = self.testing_dir
41+
3742
with skip_if_downloading_fails():
38-
download_and_extract(url, filepath, output_dir, hash_val=hash_val, hash_type=hash_type)
39-
download_and_extract(url, filepath, output_dir, hash_val=hash_val, hash_type=hash_type)
43+
download_and_extract(self.url, filepath, output_dir, hash_val=self.hash_val, hash_type=self.hash_type)
4044

41-
wrong_md5 = "0"
42-
with self.assertLogs(logger="monai.apps", level="ERROR"):
43-
try:
44-
download_url(url, filepath, wrong_md5)
45-
except (ContentTooShortError, HTTPError, RuntimeError) as e:
46-
if isinstance(e, RuntimeError):
47-
# FIXME: skip MD5 check as current downloading method may fail
48-
self.assertTrue(str(e).startswith("md5 check"))
49-
return # skipping this test due the network connection errors
50-
51-
try:
52-
extractall(filepath, output_dir, wrong_md5)
53-
except RuntimeError as e:
54-
self.assertTrue(str(e).startswith("md5 check"))
45+
self.assertTrue(filepath.exists(), "Downloaded file does not exist")
46+
self.assertTrue(any(output_dir.iterdir()), "Extraction output is empty")
47+
48+
@skip_if_quick
49+
def test_download_url_hash_mismatch(self):
50+
"""download_url should raise HashCheckError on hash mismatch."""
51+
filepath = self.testing_dir / "MedNIST.tar.gz"
52+
53+
with skip_if_downloading_fails():
54+
# First ensure file is downloaded correctly
55+
download_url(self.url, filepath, hash_val=self.hash_val, hash_type=self.hash_type)
56+
57+
# Now test incorrect hash
58+
with self.assertRaises(HashCheckError):
59+
download_url(self.url, filepath, hash_val="0" * len(self.hash_val), hash_type=self.hash_type)
5560

5661
@skip_if_quick
57-
@parameterized.expand((("icon", "tar"), ("favicon", "zip")))
58-
def test_default(self, key, file_type):
62+
def test_extractall_hash_mismatch(self):
63+
"""extractall should raise HashCheckError when hash is incorrect."""
64+
filepath = self.testing_dir / "MedNIST.tar.gz"
65+
output_dir = self.testing_dir
66+
67+
with skip_if_downloading_fails():
68+
download_url(self.url, filepath, hash_val=self.hash_val, hash_type=self.hash_type)
69+
70+
with self.assertRaises(HashCheckError):
71+
extractall(filepath, output_dir, hash_val="0" * len(self.hash_val), hash_type=self.hash_type)
72+
73+
@skip_if_quick
74+
@parameterized.expand([("icon", "tar"), ("favicon", "zip")])
75+
def test_download_and_extract_various_formats(self, key, file_type):
76+
"""Verify different archive formats download and extract correctly."""
5977
with tempfile.TemporaryDirectory() as tmp_dir:
78+
img_spec = testing_data_config("images", key)
79+
6080
with skip_if_downloading_fails():
61-
img_spec = testing_data_config("images", key)
6281
download_and_extract(
6382
img_spec["url"],
6483
output_dir=tmp_dir,
@@ -67,6 +86,8 @@ def test_default(self, key, file_type):
6786
file_type=file_type,
6887
)
6988

89+
self.assertTrue(any(Path(tmp_dir).iterdir()), f"Extraction failed for format: {file_type}")
90+
7091

7192
class TestPathTraversalProtection(unittest.TestCase):
7293
"""Test cases for path traversal attack protection in extractall function."""

tests/test_timedcall_dist.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from tests.test_utils import TimedCall
2020

2121

22-
@TimedCall(seconds=20 if sys.platform == "linux" else 60, force_quit=False)
22+
@TimedCall(seconds=20 if sys.platform == "linux" else 60, force_quit=True)
2323
def case_1_seconds(arg=None):
2424
time.sleep(1)
2525
return "good" if not arg else arg

tests/test_utils.py

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
"unexpected EOF", # incomplete download
8282
"network issue",
8383
"gdown dependency", # gdown not installed
84-
"md5 check",
84+
"hash check", # check hash value of downloaded file
8585
"limit", # HTTP Error 503: Egress is over the account limit
8686
"authenticate",
8787
"timed out", # urlopen error [Errno 110] Connection timed out
@@ -186,37 +186,6 @@ def skip_if_downloading_fails():
186186
raise rt_e
187187

188188

189-
SAMPLE_TIFF = "https://huggingface.co/datasets/MONAI/testing_data/resolve/main/CMU-1.tiff"
190-
SAMPLE_TIFF_HASH = "73a7e89bc15576587c3d68e55d9bf92f09690280166240b48ff4b48230b13bcd"
191-
SAMPLE_TIFF_HASH_TYPE = "sha256"
192-
193-
194-
class TestDownloadUrl(unittest.TestCase):
195-
"""Exercise ``download_url`` success and hash-mismatch paths."""
196-
197-
def test_download_url(self):
198-
"""Download a sample TIFF and validate hash handling.
199-
200-
Raises:
201-
RuntimeError: When the downloaded file's hash does not match.
202-
"""
203-
with tempfile.TemporaryDirectory() as tempdir:
204-
with skip_if_downloading_fails():
205-
download_url(
206-
url=SAMPLE_TIFF,
207-
filepath=os.path.join(tempdir, "model.tiff"),
208-
hash_val=SAMPLE_TIFF_HASH,
209-
hash_type=SAMPLE_TIFF_HASH_TYPE,
210-
)
211-
with self.assertRaises(RuntimeError):
212-
download_url(
213-
url=SAMPLE_TIFF,
214-
filepath=os.path.join(tempdir, "model_bad.tiff"),
215-
hash_val="0" * 64,
216-
hash_type=SAMPLE_TIFF_HASH_TYPE,
217-
)
218-
219-
220189
def test_pretrained_networks(network, input_param, device):
221190
with skip_if_downloading_fails():
222191
return network(**input_param).to(device)

0 commit comments

Comments
 (0)