@@ -311,3 +311,234 @@ def test_weather_model_mapping_exposes_legacy_aliases():
311311
312312 assert mapping .get ("GFS_LEGACY" )["temperature" ] == "tmpprs"
313313 assert mapping .get ("gfs_legacy" )["temperature" ] == "tmpprs"
314+
315+
316+ def test_dictionary_matches_dataset_rejects_missing_projection (example_plain_env ):
317+ """Reject mapping when projection key is declared but variable is missing."""
318+ # Arrange
319+ mapping = {
320+ "time" : "time" ,
321+ "latitude" : "y" ,
322+ "longitude" : "x" ,
323+ "projection" : "LambertConformal_Projection" ,
324+ "level" : "isobaric" ,
325+ "temperature" : "Temperature_isobaric" ,
326+ "geopotential_height" : "Geopotential_height_isobaric" ,
327+ "geopotential" : None ,
328+ "u_wind" : "u-component_of_wind_isobaric" ,
329+ "v_wind" : "v-component_of_wind_isobaric" ,
330+ }
331+ dataset = _DummyDataset (
332+ [
333+ "time" ,
334+ "y" ,
335+ "x" ,
336+ "isobaric" ,
337+ "Temperature_isobaric" ,
338+ "Geopotential_height_isobaric" ,
339+ "u-component_of_wind_isobaric" ,
340+ "v-component_of_wind_isobaric" ,
341+ ]
342+ )
343+
344+ # Act
345+ is_compatible = example_plain_env ._Environment__dictionary_matches_dataset (
346+ mapping , dataset
347+ )
348+
349+ # Assert
350+ assert not is_compatible
351+
352+
353+ def test_dictionary_matches_dataset_accepts_geopotential_only (example_plain_env ):
354+ """Accept mapping when geopotential exists and geopotential height is absent."""
355+ # Arrange
356+ mapping = {
357+ "time" : "time" ,
358+ "latitude" : "latitude" ,
359+ "longitude" : "longitude" ,
360+ "level" : "level" ,
361+ "temperature" : "t" ,
362+ "geopotential_height" : None ,
363+ "geopotential" : "z" ,
364+ "u_wind" : "u" ,
365+ "v_wind" : "v" ,
366+ }
367+ dataset = _DummyDataset (
368+ [
369+ "time" ,
370+ "latitude" ,
371+ "longitude" ,
372+ "level" ,
373+ "t" ,
374+ "z" ,
375+ "u" ,
376+ "v" ,
377+ ]
378+ )
379+
380+ # Act
381+ is_compatible = example_plain_env ._Environment__dictionary_matches_dataset (
382+ mapping , dataset
383+ )
384+
385+ # Assert
386+ assert is_compatible
387+
388+
389+ def test_resolve_dictionary_warns_when_falling_back (example_plain_env ):
390+ """Emit warning and return a built-in mapping when fallback is required."""
391+ # Arrange
392+ incompatible_mapping = {
393+ "time" : "bad_time" ,
394+ "latitude" : "bad_lat" ,
395+ "longitude" : "bad_lon" ,
396+ "level" : "bad_level" ,
397+ "temperature" : "bad_temp" ,
398+ "geopotential_height" : "bad_height" ,
399+ "geopotential" : None ,
400+ "u_wind" : "bad_u" ,
401+ "v_wind" : "bad_v" ,
402+ }
403+ dataset = _DummyDataset (
404+ [
405+ "time" ,
406+ "lat" ,
407+ "lon" ,
408+ "isobaric" ,
409+ "Temperature_isobaric" ,
410+ "Geopotential_height_isobaric" ,
411+ "u-component_of_wind_isobaric" ,
412+ "v-component_of_wind_isobaric" ,
413+ ]
414+ )
415+
416+ # Act
417+ with pytest .warns (UserWarning , match = "Falling back to built-in mapping" ):
418+ resolved = example_plain_env ._Environment__resolve_dictionary_for_dataset (
419+ incompatible_mapping , dataset
420+ )
421+
422+ # Assert
423+ assert resolved == example_plain_env ._Environment__weather_model_map .get ("GFS" )
424+
425+
426+ def test_resolve_dictionary_returns_original_when_no_compatible_builtin (
427+ example_plain_env ,
428+ ):
429+ """Return original mapping unchanged when no built-in mapping can match."""
430+ # Arrange
431+ original_mapping = {
432+ "time" : "a" ,
433+ "latitude" : "b" ,
434+ "longitude" : "c" ,
435+ "level" : "d" ,
436+ "temperature" : "e" ,
437+ "geopotential_height" : "f" ,
438+ "geopotential" : None ,
439+ "u_wind" : "g" ,
440+ "v_wind" : "h" ,
441+ }
442+ dataset = _DummyDataset (["foo" , "bar" ])
443+
444+ # Act
445+ resolved = example_plain_env ._Environment__resolve_dictionary_for_dataset (
446+ original_mapping , dataset
447+ )
448+
449+ # Assert
450+ assert resolved is original_mapping
451+
452+
453+ @pytest .mark .parametrize (
454+ "model_type,file_name,error_message" ,
455+ [
456+ (
457+ "Forecast" ,
458+ "hiresw" ,
459+ "HIRESW latest-model shortcut is currently unavailable" ,
460+ ),
461+ (
462+ "Ensemble" ,
463+ "gefs" ,
464+ "GEFS latest-model shortcut is currently unavailable" ,
465+ ),
466+ ],
467+ )
468+ def test_set_atmospheric_model_blocks_deactivated_shortcuts_case_insensitive (
469+ example_plain_env ,
470+ model_type ,
471+ file_name ,
472+ error_message ,
473+ ):
474+ """Reject deactivated shortcut aliases regardless of input string case."""
475+ # Arrange
476+ environment = example_plain_env
477+
478+ # Act / Assert
479+ with pytest .raises (ValueError , match = error_message ):
480+ environment .set_atmospheric_model (type = model_type , file = file_name )
481+
482+
483+ def test_validate_dictionary_uses_case_insensitive_file_shortcut (example_plain_env ):
484+ """Infer built-in mapping from file shortcut even when shortcut is lowercase."""
485+ # Arrange
486+ environment = example_plain_env
487+
488+ # Act
489+ mapping = environment ._Environment__validate_dictionary ("gfs" , None )
490+
491+ # Assert
492+ assert mapping == environment ._Environment__weather_model_map .get ("GFS" )
493+
494+
495+ def test_validate_dictionary_raises_type_error_for_invalid_dictionary (
496+ example_plain_env ,
497+ ):
498+ """Raise TypeError when no valid dictionary can be inferred."""
499+ # Arrange
500+ environment = example_plain_env
501+
502+ # Act / Assert
503+ with pytest .raises (TypeError , match = "Please specify a dictionary" ):
504+ environment ._Environment__validate_dictionary ("not_a_model" , None )
505+
506+
507+ def test_set_atmospheric_model_normalizes_shortcut_case_for_forecast (example_plain_env ):
508+ """Normalize shortcut name before lookup and process forecast data."""
509+ # Arrange
510+ environment = example_plain_env
511+
512+ environment ._Environment__atm_type_file_to_function_map = {
513+ "forecast" : {
514+ "GFS" : lambda : "fake-dataset" ,
515+ },
516+ "ensemble" : {},
517+ }
518+
519+ called_arguments = {}
520+
521+ def fake_process_forecast_reanalysis (dataset , dictionary ):
522+ called_arguments ["dataset" ] = dataset
523+ called_arguments ["dictionary" ] = dictionary
524+
525+ environment .process_forecast_reanalysis = fake_process_forecast_reanalysis
526+
527+ # Act
528+ environment .set_atmospheric_model (type = "Forecast" , file = "gfs" )
529+
530+ # Assert
531+ assert called_arguments ["dataset" ] == "fake-dataset"
532+ assert called_arguments [
533+ "dictionary"
534+ ] == environment ._Environment__weather_model_map .get ("GFS" )
535+
536+
537+ def test_set_atmospheric_model_raises_for_unknown_model_type (example_plain_env ):
538+ """Raise ValueError for unknown atmospheric model selector."""
539+ # Arrange
540+ environment = example_plain_env
541+
542+ # Act / Assert
543+ with pytest .raises (ValueError , match = "Unknown model type" ):
544+ environment .set_atmospheric_model (type = "unknown_type" )
0 commit comments