@@ -217,3 +217,149 @@ def test_load_bad_model():
217217 load_model ("bad_serialized_model" )
218218 finally :
219219 shutil .rmtree ("bad_serialized_model" )
220+
221+
222+ def test_load_model_wraps_unknown_error_as_runtime_error (tmp_path ):
223+ """Non-(ImportError, ValueError) exceptions should be re-raised as ``RuntimeError``."""
224+ serialized_dir = tmp_path / "weird_model"
225+ serialized_dir .mkdir ()
226+ for fn in ("model.pt" , "state.pt" , "model.json" ):
227+ (serialized_dir / fn ).write_text ("doesn't matter" )
228+
229+ def boom (* _args , ** _kwargs ):
230+ raise KeyboardInterrupt ("boom" )
231+
232+ with (
233+ patch .object (matgl_io , "_get_file_paths" , side_effect = boom ),
234+ pytest .raises (RuntimeError , match = "Unknown error occurred while loading model" ),
235+ ):
236+ load_model (serialized_dir )
237+
238+
239+ def test_get_file_paths_malformed_identifier_raises_value_error (tmp_path ):
240+ """An identifier that contains ``/`` but doesn't match a valid HF repo id must
241+ fail with a clear ``ValueError`` rather than attempting a network call."""
242+ from matgl .utils .io import _get_file_paths
243+
244+ # Doesn't exist locally and doesn't match ``owner/name`` (leading slash, double slash).
245+ bogus = "//bad//repo//id"
246+ with pytest .raises (ValueError , match = r"No valid model found locally or at Hugging Face Hub" ):
247+ _get_file_paths (tmp_path / bogus , str_path = bogus )
248+
249+
250+ def test_get_file_paths_bare_name_hub_failure_raises_value_error (tmp_path ):
251+ """When a bare name fails to download from the materialyze HF org, raise ``ValueError``."""
252+ from matgl .utils .io import _get_file_paths
253+
254+ def boom (* _args , ** _kwargs ):
255+ raise RuntimeError ("simulated hub failure" )
256+
257+ with (
258+ patch .object (matgl_io , "_download_from_hf_hub" , side_effect = boom ),
259+ pytest .raises (ValueError , match = r"No valid model found locally or at Hugging Face repo" ),
260+ ):
261+ _get_file_paths (tmp_path / "BareName" , str_path = "BareName" )
262+
263+
264+ def test_iomixin_load_dgl_class_under_pyg_warns ():
265+ """Loading a model whose nested kwargs reference a DGL-only class under PYG must warn.
266+
267+ Triggers the branch that auto-flips the backend to DGL when a serialized model has a
268+ nested component class name containing ``m3gnet`` / ``megnet`` / ``chgnet`` / ``qet``.
269+ """
270+ import matgl as _matgl
271+
272+ if _matgl .config .BACKEND != "PYG" :
273+ pytest .skip ("Only meaningful on the PyG backend." )
274+
275+ # IOMixIn.load expects a dict-of-paths or a Path. Pre-build the artifacts on disk.
276+ import json as _json
277+ import tempfile
278+
279+ with tempfile .TemporaryDirectory () as tmpdir :
280+ tmp_path = Path (tmpdir )
281+ nested = {
282+ "@class" : "M3GNet" ,
283+ "@module" : "definitely.not.a.real.module.at.all" ,
284+ "@model_version" : 1 ,
285+ "init_args" : {},
286+ }
287+ init_args = {"n" : 1 , "submodel" : nested }
288+
289+ torch .save (init_args , tmp_path / "model.pt" )
290+ torch .save ({}, tmp_path / "state.pt" )
291+ (tmp_path / "model.json" ).write_text (
292+ _json .dumps (
293+ {
294+ "@class" : "OldModel" ,
295+ "@module" : "tests.utils.test_io" ,
296+ "@model_version" : 1 ,
297+ "metadata" : None ,
298+ "kwargs" : init_args ,
299+ }
300+ )
301+ )
302+
303+ # ``matgl.set_backend("DGL")`` would mutate global state and may fail if DGL
304+ # isn't installed; patch it to a no-op so the test is self-contained.
305+ with (
306+ patch .object (_matgl , "set_backend" ) as mock_set_backend ,
307+ pytest .warns (UserWarning , match = r"Setting the backend to DGL" ),
308+ pytest .raises ((ImportError , ValueError , ModuleNotFoundError )),
309+ ):
310+ OldModel .load (tmp_path )
311+
312+ mock_set_backend .assert_called_with ("DGL" )
313+
314+
315+ def test_generate_hf_model_card_with_unserializable_metadata ():
316+ """``_generate_hf_model_card`` must swallow ``TypeError`` from non-serializable metadata.
317+
318+ Forces the ``json.dumps`` fallback to fail even with ``default=str`` by using an
319+ object whose ``__repr__`` raises (and therefore so does ``str(obj)``).
320+ """
321+ from matgl .utils .io import _generate_hf_model_card
322+
323+ class Unserializable :
324+ def __repr__ (self ):
325+ raise TypeError ("repr exploded" )
326+
327+ model = OldModel (1 )
328+ card = _generate_hf_model_card (model , metadata = {"oops" : Unserializable ()})
329+
330+ assert "## Metadata" not in card
331+ assert "OldModel" in card
332+
333+
334+ def test_get_available_pretrained_models_handles_hub_errors ():
335+ """If the HF hub call fails, ``get_available_pretrained_models`` returns an empty list."""
336+
337+ class _BoomApi :
338+ def list_models (self , ** _kwargs ):
339+ raise RuntimeError ("network is down" )
340+
341+ with patch .object (matgl_io , "HfApi" , return_value = _BoomApi ()):
342+ names = get_available_pretrained_models ()
343+
344+ assert names == []
345+
346+
347+ def test_get_available_pretrained_models_strips_owner_prefix ():
348+ """Returned names should be bare (no ``"owner/"`` prefix) and sorted."""
349+
350+ class _FakeModelInfo :
351+ def __init__ (self , repo_id : str ):
352+ self .id = repo_id
353+
354+ class _FakeApi :
355+ def list_models (self , ** _kwargs ):
356+ return [
357+ _FakeModelInfo ("materialyze/Zeta" ),
358+ _FakeModelInfo ("materialyze/Alpha" ),
359+ _FakeModelInfo ("no-slash-id" ), # malformed entries are silently skipped
360+ ]
361+
362+ with patch .object (matgl_io , "HfApi" , return_value = _FakeApi ()):
363+ names = get_available_pretrained_models ()
364+
365+ assert names == ["Alpha" , "Zeta" ]
0 commit comments