diff --git a/docs/index.rst b/docs/index.rst index c99bac998..18d538b5b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -162,6 +162,12 @@ make_solo_mosaic | **CLI**: ``uw make_solo_mosaic -h`` | **API**: ``import uwtools.api.make_solo_mosaic`` +orog +"""" + +| **CLI**: ``uw orog -h`` +| **API**: ``import uwtools.api.orog`` + orog_gsl """""""" diff --git a/docs/sections/user_guide/api/index.rst b/docs/sections/user_guide/api/index.rst index cd29d7614..2bdb3dbac 100644 --- a/docs/sections/user_guide/api/index.rst +++ b/docs/sections/user_guide/api/index.rst @@ -19,6 +19,7 @@ API make_solo_mosaic mpas mpas_init + orog orog_gsl rocoto schism diff --git a/docs/sections/user_guide/api/orog.rst b/docs/sections/user_guide/api/orog.rst new file mode 100644 index 000000000..a4f9925f1 --- /dev/null +++ b/docs/sections/user_guide/api/orog.rst @@ -0,0 +1,6 @@ +``uwtools.api.orog`` +==================== + +.. automodule:: uwtools.api.orog + :inherited-members: + :members: diff --git a/docs/sections/user_guide/cli/drivers/index.rst b/docs/sections/user_guide/cli/drivers/index.rst index b61b8242b..f32514cf2 100644 --- a/docs/sections/user_guide/cli/drivers/index.rst +++ b/docs/sections/user_guide/cli/drivers/index.rst @@ -18,6 +18,7 @@ Drivers make_solo_mosaic mpas mpas_init + orog orog_gsl sfc_climo_gen shave diff --git a/docs/sections/user_guide/cli/drivers/orog.rst b/docs/sections/user_guide/cli/drivers/orog.rst new file mode 100644 index 000000000..599c4a020 --- /dev/null +++ b/docs/sections/user_guide/cli/drivers/orog.rst @@ -0,0 +1,62 @@ +``orog`` +======== + +.. include:: /shared/idempotent.rst + +The ``uw`` mode for configuring and running the UFS Utils preprocessing component ``orog``. + +.. literalinclude:: orog/help.cmd + :language: text + :emphasize-lines: 1 +.. literalinclude:: orog/help.out + :language: text + +All tasks take the same arguments. For example: + +.. literalinclude:: orog/run-help.cmd + :language: text + :emphasize-lines: 1 +.. literalinclude:: orog/run-help.out + :language: text + +Examples +^^^^^^^^ + +The examples use a configuration file named ``config.yaml`` with contents similar to: + +.. highlight:: yaml +.. literalinclude:: /shared/orog.yaml + +Its contents are described in section :ref:`orog_yaml`. + +* Run ``orog`` on an interactive node + + .. code-block:: text + + $ uw orog run --config-file config.yaml + + The driver creates a ``runscript.orog`` file in the directory specified by ``rundir:`` in the config and runs it, executing ``orog``. + +* Run ``orog`` via a batch job + + .. code-block:: text + + $ uw orog run --config-file config.yaml --batch + + The driver creates a ``runscript.orog`` file in the directory specified by ``rundir:`` in the config and submits it to the batch system. Running with ``--batch`` requires a correctly configured ``platform:`` block in ``config.yaml``, as well as appropriate settings in the ``execution:`` block under ``orog:``. + +* Specifying the ``--dry-run`` flag results in the driver logging messages about actions it would have taken, without actually taking any. + + .. code-block:: text + + $ uw orog run --config-file config.yaml --batch --dry-run + +.. include:: /shared/key_path.rst + +* Specifying the ``--show-schema`` flag, with no other options, prints the driver's schema: + +.. literalinclude:: orog/show-schema.cmd + :language: text + :emphasize-lines: 1 +.. literalinclude:: orog/show-schema.out + :language: text diff --git a/docs/sections/user_guide/cli/drivers/orog/Makefile b/docs/sections/user_guide/cli/drivers/orog/Makefile new file mode 120000 index 000000000..2486334a6 --- /dev/null +++ b/docs/sections/user_guide/cli/drivers/orog/Makefile @@ -0,0 +1 @@ +../../Makefile.outputs \ No newline at end of file diff --git a/docs/sections/user_guide/cli/drivers/orog/help.cmd b/docs/sections/user_guide/cli/drivers/orog/help.cmd new file mode 100644 index 000000000..99cc5dea8 --- /dev/null +++ b/docs/sections/user_guide/cli/drivers/orog/help.cmd @@ -0,0 +1 @@ +uw orog --help diff --git a/docs/sections/user_guide/cli/drivers/orog/help.out b/docs/sections/user_guide/cli/drivers/orog/help.out new file mode 100644 index 000000000..7db906799 --- /dev/null +++ b/docs/sections/user_guide/cli/drivers/orog/help.out @@ -0,0 +1,28 @@ +usage: uw orog [-h] [--version] [--show-schema] TASK ... + +Execute orog tasks + +Optional arguments: + -h, --help + Show help and exit + --version + Show version info and exit + --show-schema + Show driver schema and exit + +Positional arguments: + TASK + files_linked + Files linked for run + grid_file + The input grid file + input_config_file + The input config file + provisioned_rundir + Run directory provisioned with all required content + run + A run + runscript + The runscript + validate + Validate the UW driver config diff --git a/docs/sections/user_guide/cli/drivers/orog/run-help.cmd b/docs/sections/user_guide/cli/drivers/orog/run-help.cmd new file mode 100644 index 000000000..addf31211 --- /dev/null +++ b/docs/sections/user_guide/cli/drivers/orog/run-help.cmd @@ -0,0 +1 @@ +uw orog run --help diff --git a/docs/sections/user_guide/cli/drivers/orog/run-help.out b/docs/sections/user_guide/cli/drivers/orog/run-help.out new file mode 100644 index 000000000..833c52f8e --- /dev/null +++ b/docs/sections/user_guide/cli/drivers/orog/run-help.out @@ -0,0 +1,26 @@ +usage: uw orog run [-h] [--version] [--config-file PATH] [--batch] [--dry-run] + [--graph-file PATH] [--key-path KEY[.KEY...]] [--quiet] + [--verbose] + +A run + +Optional arguments: + -h, --help + Show help and exit + --version + Show version info and exit + --config-file PATH, -c PATH + Path to UW YAML config file (default: read from stdin) + --batch + Submit run to batch scheduler + --dry-run + Only log info, making no changes + --graph-file PATH + Path to Graphviz DOT output [experimental] + --key-path KEY[.KEY...] + Dot-separated path of keys leading through the config to the driver's + configuration block + --quiet, -q + Print no logging messages + --verbose, -v + Print all logging messages diff --git a/docs/sections/user_guide/cli/drivers/orog/show-schema.cmd b/docs/sections/user_guide/cli/drivers/orog/show-schema.cmd new file mode 100644 index 000000000..62618510d --- /dev/null +++ b/docs/sections/user_guide/cli/drivers/orog/show-schema.cmd @@ -0,0 +1,2 @@ +uw orog --show-schema >schema +head schema && echo ... && tail schema diff --git a/docs/sections/user_guide/cli/drivers/orog/show-schema.out b/docs/sections/user_guide/cli/drivers/orog/show-schema.out new file mode 100644 index 000000000..71494a8b1 --- /dev/null +++ b/docs/sections/user_guide/cli/drivers/orog/show-schema.out @@ -0,0 +1,21 @@ +{ + "properties": { + "orog": { + "additionalProperties": false, + "properties": { + "execution": { + "additionalProperties": false, + "properties": { + "batchargs": { + "additionalProperties": true, +... + "rundir" + ], + "type": "object" + } + }, + "required": [ + "orog" + ], + "type": "object" +} diff --git a/docs/sections/user_guide/yaml/components/index.rst b/docs/sections/user_guide/yaml/components/index.rst index 919a5ac40..550d67867 100644 --- a/docs/sections/user_guide/yaml/components/index.rst +++ b/docs/sections/user_guide/yaml/components/index.rst @@ -16,6 +16,7 @@ UW YAML for Components make_solo_mosaic mpas mpas_init + orog orog_gsl schism sfc_climo_gen diff --git a/docs/sections/user_guide/yaml/components/orog.rst b/docs/sections/user_guide/yaml/components/orog.rst new file mode 100644 index 000000000..4466f80e9 --- /dev/null +++ b/docs/sections/user_guide/yaml/components/orog.rst @@ -0,0 +1,57 @@ +.. _orog_yaml: + +orog +==== + +Structured YAML to run the component ``orog`` is validated by JSON Schema and requires the ``orog:`` block, described below. If ``orog`` is to be run via a batch system, the ``platform:`` block, described :ref:`here `, is also required. + +Documentation for the UFS Utils ``orog`` program is :ufs-utils:`here `. + +Here is a prototype UW YAML ``orog:`` block, explained in detail below: + +.. highlight:: yaml +.. literalinclude:: /shared/orog.yaml + +UW YAML for the ``orog:`` Block +------------------------------- + +old_line1_items +^^^^^^^^^^^^^^^ + +Configuration parameters for the ``orog`` component corresponding to the first line entries prior to hash 57bd832 from (July 9, 2024). If using this section, a value for ``orog_file`` should also be provided. + + +execution: +^^^^^^^^^^ + +See :ref:`here ` for details. + +files_to_copy: +^^^^^^^^^^^^^^ + +See :ref:`this page ` for details. + +files_to_link: +^^^^^^^^^^^^^^ + +Identical to ``files_to_copy:`` except that symbolic links will be created in the run directory instead of copies. + +mask: +^^^^^ + +Boolean indicating whether only the land mask will be generated. Defaults to ``false``. + +merge: +^^^^^^ + +Path to an ocean merge file. + +orog_file: +^^^^^^^^^^ + +Path to an output orography file if using a version of UFS_UTILS prior to hash 57bd832 from (July 9, 2024). If using this section, values for ``old_line1_items`` should also be provided. + +rundir: +^^^^^^^ + +The path to the run directory. diff --git a/docs/shared/mpas.yaml b/docs/shared/mpas.yaml index 8ea7a102b..93ad1b0ea 100644 --- a/docs/shared/mpas.yaml +++ b/docs/shared/mpas.yaml @@ -42,7 +42,7 @@ mpas: nhyd_model: config_dt: 60 validate: true - rundir: /path/to/run/directory + rundir: /path/to/run/dir streams: input: filename_template: conus.init.nc diff --git a/docs/shared/orog.yaml b/docs/shared/orog.yaml new file mode 100644 index 000000000..46df4f687 --- /dev/null +++ b/docs/shared/orog.yaml @@ -0,0 +1,15 @@ +orog: + execution: + batchargs: + cores: 1 + walltime: 00:05:00 + executable: /path/to/orog + files_to_link: + fort.15: /path/to/fix/thirty.second.antarctic.new.bin + landcover30.fixed: /path/to/fix/landcover30.fixed + fort.235: /path/to/fix/gmted2010.30sec.int + grid_file: /path/to/netcdf/grid/file + rundir: /path/to/run/dir +platform: + account: me + scheduler: slurm diff --git a/docs/shared/schism.yaml b/docs/shared/schism.yaml index 66d4bfbb0..b77c8ce8d 100644 --- a/docs/shared/schism.yaml +++ b/docs/shared/schism.yaml @@ -3,4 +3,4 @@ schism: template_file: /path/to/schism/param.nml.IN template_values: dt: 100 - rundir: /path/to/run/directory + rundir: /path/to/run/dir diff --git a/docs/shared/ww3.yaml b/docs/shared/ww3.yaml index ac1cefa83..4800d7e1a 100644 --- a/docs/shared/ww3.yaml +++ b/docs/shared/ww3.yaml @@ -3,4 +3,4 @@ ww3: template_file: /path/to/ww3/ww3_shel.nml.IN template_values: input_forcing_winds: C - rundir: /path/to/run/directory + rundir: /path/to/run/dir diff --git a/src/uwtools/api/orog.py b/src/uwtools/api/orog.py new file mode 100644 index 000000000..5b71576db --- /dev/null +++ b/src/uwtools/api/orog.py @@ -0,0 +1,28 @@ +""" +API access to the ``uwtools`` ``orog`` driver. +""" + +from uwtools.drivers.orog import Orog +from uwtools.drivers.support import graph +from uwtools.drivers.support import tasks as _tasks +from uwtools.utils.api import make_execute as _make_execute + +_driver = Orog +execute = _make_execute(_driver) + + +def schema() -> dict: + """ + Return the driver's schema. + """ + return _driver.schema() + + +def tasks() -> dict[str, str]: + """ + Return a mapping from task names to their one-line descriptions. + """ + return _tasks(_driver) + + +__all__ = ["Orog", "execute", "graph", "schema", "tasks"] diff --git a/src/uwtools/cli.py b/src/uwtools/cli.py index 15f3dda36..c0d0a5a13 100644 --- a/src/uwtools/cli.py +++ b/src/uwtools/cli.py @@ -81,6 +81,7 @@ def main() -> None: STR.makesolomosaic, STR.mpas, STR.mpasinit, + STR.orog, STR.oroggsl, STR.sfcclimogen, STR.shave, @@ -1188,6 +1189,7 @@ def _parse_args(raw_args: list[str]) -> tuple[Args, Checks]: STR.globalequivresol, STR.makehgrid, STR.makesolomosaic, + STR.orog, STR.oroggsl, STR.sfcclimogen, STR.shave, diff --git a/src/uwtools/drivers/orog.py b/src/uwtools/drivers/orog.py new file mode 100644 index 000000000..b82c56857 --- /dev/null +++ b/src/uwtools/drivers/orog.py @@ -0,0 +1,112 @@ +""" +A driver for UFS_UTILS's orog. +""" + +from pathlib import Path + +from iotaa import asset, external, task, tasks + +from uwtools.drivers.driver import DriverTimeInvariant +from uwtools.drivers.support import set_driver_docstring +from uwtools.strings import STR +from uwtools.utils.file import writable +from uwtools.utils.tasks import symlink + + +class Orog(DriverTimeInvariant): + """ + A driver for orog. + """ + + # Workflow tasks + + @tasks + def files_linked(self): + """ + Files linked for run. + """ + yield self.taskname("files linked") + yield [ + symlink(target=Path(target), linkname=self.rundir / linkname) + for linkname, target in self.config.get("files_to_link", {}).items() + ] + + @external + def grid_file(self): + """ + The input grid file. + """ + grid_file = Path(self.config["grid_file"]) + yield self.taskname("Input grid file") + yield asset(grid_file, grid_file.is_file) if str(grid_file) != "none" else None + + @task + def input_config_file(self): + """ + The input config file. + """ + path = self._input_config_path + yield self.taskname(str(path)) + yield asset(path, path.is_file) + yield self.grid_file() + if inputs := self.config.get("old_line1_items"): + ordered_entries = [ + "mtnres", + "lonb", + "latb", + "jcap", + "nr", + "nf1", + "nf2", + "efac", + "blat", + ] + inputs = " ".join([str(inputs[i]) for i in ordered_entries]) + outgrid = self.config["grid_file"] + orogfile = self.config.get("orog_file") + mask_only = ".true." if self.config.get("mask") else ".false." + merge_file = self.config.get("merge", "none") # string none is intentional + content = [i for i in [inputs, outgrid, orogfile, mask_only, merge_file] if i is not None] + with writable(path) as f: + print("\n".join(content), file=f) + + @tasks + def provisioned_rundir(self): + """ + Run directory provisioned with all required content. + """ + yield self.taskname("provisioned run directory") + yield [ + self.files_linked(), + self.input_config_file(), + self.runscript(), + ] + + # Public helper methods + + @classmethod + def driver_name(cls) -> str: + """ + The name of this driver. + """ + return STR.orog + + # Private helper methods + + @property + def _input_config_path(self) -> Path: + """ + Path to the input config file. + """ + return self.rundir / "orog.cfg" + + @property + def _runcmd(self): + """ + The full command-line component invocation. + """ + executable = self.config[STR.execution][STR.executable] + return "%s < %s" % (executable, self._input_config_path.name) + + +set_driver_docstring(Orog) diff --git a/src/uwtools/drivers/orog_gsl.py b/src/uwtools/drivers/orog_gsl.py index bfde683e7..f9dcdb6fe 100644 --- a/src/uwtools/drivers/orog_gsl.py +++ b/src/uwtools/drivers/orog_gsl.py @@ -28,7 +28,7 @@ def input_grid_file(self): self.config["config"][k] for k in ["resolution", "tile", "halo"] ) src = Path(self.config["config"]["input_grid_file"]) - dst = Path(self.config[STR.rundir], fn) + dst = self.rundir / fn yield self.taskname("Input grid") yield asset(dst, dst.is_file) yield symlink(target=src, linkname=dst) @@ -53,7 +53,7 @@ def topo_data_2p5m(self): """ fn = "geo_em.d01.lat-lon.2.5m.HGT_M.nc" src = Path(self.config["config"]["topo_data_2p5m"]) - dst = Path(self.config[STR.rundir], fn) + dst = self.rundir / fn yield self.taskname("Input grid") yield asset(dst, dst.is_file) yield symlink(target=src, linkname=dst) @@ -65,7 +65,7 @@ def topo_data_30s(self): """ fn = "HGT.Beljaars_filtered.lat-lon.30s_res.nc" src = Path(self.config["config"]["topo_data_30s"]) - dst = Path(self.config[STR.rundir], fn) + dst = self.rundir / fn yield self.taskname("Input grid") yield asset(dst, dst.is_file) yield symlink(target=src, linkname=dst) diff --git a/src/uwtools/resources/jsonschema/orog.jsonschema b/src/uwtools/resources/jsonschema/orog.jsonschema new file mode 100644 index 000000000..6798fb1cd --- /dev/null +++ b/src/uwtools/resources/jsonschema/orog.jsonschema @@ -0,0 +1,73 @@ +{ + "properties": { + "orog": { + "additionalProperties": false, + "properties": { + "execution": { + "$ref": "urn:uwtools:execution-serial" + }, + "files_to_link": { + "$ref": "urn:uwtools:files-to-stage" + }, + "grid_file": { + "type": "string" + }, + "mask": { + "type": "boolean" + }, + "merge": { + "type": "string" + }, + "old_line1_items": { + "additionalProperties": false, + "minProperties": 9, + "properties": { + "blat": { + "type": "integer" + }, + "efac": { + "type": "integer" + }, + "jcap": { + "type": "integer" + }, + "latb": { + "type": "integer" + }, + "lonb": { + "type": "integer" + }, + "mtnres": { + "type": "integer" + }, + "nf1": { + "type": "integer" + }, + "nf2": { + "type": "integer" + }, + "nr": { + "type": "integer" + } + } + }, + "orog_file": { + "type": "string" + }, + "rundir": { + "type": "string" + } + }, + "required": [ + "execution", + "grid_file", + "rundir" + ], + "type": "object" + } + }, + "required": [ + "orog" + ], + "type": "object" +} diff --git a/src/uwtools/strings.py b/src/uwtools/strings.py index 098724d62..234ef728a 100644 --- a/src/uwtools/strings.py +++ b/src/uwtools/strings.py @@ -111,6 +111,7 @@ class STR: mpiargs: str = "mpiargs" mpicmd: str = "mpicmd" namelist: str = "namelist" + orog: str = "orog" oroggsl: str = "orog_gsl" outfile: str = "output_file" outfmt: str = "output_format" diff --git a/src/uwtools/tests/api/test_drivers.py b/src/uwtools/tests/api/test_drivers.py index 70bf280b3..231ba131c 100644 --- a/src/uwtools/tests/api/test_drivers.py +++ b/src/uwtools/tests/api/test_drivers.py @@ -19,6 +19,7 @@ make_solo_mosaic, mpas, mpas_init, + orog, orog_gsl, schism, sfc_climo_gen, @@ -43,6 +44,7 @@ make_solo_mosaic, mpas, mpas_init, + orog, orog_gsl, schism, sfc_climo_gen, diff --git a/src/uwtools/tests/drivers/test_orog.py b/src/uwtools/tests/drivers/test_orog.py new file mode 100644 index 000000000..2092dc82e --- /dev/null +++ b/src/uwtools/tests/drivers/test_orog.py @@ -0,0 +1,161 @@ +# pylint: disable=missing-function-docstring,protected-access,redefined-outer-name +""" +Orog driver tests. +""" +import logging +from pathlib import Path +from unittest.mock import DEFAULT as D +from unittest.mock import patch + +from pytest import fixture, mark + +from uwtools.drivers.driver import Driver +from uwtools.drivers.orog import Orog +from uwtools.logging import log +from uwtools.tests.support import regex_logged + +# Fixtures + + +@fixture +def config(tmp_path): + afile = tmp_path / "afile" + afile.touch() + return { + "orog": { + "old_line1_items": { + "blat": 0, + "efac": 0, + "jcap": 0, + "latb": 0, + "lonb": 0, + "mtnres": 1, + "nr": 0, + "nf1": 0, + "nf2": 0, + }, + "execution": { + "batchargs": { + "walltime": "00:01:00", + }, + "executable": "/path/to/orog", + }, + "files_to_link": { + "foo": str(tmp_path / "foo"), + "bar": str(tmp_path / "bar"), + }, + "grid_file": str(tmp_path / "grid_file.in"), + "orog_file": "none", + "rundir": str(tmp_path / "run"), + }, + "platform": { + "account": "me", + "scheduler": "slurm", + }, + } + + +@fixture +def driverobj(config): + return Orog(config=config, batch=True) + + +# Tests + + +@mark.parametrize( + "method", + [ + "_run_resources", + "_run_via_batch_submission", + "_run_via_local_execution", + "_runscript", + "_runscript_done_file", + "_runscript_path", + "_scheduler", + "_validate", + "_write_runscript", + "run", + "runscript", + "taskname", + ], +) +def test_Orog(method): + assert getattr(Orog, method) is getattr(Driver, method) + + +def test_Orog_files_linked(driverobj): + for _, src in driverobj.config["files_to_link"].items(): + Path(src).touch() + for dst, _ in driverobj.config["files_to_link"].items(): + assert not (driverobj.rundir / dst).is_file() + driverobj.files_linked() + for dst, _ in driverobj.config["files_to_link"].items(): + assert (driverobj.rundir / dst).is_symlink() + + +@mark.parametrize("exist", [True, False]) +def test_Orog_grid_file_existence(caplog, driverobj, exist): + log.setLevel(logging.DEBUG) + grid_file = Path(driverobj.config["grid_file"]) + status = "Input grid file: State: Not Ready (external asset)" + if exist: + grid_file.touch() + status = "Input grid file: State: Ready" + driverobj.grid_file() + assert regex_logged(caplog, status) + + +def test_Orog_grid_file_nonexistence(caplog, driverobj): + log.setLevel(logging.INFO) + driverobj._config["grid_file"] = "none" + driverobj.grid_file() + assert regex_logged(caplog, "Input grid file: State: Ready") + + +def test_Orog_input_config_file_new(driverobj): + del driverobj._config["old_line1_items"] + del driverobj._config["orog_file"] + grid_file = Path(driverobj.config["grid_file"]) + grid_file.touch() + driverobj.input_config_file() + with open(driverobj._input_config_path, "r", encoding="utf-8") as inps: + content = inps.readlines() + content = [l.strip("\n") for l in content] + assert len(content) == 3 + assert content[0] == driverobj.config["grid_file"] + assert content[1] == ".false." + assert content[2] == "none" + + +def test_Orog_input_config_file_old(driverobj): + grid_file = Path(driverobj.config["grid_file"]) + grid_file.touch() + driverobj.input_config_file() + with open(driverobj._input_config_path, "r", encoding="utf-8") as inps: + content = inps.readlines() + content = [l.strip("\n") for l in content] + assert len(content) == 5 + assert len(content[0].split()) == 9 + assert content[1] == driverobj.config["grid_file"] + assert content[2] == driverobj.config.get("orog_file") + assert content[3] == ".false." + assert content[4] == "none" + + +def test_Orog_provisioned_rundir(driverobj): + with patch.multiple(driverobj, files_linked=D, input_config_file=D, runscript=D) as mocks: + driverobj.provisioned_rundir() + for m in mocks: + mocks[m].assert_called_once_with() + + +def test_Orog_driver_name(driverobj): + assert driverobj.driver_name() == Orog.driver_name() == "orog" + + +def test_Orog__runcmd(driverobj): + assert driverobj._runcmd == "%s < %s" % ( + driverobj.config["execution"]["executable"], + driverobj._input_config_path.name, + ) diff --git a/src/uwtools/tests/test_schemas.py b/src/uwtools/tests/test_schemas.py index ee0dd3043..cd549a0eb 100644 --- a/src/uwtools/tests/test_schemas.py +++ b/src/uwtools/tests/test_schemas.py @@ -1551,6 +1551,58 @@ def test_schema_namelist(): assert "[] is not of type 'object'\n" in errors({"namelist": []}) +# orog + + +def test_schema_orog(): + config: dict = { + "execution": { + "executable": "/path/to/orog", + }, + "grid_file": "/path/to/grid/file", + "mask": False, + "merge": "none", + "old_line1_items": { + "blat": 0, + "efac": 0, + "jcap": 0, + "latb": 0, + "lonb": 0, + "mtnres": 1, + "nr": 0, + "nf1": 0, + "nf2": 0, + }, + "orog_file": "/path/to/orog/file", + "rundir": "/path/to/run/dir", + } + + errors = schema_validator("orog", "properties", "orog") + # Basic correctness: + assert not errors(config) + # All 9 config keys are required: + assert "does not have enough properties" in errors(with_del(config, "old_line1_items", "blat")) + # Other config keys are not allowed: + assert "Additional properties are not allowed" in errors( + with_set(config, "bar", "old_line1_items", "foo") + ) + # All old_line1_items keys require integer values: + for key in config["old_line1_items"]: + assert "is not of type 'integer'\n" in errors( + with_set(config, None, "old_line1_items", key) + ) + # Some top level keys are required: + for key in ["execution", "grid_file", "rundir"]: + assert f"'{key}' is a required property" in errors(with_del(config, key)) + # Other top-level keys are not allowed: + assert "Additional properties are not allowed" in errors(with_set(config, "bar", "foo")) + # The mask key requires boolean values: + assert "is not of type 'boolean'\n" in errors({"mask": None}) + # Top-level keys require a string value: + for key in ["grid_file", "rundir", "merge", "orog_file"]: + assert "is not of type 'string'\n" in errors(with_set(config, None, "rundir")) + + # orog-gsl