Skip to content

Commit 28eda4c

Browse files
committed
Fix: Prevent Silent Exception Swallowing in Model Loading
1 parent c4dd957 commit 28eda4c

5 files changed

Lines changed: 136 additions & 25 deletions

File tree

perceptionmetrics/models/torch_detection.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -263,23 +263,22 @@ def __init__(
263263

264264
# Load model from file or use passed instance
265265
if isinstance(model, str):
266-
assert os.path.isfile(model), "Torch model file not found"
266+
if not os.path.isfile(model):
267+
raise FileNotFoundError(f"Model file not found: {model}")
267268
model_fname = model
268269
try:
269270
model = torch.jit.load(model, map_location=self.device)
270271
model_type = "compiled"
271-
except RuntimeError:
272+
except (RuntimeError, EOFError) as jit_err:
273+
# TorchScript load failed, try loading as native PyTorch
272274
try:
273-
model = torch.load(
274-
model, map_location=self.device, weights_only=False
275-
)
275+
model = torch.load(model, map_location=self.device, weights_only=False)
276276
model_type = "native"
277-
except Exception as e:
278-
raise ValueError(
279-
f"Failed to load model. "
280-
f"Ensure it is a valid PyTorch or TorchScript model. Error : {e}"
281-
)
282-
277+
except Exception as load_err:
278+
raise RuntimeError(
279+
f"Failed to load model as TorchScript or PyTorch module. "
280+
f"TorchScript error: {jit_err}. PyTorch error: {load_err}"
281+
) from load_err
283282
elif isinstance(model, torch.nn.Module):
284283
model_fname = None
285284
model_type = "native"

perceptionmetrics/models/torch_segmentation.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -217,15 +217,21 @@ def __init__(
217217

218218
# If 'model' contains a string, check that it is a valid filename and load model
219219
if isinstance(model, str):
220-
assert os.path.isfile(model), "TorchScript Model file not found"
220+
if not os.path.isfile(model):
221+
raise FileNotFoundError(f"Model file not found: {model}")
221222
model_fname = model
222223
try:
223224
model = torch.jit.load(model, map_location=self.device)
224225
model_type = "compiled"
225-
except:
226-
print("Model is not a TorchScript model. Loading as a PyTorch module.")
227-
model = torch.load(model, map_location=self.device)
228-
model_type = "native"
226+
except (RuntimeError, EOFError) as jit_err:
227+
try:
228+
model = torch.load(model, map_location=self.device, weights_only=False)
229+
model_type = "native"
230+
except Exception as load_err:
231+
raise RuntimeError(
232+
f"Failed to load model as TorchScript or PyTorch module. "
233+
f"TorchScript error: {jit_err}. PyTorch error: {load_err}"
234+
) from load_err
229235
# Otherwise, check that it is a PyTorch module
230236
elif isinstance(model, torch.nn.Module):
231237
model_fname = None
@@ -563,15 +569,22 @@ def __init__(
563569

564570
# If 'model' contains a string, check that it is a valid filename and load model
565571
if isinstance(model, str):
566-
assert os.path.isfile(model), "TorchScript Model file not found"
572+
if not os.path.isfile(model):
573+
raise FileNotFoundError(f"Model file not found: {model}")
567574
model_fname = model
568575
try:
569576
model = torch.jit.load(model, map_location=self.device)
570577
model_type = "compiled"
571-
except Exception:
572-
print("Model is not a TorchScript model. Loading as a PyTorch module.")
573-
model = torch.load(model, map_location=self.device)
574-
model_type = "native"
578+
except (RuntimeError, EOFError) as jit_err:
579+
# TorchScript load failed, try loading as native PyTorch
580+
try:
581+
model = torch.load(model, map_location=self.device, weights_only=False)
582+
model_type = "native"
583+
except Exception as load_err:
584+
raise RuntimeError(
585+
f"Failed to load model as TorchScript or PyTorch module. "
586+
f"TorchScript error: {jit_err}. PyTorch error: {load_err}"
587+
) from load_err
575588

576589
# Otherwise, check that it is a PyTorch module
577590
elif isinstance(model, torch.nn.Module):

perceptionmetrics/models/utils/o3d/kpconv.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
try:
66
from open3d._ml3d.torch.models.kpconv import batch_grid_subsampling, batch_neighbors
7-
except Exception:
8-
print("Open3D-ML3D not available")
7+
except ImportError:
8+
print("Warning: Open3D-ML3D not available. KPConv functionality will be limited.")
99
import torch
1010

1111
import perceptionmetrics.utils.lidar as ul

perceptionmetrics/models/utils/o3d/randlanet.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
try:
66
from open3d._ml3d.datasets.utils import DataProcessing
7-
except Exception:
8-
print("Open3D-ML3D not available")
7+
except ImportError:
8+
print("Warning: Open3D-ML3D not available. RandLANet functionality will be limited.")
99
import torch
1010

1111
import perceptionmetrics.utils.lidar as ul

tests/test_model_loading.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Unit tests for model loading robustness"""
2+
import os
3+
import tempfile
4+
import pytest
5+
import torch
6+
from PIL import Image
7+
8+
from perceptionmetrics.models.torch_detection import TorchImageDetectionModel
9+
from perceptionmetrics.models.torch_segmentation import TorchImageSegmentationModel
10+
11+
12+
class TestModelLoadingExceptions:
13+
"""Test that model loading raises appropriate exceptions"""
14+
15+
@pytest.fixture
16+
def temp_files(self):
17+
"""Create temporary test files"""
18+
with tempfile.TemporaryDirectory() as tmpdir:
19+
# Create dummy model file (corrupted)
20+
bad_model = os.path.join(tmpdir, "bad_model.pt")
21+
with open(bad_model, "w") as f:
22+
f.write("this is not a pytorch model")
23+
24+
# Create dummy ontology
25+
ontology = os.path.join(tmpdir, "ontology.json")
26+
import json
27+
with open(ontology, "w") as f:
28+
json.dump({
29+
"car": {"idx": 0, "rgb": [0, 0, 0]},
30+
"person": {"idx": 1, "rgb": [255, 0, 0]},
31+
}, f)
32+
33+
# Create dummy config
34+
config = os.path.join(tmpdir, "config.json")
35+
with open(config, "w") as f:
36+
json.dump({
37+
"resize": {"width": 512, "height": 512},
38+
"normalization": {
39+
"mean": [0.485, 0.456, 0.406],
40+
"std": [0.229, 0.224, 0.225]
41+
},
42+
"batch_size": 1,
43+
"model_format": "torchvision"
44+
}, f)
45+
46+
yield {
47+
"bad_model": bad_model,
48+
"ontology": ontology,
49+
"config": config,
50+
"tmpdir": tmpdir
51+
}
52+
53+
def test_detection_model_bad_file_raises_specific_error(self, temp_files):
54+
"""Test that loading corrupted model raises RuntimeError, not generic Exception"""
55+
with pytest.raises(RuntimeError) as exc_info:
56+
TorchImageDetectionModel(
57+
model=temp_files["bad_model"],
58+
model_cfg=temp_files["config"],
59+
ontology_fname=temp_files["ontology"]
60+
)
61+
62+
# Check error message is informative
63+
error_msg = str(exc_info.value)
64+
assert "Failed to load model" in error_msg
65+
assert "TorchScript error" in error_msg or "PyTorch error" in error_msg
66+
67+
def test_segmentation_model_bad_file_raises_specific_error(self, temp_files):
68+
"""Test that loading corrupted segmentation model raises RuntimeError"""
69+
with pytest.raises(RuntimeError) as exc_info:
70+
TorchImageSegmentationModel(
71+
model=temp_files["bad_model"],
72+
model_cfg=temp_files["config"],
73+
ontology_fname=temp_files["ontology"]
74+
)
75+
76+
error_msg = str(exc_info.value)
77+
assert "Failed to load model" in error_msg
78+
79+
def test_detection_model_missing_file_raises_file_not_found(self, temp_files):
80+
"""Test that missing model file raises FileNotFoundError"""
81+
with pytest.raises(FileNotFoundError) as exc_info:
82+
TorchImageDetectionModel(
83+
model="/nonexistent/path/model.pt",
84+
model_cfg=temp_files["config"],
85+
ontology_fname=temp_files["ontology"]
86+
)
87+
88+
assert "Model file not found" in str(exc_info.value)
89+
90+
def test_segmentation_model_missing_file_raises_file_not_found(self, temp_files):
91+
"""Test that missing segmentation model file raises FileNotFoundError"""
92+
with pytest.raises(FileNotFoundError) as exc_info:
93+
TorchImageSegmentationModel(
94+
model="/nonexistent/path/model.pt",
95+
model_cfg=temp_files["config"],
96+
ontology_fname=temp_files["ontology"]
97+
)
98+
99+
assert "Model file not found" in str(exc_info.value)

0 commit comments

Comments
 (0)