diff --git a/src/model_config_tests/qa/test_access_esm1p5_config.py b/src/model_config_tests/qa/test_access_esm1p5_config.py index 86f3e7a..5b83d7b 100644 --- a/src/model_config_tests/qa/test_access_esm1p5_config.py +++ b/src/model_config_tests/qa/test_access_esm1p5_config.py @@ -3,12 +3,164 @@ """ACCESS-ESM1.5 specific configuration tests""" +import re +import warnings +from typing import Any + import pytest +from model_config_tests.util import get_git_branch_name + +### Bunch of expected values for tests ### +VALID_REALMS: set[str] = {"atmosphere", "land", "ocean", "ocnBgchm", "seaIce"} +VALID_KEYWORDS: set[str] = {"global", "access-esm1.5"} +VALID_NOMINAL_RESOLUTION: str = "100 km" +VALID_REFERENCE: str = "https://doi.org/10.1071/ES19035" +VALID_PREINDUSTRIAL_START: dict[str, int] = {"year": 101, "month": 1, "day": 1} +VALID_HISTORICAL_START: dict[str, int] = {"year": 1850, "month": 1, "day": 1} +VALID_RUNTIME: dict[str, int] = {"year": 1, "month": 0, "day": 0} +VALID_RESTART_FREQ: str = "20YS" + + +### Some functions to avoid copying assertion error text +def error_field_nonexistence(field: str, file: str) -> str: + return f"Field '{field}' is null or does not exist in {file}." + + +def error_field_incorrect(field: str, file: str, expected: Any) -> str: + return f"Field '{field}' in {file} is not expected value: {expected}" + + +class AccessEsm1p5Branch: + """Use the naming patterns of the branch name to infer information of + the ACCESS-ESM1.5 config""" + + def __init__(self, branch_name): + self.branch_name = branch_name + self.config_scenario = self.set_config_scenario() + self.config_modifiers = self.set_config_modifiers() + + def set_config_scenario(self) -> str: + # Regex below is split into three sections: + # Config type start section: '^.+-' for 'release-', 'dev-'... + # Scenario section: '([^+]+)' for 'preindustrial', 'historical'...anything that isn't the '+' modifier sigil + # Modifiers end section: '(?:\+.+)*' any amount of '+modifer' sections + scenario_match = re.match(r"^.+-([^+]+)(?:\+.+)*$", self.branch_name) + if len(scenario_match.group) == 0: + pytest.fail( + f"Could not find a scenario in the branch {self.branch_name}. " + + "Branches must be of the form 'type-scenario[+modifier...]'. " + + "See README.md for more information." + ) + return scenario_match.group[0] + + def set_config_modifiers(self) -> list[str]: + # Regex below is essentially 'give me the 'modifier' part in all the '+modifier's in the branch name' + return re.findall(r"\+([^+]+)", self.branch_name) + + +@pytest.fixture(scope="class") +def branch(control_path, target_branch): + branch_name = target_branch + if branch_name is None: + # Default to current branch name + branch_name = get_git_branch_name(control_path) + assert ( + branch_name is not None + ), f"Failed getting git branch name of control path: {control_path}" + warnings.warn( + "Target branch is not specifed, defaulting to current git branch: " + f"{branch_name}. As some ACCESS-ESM1.5 tests infer information, " + "such as resolution, from the target branch name, some tests may " + "not be run. To set use --target-branch flag in pytest call" + ) + + return AccessEsm1p5Branch(branch_name) + @pytest.mark.access_esm1p5 class TestAccessEsm1p5: """ACCESS-ESM1.5 Specific configuration and metadata tests""" - def test_pass(self): - pass + @pytest.mark.parametrize( + "field,expected", [("realm", VALID_REALMS), ("keyword", VALID_KEYWORDS)] + ) + def test_metadata_field_equal_expected_sequence(self, field, expected, metadata): + + assert ( + field in metadata and metadata[field] is not None + ), error_field_nonexistence(field, "metadata.yaml") + + field_set: set[str] = set(metadata[field]) + + assert field_set == expected, error_field_incorrect( + field, "metadata.yaml", "sequence", expected + ) + + @pytest.mark.parametrize( + "field,expected", + [ + ("nominal_resolution", VALID_NOMINAL_RESOLUTION), + ("reference", VALID_REFERENCE), + ], + ) + def test_metadata_field_equal_expected_value(self, field, expected, metadata): + assert field in metadata and metadata[field] == expected, error_field_incorrect( + field, "metadata.yaml", expected + ) + + def test_config_start(self, branch, config): + assert ( + "calendar" in config + and config["calendar"] is not None + and "start" in config["calendar"] + and config["calendar"]["start"] is not None + ), error_field_nonexistence("calendar.start", "config.yaml") + + start: dict[str, int] = config["calendar"]["start"] + + match branch.scenario_name: + case "preindustrial": + assert start == VALID_PREINDUSTRIAL_START, error_field_incorrect( + "calendar.start", "config.yaml", VALID_PREINDUSTRIAL_START + ) + case "historical": + assert start == VALID_HISTORICAL_START, error_field_incorrect( + "calendar.start", "config.yaml", VALID_HISTORICAL_START + ) + case _: + pytest.fail(f"Cannot test unknown scenario {branch.scenario_name}.") + + def test_config_runtime(self, config): + assert ( + "calendar" in config + and config["calendar"] is not None + and "runtime" in config["calendar"] + and config["calendar"]["runtime"] is not None + ), error_field_nonexistence("calendar.runtime", "config.yaml") + + runtime: dict[str, int] = config["calendar"]["runtime"] + + assert runtime == VALID_RUNTIME, error_field_incorrect( + "calendar.runtime", "config.yaml", VALID_RUNTIME + ) + + def test_config_restart_freq(self, config): + assert ( + "restart_freq" in config and config["restart_freq"] is not None + ), error_field_nonexistence("restart_freq", "config.yaml") + assert config["restart_freq"] == VALID_RESTART_FREQ, error_field_incorrect( + "restart_freq", "config.yaml", VALID_RESTART_FREQ + ) + + def test_mppncombine_fast_collate_exe(self, config): + # TODO: We don't check for high resolution here like we do in the ACCESS-OM2 version of the test. Should we? + pattern = r"/g/data/vk83/apps/mppnccombine-fast/.*/bin/mppnccombine-fast" + if "collate" in config: + assert re.match( + pattern, config["collate"]["exe"] + ), "Expect collate executable set to mppnccombine-fast" + + assert config["collate"][ + "mpi" + ], "Expect `mpi: true` when using mppnccombine-fast"