From 71bf1e869d5d1fb0a4eab6bf4b8c42df3a9f22ec Mon Sep 17 00:00:00 2001 From: Paul Madden <136389411+maddenp-noaa@users.noreply.github.com> Date: Thu, 13 Jun 2024 13:01:06 -0600 Subject: [PATCH] UW-607 orog_gsl driver (#509) --- docs/index.rst | 6 + docs/sections/user_guide/api/index.rst | 1 + docs/sections/user_guide/api/orog_gsl.rst | 5 + .../sections/user_guide/cli/drivers/index.rst | 1 + .../user_guide/cli/drivers/orog_gsl.rst | 58 ++++++++ .../user_guide/cli/drivers/orog_gsl/Makefile | 1 + .../user_guide/cli/drivers/orog_gsl/help.cmd | 1 + .../user_guide/cli/drivers/orog_gsl/help.out | 26 ++++ .../cli/drivers/orog_gsl/run-help.cmd | 1 + .../cli/drivers/orog_gsl/run-help.out | 26 ++++ .../yaml/components/global_equiv_resol.rst | 2 +- .../user_guide/yaml/components/index.rst | 1 + .../yaml/components/make_solo_mosaic.rst | 2 +- .../user_guide/yaml/components/orog_gsl.rst | 55 ++++++++ docs/shared/orog_gsl.yaml | 20 +++ src/uwtools/api/orog_gsl.py | 12 ++ src/uwtools/cli.py | 2 + src/uwtools/drivers/driver.py | 4 +- src/uwtools/drivers/esg_grid.py | 2 +- src/uwtools/drivers/global_equiv_resol.py | 2 +- src/uwtools/drivers/jedi.py | 2 +- src/uwtools/drivers/make_hgrid.py | 2 +- src/uwtools/drivers/make_solo_mosaic.py | 2 +- src/uwtools/drivers/mpas_base.py | 2 +- src/uwtools/drivers/orog_gsl.py | 117 ++++++++++++++++ src/uwtools/drivers/sfc_climo_gen.py | 2 +- src/uwtools/drivers/shave.py | 10 +- src/uwtools/drivers/ungrib.py | 2 +- src/uwtools/drivers/upp.py | 2 +- src/uwtools/drivers/ww3.py | 5 +- .../resources/jsonschema/orog-gsl.jsonschema | 55 ++++++++ .../resources/jsonschema/shave.jsonschema | 2 +- src/uwtools/strings.py | 1 + src/uwtools/tests/api/test_drivers.py | 2 + src/uwtools/tests/drivers/test_chgres_cube.py | 13 +- src/uwtools/tests/drivers/test_driver.py | 10 +- src/uwtools/tests/drivers/test_esg_grid.py | 13 +- src/uwtools/tests/drivers/test_fv3.py | 12 +- .../tests/drivers/test_global_equiv_resol.py | 13 +- src/uwtools/tests/drivers/test_jedi.py | 16 +-- src/uwtools/tests/drivers/test_make_hgrid.py | 30 ++-- .../tests/drivers/test_make_solo_mosaic.py | 13 +- src/uwtools/tests/drivers/test_mpas.py | 60 ++++---- src/uwtools/tests/drivers/test_mpas_init.py | 13 +- src/uwtools/tests/drivers/test_orog_gsl.py | 131 ++++++++++++++++++ .../tests/drivers/test_sfc_climo_gen.py | 34 ++--- src/uwtools/tests/drivers/test_shave.py | 17 +-- src/uwtools/tests/drivers/test_ungrib.py | 13 +- src/uwtools/tests/drivers/test_upp.py | 13 +- src/uwtools/tests/drivers/test_ww3.py | 12 +- src/uwtools/tests/test_schemas.py | 51 ++++++- 51 files changed, 678 insertions(+), 220 deletions(-) create mode 100644 docs/sections/user_guide/api/orog_gsl.rst create mode 100644 docs/sections/user_guide/cli/drivers/orog_gsl.rst create mode 120000 docs/sections/user_guide/cli/drivers/orog_gsl/Makefile create mode 100644 docs/sections/user_guide/cli/drivers/orog_gsl/help.cmd create mode 100644 docs/sections/user_guide/cli/drivers/orog_gsl/help.out create mode 100644 docs/sections/user_guide/cli/drivers/orog_gsl/run-help.cmd create mode 100644 docs/sections/user_guide/cli/drivers/orog_gsl/run-help.out create mode 100644 docs/sections/user_guide/yaml/components/orog_gsl.rst create mode 100644 docs/shared/orog_gsl.yaml create mode 100644 src/uwtools/api/orog_gsl.py create mode 100644 src/uwtools/drivers/orog_gsl.py create mode 100644 src/uwtools/resources/jsonschema/orog-gsl.jsonschema create mode 100644 src/uwtools/tests/drivers/test_orog_gsl.py diff --git a/docs/index.rst b/docs/index.rst index bdfc5515d..b162de430 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -133,6 +133,12 @@ make_solo_mosaic | **CLI**: ``uw make_solo_mosaic -h`` | **API**: ``import uwtools.api.make_solo_mosaic`` +orog_gsl +"""""""" + +| **CLI**: ``uw orog_gsl -h`` +| **API**: ``import uwtools.api.orog_gsl`` + sfc_climo_gen """"""""""""" diff --git a/docs/sections/user_guide/api/index.rst b/docs/sections/user_guide/api/index.rst index cd3bf0ca0..928f081d0 100644 --- a/docs/sections/user_guide/api/index.rst +++ b/docs/sections/user_guide/api/index.rst @@ -14,6 +14,7 @@ API make_solo_mosaic mpas mpas_init + orog_gsl rocoto sfc_climo_gen shave diff --git a/docs/sections/user_guide/api/orog_gsl.rst b/docs/sections/user_guide/api/orog_gsl.rst new file mode 100644 index 000000000..7c7bad50b --- /dev/null +++ b/docs/sections/user_guide/api/orog_gsl.rst @@ -0,0 +1,5 @@ +``uwtools.api.orog_gsl`` +======================== + +.. automodule:: uwtools.api.orog_gsl + :members: diff --git a/docs/sections/user_guide/cli/drivers/index.rst b/docs/sections/user_guide/cli/drivers/index.rst index d990842ef..e0a1b00d9 100644 --- a/docs/sections/user_guide/cli/drivers/index.rst +++ b/docs/sections/user_guide/cli/drivers/index.rst @@ -13,6 +13,7 @@ Drivers make_solo_mosaic mpas mpas_init + orog_gsl sfc_climo_gen shave ungrib diff --git a/docs/sections/user_guide/cli/drivers/orog_gsl.rst b/docs/sections/user_guide/cli/drivers/orog_gsl.rst new file mode 100644 index 000000000..d74789e1d --- /dev/null +++ b/docs/sections/user_guide/cli/drivers/orog_gsl.rst @@ -0,0 +1,58 @@ +``orog_gsl`` +============ + +The ``uw`` mode for configuring and running the UFS Utils preprocessing component ``orog_gsl``. Documentation for this UFS Utils component is :ufs-utils:`here `. + +.. literalinclude:: orog_gsl/help.cmd + :language: text + :emphasize-lines: 1 +.. literalinclude:: orog_gsl/help.out + :language: text + +All tasks take the same arguments. For example: + +.. literalinclude:: orog_gsl/run-help.cmd + :language: text + :emphasize-lines: 1 +.. literalinclude:: orog_gsl/run-help.out + :language: text + +Examples +^^^^^^^^ + +The examples use a configuration file named ``config.yaml`` with contents similar to: + +.. highlight:: yaml +.. literalinclude:: /shared/orog_gsl.yaml + +Its contents are described in section :ref:`orog_gsl_yaml`. + +* Run ``orog_gsl`` on an interactive node + + .. code-block:: text + + $ uw orog_gsl run --config-file config.yaml + + The driver creates a ``runscript.orog_gsl`` file in the directory specified by ``run_dir:`` in the config and runs it, executing ``orog_gsl``. + +* Run ``orog_gsl`` via a batch job + + .. code-block:: text + + $ uw orog_gsl run --config-file config.yaml --batch + + The driver creates a ``runscript.orog_gsl`` file in the directory specified by ``run_dir:`` 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_gsl:``. + + Looking at the run command in ``runscript.orog_gsl`` shows us the specified executable as well as the YAML keys converted to appropriate command line flags. + + .. code-block:: text + + time orog_gsl --num_tiles 1 --dir /path/to/grid/ --tile_file C403_grid.tile7.halo6.nc --periodx 360 --periody 360 + +* 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_gsl run --config-file config.yaml --batch --dry-run + +.. include:: /shared/key_path.rst diff --git a/docs/sections/user_guide/cli/drivers/orog_gsl/Makefile b/docs/sections/user_guide/cli/drivers/orog_gsl/Makefile new file mode 120000 index 000000000..2486334a6 --- /dev/null +++ b/docs/sections/user_guide/cli/drivers/orog_gsl/Makefile @@ -0,0 +1 @@ +../../Makefile.outputs \ No newline at end of file diff --git a/docs/sections/user_guide/cli/drivers/orog_gsl/help.cmd b/docs/sections/user_guide/cli/drivers/orog_gsl/help.cmd new file mode 100644 index 000000000..aa40feaf8 --- /dev/null +++ b/docs/sections/user_guide/cli/drivers/orog_gsl/help.cmd @@ -0,0 +1 @@ +uw orog_gsl --help diff --git a/docs/sections/user_guide/cli/drivers/orog_gsl/help.out b/docs/sections/user_guide/cli/drivers/orog_gsl/help.out new file mode 100644 index 000000000..4ec7238b0 --- /dev/null +++ b/docs/sections/user_guide/cli/drivers/orog_gsl/help.out @@ -0,0 +1,26 @@ +usage: uw orog_gsl [-h] [--version] TASK ... + +Execute orog_gsl tasks + +Optional arguments: + -h, --help + Show help and exit + --version + Show version info and exit + +Positional arguments: + TASK + input_grid_file + The input grid file + provisioned_run_directory + Run directory provisioned with all required content + run + A run + runscript + The runscript + topo_data_2p5m + Global topographic data on 2.5-minute lat-lon grid + topo_data_30s + Global topographic data on 30-second lat-lon grid + validate + Validate the UW driver config diff --git a/docs/sections/user_guide/cli/drivers/orog_gsl/run-help.cmd b/docs/sections/user_guide/cli/drivers/orog_gsl/run-help.cmd new file mode 100644 index 000000000..979241b98 --- /dev/null +++ b/docs/sections/user_guide/cli/drivers/orog_gsl/run-help.cmd @@ -0,0 +1 @@ +uw orog_gsl run --help diff --git a/docs/sections/user_guide/cli/drivers/orog_gsl/run-help.out b/docs/sections/user_guide/cli/drivers/orog_gsl/run-help.out new file mode 100644 index 000000000..5601cf270 --- /dev/null +++ b/docs/sections/user_guide/cli/drivers/orog_gsl/run-help.out @@ -0,0 +1,26 @@ +usage: uw orog_gsl 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/yaml/components/global_equiv_resol.rst b/docs/sections/user_guide/yaml/components/global_equiv_resol.rst index 6537b602f..ef0c9d512 100644 --- a/docs/sections/user_guide/yaml/components/global_equiv_resol.rst +++ b/docs/sections/user_guide/yaml/components/global_equiv_resol.rst @@ -3,7 +3,7 @@ global_equiv_resol ================== -Structured YAML to run the WRF preprocessing component ``global_equiv_resol`` is validated by JSON Schema and requires the ``global_equiv_resol:`` block, described below. If ``global_equiv_resol`` is to be run via a batch system, the ``platform:`` block, described :ref:`here `, is also required. +Structured YAML to run the component ``global_equiv_resol`` is validated by JSON Schema and requires the ``global_equiv_resol:`` block, described below. If ``global_equiv_resol`` is to be run via a batch system, the ``platform:`` block, described :ref:`here `, is also required. Documentation for the UFS Utils ``global_equiv_resol`` program is :ufs-utils:`here `. diff --git a/docs/sections/user_guide/yaml/components/index.rst b/docs/sections/user_guide/yaml/components/index.rst index 0ae0e7f56..6b33e7bfc 100644 --- a/docs/sections/user_guide/yaml/components/index.rst +++ b/docs/sections/user_guide/yaml/components/index.rst @@ -13,6 +13,7 @@ UW YAML for Components make_solo_mosaic mpas mpas_init + orog_gsl schism sfc_climo_gen shave diff --git a/docs/sections/user_guide/yaml/components/make_solo_mosaic.rst b/docs/sections/user_guide/yaml/components/make_solo_mosaic.rst index a9270b851..1f8587833 100644 --- a/docs/sections/user_guide/yaml/components/make_solo_mosaic.rst +++ b/docs/sections/user_guide/yaml/components/make_solo_mosaic.rst @@ -3,7 +3,7 @@ make_solo_mosaic ================ -Structured YAML to run the WRF preprocessing component ``make_solo_mosaic`` is validated by JSON Schema and requires the ``make_solo_mosaic:`` block, described below. If ``make_solo_mosaic`` is to be run via a batch system, the ``platform:`` block, described :ref:`here `, is also required. +Structured YAML to run the component ``make_solo_mosaic`` is validated by JSON Schema and requires the ``make_solo_mosaic:`` block, described below. If ``make_solo_mosaic`` is to be run via a batch system, the ``platform:`` block, described :ref:`here `, is also required. Documentation for the UFS Utils ``make_solo_mosaic`` program is :ufs-utils:`here `. diff --git a/docs/sections/user_guide/yaml/components/orog_gsl.rst b/docs/sections/user_guide/yaml/components/orog_gsl.rst new file mode 100644 index 000000000..f77ce6265 --- /dev/null +++ b/docs/sections/user_guide/yaml/components/orog_gsl.rst @@ -0,0 +1,55 @@ +.. _orog_gsl_yaml: + +orog_gsl +======== + +Structured YAML to run the component ``orog_gsl`` is validated by JSON Schema and requires the ``orog_gsl:`` block, described below. If ``orog_gsl`` is to be run via a batch system, the ``platform:`` block, described :ref:`here `, is also required. + +Documentation for the UFS Utils ``orog_gsl`` program is :ufs-utils:`here `. + +Here is a prototype UW YAML ``orog_gsl:`` block, explained in detail below: + +.. highlight:: yaml +.. literalinclude:: /shared/orog_gsl.yaml + +UW YAML for the ``orog_gsl:`` Block +----------------------------------- + +config: +^^^^^^^ + +Configuration parameters for the ``orog_gsl`` component. + + **halo:** + + Halo number (-999 for no halo). + + **input_grid_file:** + + Path to the tiled input grid file. + + **resolution:** + + Input grid resolution index. + + **tile:** + + Tile number (1-6 for global, 7 for regional). + + **topo_data_2p5m:** + + Path to file containing global topographic data on 2.5-minute lat-lon grid. + + **topo_data_30s:** + + Path to file containing global topographic data on 30-second lat-lon grid. + +execution: +^^^^^^^^^^ + +See :ref:`here ` for details. + +run_dir: +^^^^^^^^ + +The path to the run directory. diff --git a/docs/shared/orog_gsl.yaml b/docs/shared/orog_gsl.yaml new file mode 100644 index 000000000..f872ef7b2 --- /dev/null +++ b/docs/shared/orog_gsl.yaml @@ -0,0 +1,20 @@ +orog_gsl: + config: + halo: 4 + input_grid_file: /path/to/C{{ orog_gsl.config.resolution }}_grid.tile{{ orog_gsl.config.tile }}.halo{{ orog_gsl.config.halo }}.nc + resolution: 403 + tile: 7 + topo_data_2p5m: /path/to/geo_em.d01.lat-lon.2.5m.HGT_M.nc + topo_data_30s: /path/to/HGT.Beljaars_filtered.lat-lon.30s_res.nc + execution: + batchargs: + cores: 1 + walltime: "00:01:00" + envcmds: + - module use /path/to/modules + - module load module_name + executable: /path/to/orog_gsl + run_dir: /path/to/dir/run +platform: + account: me + scheduler: slurm diff --git a/src/uwtools/api/orog_gsl.py b/src/uwtools/api/orog_gsl.py new file mode 100644 index 000000000..11e289f5f --- /dev/null +++ b/src/uwtools/api/orog_gsl.py @@ -0,0 +1,12 @@ +""" +API access to the ``uwtools`` ``orog_gsl`` driver. +""" + +from uwtools.drivers.orog_gsl import OrogGSL as _Driver +from uwtools.drivers.support import graph +from uwtools.utils.api import make_execute as _make_execute +from uwtools.utils.api import make_tasks as _make_tasks + +execute = _make_execute(_Driver) +tasks = _make_tasks(_Driver) +__all__ = ["execute", "graph", "tasks"] diff --git a/src/uwtools/cli.py b/src/uwtools/cli.py index c0dcac060..d1d2d92fd 100644 --- a/src/uwtools/cli.py +++ b/src/uwtools/cli.py @@ -73,6 +73,7 @@ def main() -> None: STR.makesolomosaic, STR.mpas, STR.mpasinit, + STR.oroggsl, STR.sfcclimogen, STR.shave, STR.ungrib, @@ -1033,6 +1034,7 @@ def _parse_args(raw_args: List[str]) -> Tuple[Args, Checks]: STR.globalequivresol, STR.makehgrid, STR.makesolomosaic, + STR.oroggsl, STR.sfcclimogen, STR.shave, ] diff --git a/src/uwtools/drivers/driver.py b/src/uwtools/drivers/driver.py index 5de6330a4..12295f666 100644 --- a/src/uwtools/drivers/driver.py +++ b/src/uwtools/drivers/driver.py @@ -340,14 +340,14 @@ def _validate(self) -> None: for schema_name in (self._driver_name.replace("_", "-"), "platform"): validate_internal(schema_name=schema_name, config=self._config) - def _write_runscript(self, path: Path, envvars: Dict[str, str]) -> None: + def _write_runscript(self, path: Path, envvars: Optional[Dict[str, str]] = None) -> None: """ Write the runscript. """ path.parent.mkdir(parents=True, exist_ok=True) rs = self._runscript( envcmds=self._driver_config.get("execution", {}).get("envcmds", []), - envvars=envvars, + envvars=envvars or {}, execution=[ "time %s" % self._runcmd, "test $? -eq 0 && touch %s" % self._runscript_done_file, diff --git a/src/uwtools/drivers/esg_grid.py b/src/uwtools/drivers/esg_grid.py index 278269055..b454fcf50 100644 --- a/src/uwtools/drivers/esg_grid.py +++ b/src/uwtools/drivers/esg_grid.py @@ -75,7 +75,7 @@ def runscript(self): yield self._taskname(path.name) yield asset(path, path.is_file) yield None - self._write_runscript(path=path, envvars={}) + self._write_runscript(path) # Private helper methods diff --git a/src/uwtools/drivers/global_equiv_resol.py b/src/uwtools/drivers/global_equiv_resol.py index 0b8aec754..981d14d44 100644 --- a/src/uwtools/drivers/global_equiv_resol.py +++ b/src/uwtools/drivers/global_equiv_resol.py @@ -64,7 +64,7 @@ def runscript(self): yield self._taskname(path.name) yield asset(path, path.is_file) yield None - self._write_runscript(path=path, envvars={}) + self._write_runscript(path) # Private helper methods diff --git a/src/uwtools/drivers/jedi.py b/src/uwtools/drivers/jedi.py index d2aad830c..084669b91 100644 --- a/src/uwtools/drivers/jedi.py +++ b/src/uwtools/drivers/jedi.py @@ -106,7 +106,7 @@ def runscript(self): yield self._taskname(path.name) yield asset(path, path.is_file) yield None - self._write_runscript(path=path, envvars={}) + self._write_runscript(path) @task def validate_only(self): diff --git a/src/uwtools/drivers/make_hgrid.py b/src/uwtools/drivers/make_hgrid.py index d1c6af83b..0277b92ed 100644 --- a/src/uwtools/drivers/make_hgrid.py +++ b/src/uwtools/drivers/make_hgrid.py @@ -52,7 +52,7 @@ def runscript(self): yield self._taskname(path.name) yield asset(path, path.is_file) yield None - self._write_runscript(path=path, envvars={}) + self._write_runscript(path) # Private helper methods diff --git a/src/uwtools/drivers/make_solo_mosaic.py b/src/uwtools/drivers/make_solo_mosaic.py index 168cd06dd..abe4627fa 100644 --- a/src/uwtools/drivers/make_solo_mosaic.py +++ b/src/uwtools/drivers/make_solo_mosaic.py @@ -52,7 +52,7 @@ def runscript(self): yield self._taskname(path.name) yield asset(path, path.is_file) yield None - self._write_runscript(path=path, envvars={}) + self._write_runscript(path) # Private helper methods diff --git a/src/uwtools/drivers/mpas_base.py b/src/uwtools/drivers/mpas_base.py index 871d4dc2f..1440bb679 100644 --- a/src/uwtools/drivers/mpas_base.py +++ b/src/uwtools/drivers/mpas_base.py @@ -104,7 +104,7 @@ def runscript(self): yield self._taskname(path.name) yield asset(path, path.is_file) yield None - self._write_runscript(path=path, envvars={}) + self._write_runscript(path) @task def streams_file(self): diff --git a/src/uwtools/drivers/orog_gsl.py b/src/uwtools/drivers/orog_gsl.py new file mode 100644 index 000000000..239c1d3e4 --- /dev/null +++ b/src/uwtools/drivers/orog_gsl.py @@ -0,0 +1,117 @@ +""" +A driver for orog_gsl. +""" + +from pathlib import Path +from typing import List, Optional + +from iotaa import asset, task, tasks + +from uwtools.drivers.driver import Driver +from uwtools.strings import STR +from uwtools.utils.tasks import symlink + + +class OrogGSL(Driver): + """ + A driver for orog_gsl. + """ + + def __init__( + self, + config: Optional[Path] = None, + dry_run: bool = False, + batch: bool = False, + key_path: Optional[List[str]] = None, + ): + """ + The driver. + + :param config: Path to config file (read stdin if missing or None). + :param dry_run: Run in dry-run mode? + :param batch: Run component via the batch system? + :param key_path: Keys leading through the config to the driver's configuration block. + """ + super().__init__(config=config, dry_run=dry_run, batch=batch, key_path=key_path) + + # Workflow tasks + + @task + def input_grid_file(self): + """ + The input grid file. + """ + fn = "C%s_grid.tile%s.halo%s.nc" % tuple( + self._driver_config["config"][k] for k in ["resolution", "tile", "halo"] + ) + src = Path(self._driver_config["config"]["input_grid_file"]) + dst = Path(self._driver_config["run_dir"]) / fn + yield self._taskname("Input grid") + yield asset(dst, dst.is_file) + yield symlink(target=src, linkname=dst) + + @tasks + def provisioned_run_directory(self): + """ + Run directory provisioned with all required content. + """ + yield self._taskname("provisioned run directory") + yield [ + self.input_grid_file(), + self.runscript(), + self.topo_data_2p5m(), + self.topo_data_30s(), + ] + + @task + def runscript(self): + """ + The runscript. + """ + path = self._runscript_path + yield self._taskname(path.name) + yield asset(path, path.is_file) + yield None + self._write_runscript(path) + + @task + def topo_data_2p5m(self): + """ + Global topographic data on 2.5-minute lat-lon grid. + """ + fn = "geo_em.d01.lat-lon.2.5m.HGT_M.nc" + src = Path(self._driver_config["config"]["topo_data_2p5m"]) + dst = Path(self._driver_config["run_dir"]) / fn + yield self._taskname("Input grid") + yield asset(dst, dst.is_file) + yield symlink(target=src, linkname=dst) + + @task + def topo_data_30s(self): + """ + Global topographic data on 30-second lat-lon grid. + """ + fn = "HGT.Beljaars_filtered.lat-lon.30s_res.nc" + src = Path(self._driver_config["config"]["topo_data_30s"]) + dst = Path(self._driver_config["run_dir"]) / fn + yield self._taskname("Input grid") + yield asset(dst, dst.is_file) + yield symlink(target=src, linkname=dst) + + # Private helper methods + + @property + def _driver_name(self) -> str: + """ + Returns the name of this driver. + """ + return STR.oroggsl + + @property + def _runcmd(self): + """ + Returns the full command-line component invocation. + """ + inputs = [str(self._driver_config["config"][k]) for k in ("tile", "resolution", "halo")] + executable = self._driver_config["execution"]["executable"] + return "echo '%s' | %s" % ("\n".join(inputs), executable) diff --git a/src/uwtools/drivers/sfc_climo_gen.py b/src/uwtools/drivers/sfc_climo_gen.py index a04663596..86e72d854 100644 --- a/src/uwtools/drivers/sfc_climo_gen.py +++ b/src/uwtools/drivers/sfc_climo_gen.py @@ -78,7 +78,7 @@ def runscript(self): yield self._taskname(path.name) yield asset(path, path.is_file) yield None - self._write_runscript(path=path, envvars={}) + self._write_runscript(path) # Private helper methods diff --git a/src/uwtools/drivers/shave.py b/src/uwtools/drivers/shave.py index c4297ae73..edc013b48 100644 --- a/src/uwtools/drivers/shave.py +++ b/src/uwtools/drivers/shave.py @@ -41,9 +41,7 @@ def provisioned_run_directory(self): Run directory provisioned with all required content. """ yield self._taskname("provisioned run directory") - yield [ - self.runscript(), - ] + yield self.runscript() @task def runscript(self): @@ -54,7 +52,7 @@ def runscript(self): yield self._taskname(path.name) yield asset(path, path.is_file) yield None - self._write_runscript(path=path, envvars={}) + self._write_runscript(path) # Private helper methods @@ -72,8 +70,8 @@ def _runcmd(self): """ executable = self._driver_config["execution"]["executable"] config = self._driver_config["config"] - input_file = config.get("input_grid_file") + input_file = config["input_grid_file"] output_file = input_file.replace(".nc", "_NH0.nc") - flags = [config.get(key) for key in ["nx", "ny", "nh4", "input_grid_file"]] + flags = [config[key] for key in ["nx", "ny", "nh4", "input_grid_file"]] flags.append(output_file) return f"{executable} {' '.join(str(flag) for flag in flags)}" diff --git a/src/uwtools/drivers/ungrib.py b/src/uwtools/drivers/ungrib.py index 5293e28e9..e7454f1bc 100644 --- a/src/uwtools/drivers/ungrib.py +++ b/src/uwtools/drivers/ungrib.py @@ -121,7 +121,7 @@ def runscript(self): yield self._taskname(path.name) yield asset(path, path.is_file) yield None - self._write_runscript(path=path, envvars={}) + self._write_runscript(path) @task def vtable(self): diff --git a/src/uwtools/drivers/upp.py b/src/uwtools/drivers/upp.py index 92dec51ca..8a6de56c1 100644 --- a/src/uwtools/drivers/upp.py +++ b/src/uwtools/drivers/upp.py @@ -113,7 +113,7 @@ def runscript(self): yield self._taskname(path.name) yield asset(path, path.is_file) yield None - self._write_runscript(path=path, envvars={}) + self._write_runscript(path) # Private helper methods diff --git a/src/uwtools/drivers/ww3.py b/src/uwtools/drivers/ww3.py index eaa8b0b99..edd3ecdf2 100644 --- a/src/uwtools/drivers/ww3.py +++ b/src/uwtools/drivers/ww3.py @@ -66,7 +66,10 @@ def provisioned_run_directory(self): Run directory provisioned with all required content. """ yield self._taskname("provisioned run directory") - yield [self.namelist_file(), self.restart_directory()] + yield [ + self.namelist_file(), + self.restart_directory(), + ] @task def restart_directory(self): diff --git a/src/uwtools/resources/jsonschema/orog-gsl.jsonschema b/src/uwtools/resources/jsonschema/orog-gsl.jsonschema new file mode 100644 index 000000000..e77a770f5 --- /dev/null +++ b/src/uwtools/resources/jsonschema/orog-gsl.jsonschema @@ -0,0 +1,55 @@ +{ + "properties": { + "orog_gsl": { + "additionalProperties": false, + "properties": { + "config": { + "additionalProperties": false, + "properties": { + "halo": { + "type": "integer" + }, + "input_grid_file": { + "type": "string" + }, + "resolution": { + "type": "integer" + }, + "tile": { + "maximum": 7, + "minimum": 1, + "type": "integer" + }, + "topo_data_2p5m": { + "type": "string" + }, + "topo_data_30s": { + "type": "string" + } + }, + "required": [ + "halo", + "input_grid_file", + "resolution", + "tile", + "topo_data_2p5m", + "topo_data_30s" + ] + }, + "execution": { + "$ref": "urn:uwtools:execution-serial" + }, + "run_dir": { + "type": "string" + } + }, + "required": [ + "config", + "execution", + "run_dir" + ], + "type": "object" + } + }, + "type": "object" +} diff --git a/src/uwtools/resources/jsonschema/shave.jsonschema b/src/uwtools/resources/jsonschema/shave.jsonschema index afe918632..cf277940b 100644 --- a/src/uwtools/resources/jsonschema/shave.jsonschema +++ b/src/uwtools/resources/jsonschema/shave.jsonschema @@ -37,8 +37,8 @@ } }, "required": [ - "execution", "config", + "execution", "run_dir" ], "type": "object" diff --git a/src/uwtools/strings.py b/src/uwtools/strings.py index 42af18122..b707ccbd2 100644 --- a/src/uwtools/strings.py +++ b/src/uwtools/strings.py @@ -95,6 +95,7 @@ class STR: model: str = "model" mpas: str = "mpas" mpasinit: str = "mpas_init" + oroggsl: str = "orog_gsl" outfile: str = "output_file" outfmt: str = "output_format" quiet: str = "quiet" diff --git a/src/uwtools/tests/api/test_drivers.py b/src/uwtools/tests/api/test_drivers.py index 6a4c376bc..5fb38e231 100644 --- a/src/uwtools/tests/api/test_drivers.py +++ b/src/uwtools/tests/api/test_drivers.py @@ -16,6 +16,7 @@ make_solo_mosaic, mpas, mpas_init, + orog_gsl, sfc_climo_gen, shave, ungrib, @@ -34,6 +35,7 @@ make_solo_mosaic, mpas, mpas_init, + orog_gsl, sfc_climo_gen, shave, ungrib, diff --git a/src/uwtools/tests/drivers/test_chgres_cube.py b/src/uwtools/tests/drivers/test_chgres_cube.py index 42877110f..cccb2a061 100644 --- a/src/uwtools/tests/drivers/test_chgres_cube.py +++ b/src/uwtools/tests/drivers/test_chgres_cube.py @@ -9,7 +9,6 @@ from unittest.mock import patch import f90nml # type: ignore -import yaml from iotaa import refs from pytest import fixture @@ -27,10 +26,10 @@ def cycle(): @fixture -def config_file(tmp_path): +def config(tmp_path): afile = tmp_path / "afile" afile.touch() - config: dict = { + return { "chgres_cube": { "execution": { "batchargs": { @@ -73,15 +72,11 @@ def config_file(tmp_path): "scheduler": "slurm", }, } - path = tmp_path / "config.yaml" - with open(path, "w", encoding="utf-8") as f: - yaml.dump(config, f) - return path @fixture -def driverobj(config_file, cycle): - return chgres_cube.ChgresCube(config=config_file, cycle=cycle, batch=True) +def driverobj(config, cycle): + return chgres_cube.ChgresCube(config=config, cycle=cycle, batch=True) # Tests diff --git a/src/uwtools/tests/drivers/test_driver.py b/src/uwtools/tests/drivers/test_driver.py index eff8efa59..72f97bf9c 100644 --- a/src/uwtools/tests/drivers/test_driver.py +++ b/src/uwtools/tests/drivers/test_driver.py @@ -153,9 +153,11 @@ def test_Asset_dry_run(config, val): # Tests for workflow methods -def test_key_path(config): +def test_key_path(config, tmp_path): + config_file = tmp_path / "config.yaml" + config_file.write_text(yaml.dump({"foo": {"bar": config}})) assetobj = ConcreteAssets( - config={"foo": {"bar": config}}, + config=config_file, dry_run=False, batch=True, cycle=dt.datetime(2024, 3, 22, 18), @@ -165,7 +167,7 @@ def test_key_path(config): assert config == assetobj._config -def test_Asset_validate(caplog, assetobj): +def test_Asset_validate(assetobj, caplog): log.setLevel(logging.INFO) assetobj.validate() assert regex_logged(caplog, "State: Ready") @@ -184,7 +186,7 @@ def test_Asset_validate(caplog, assetobj): ], ) def test_Asset__create_user_updated_config_base_file( - base_file, assetobj, expected, tmp_path, update_values + assetobj, base_file, expected, tmp_path, update_values ): path = tmp_path / "updated.yaml" dc = assetobj._driver_config diff --git a/src/uwtools/tests/drivers/test_esg_grid.py b/src/uwtools/tests/drivers/test_esg_grid.py index 8b1dad1ae..bd2470f1f 100644 --- a/src/uwtools/tests/drivers/test_esg_grid.py +++ b/src/uwtools/tests/drivers/test_esg_grid.py @@ -8,7 +8,6 @@ from unittest.mock import patch import f90nml # type: ignore -import yaml from iotaa import refs from pytest import fixture @@ -56,16 +55,8 @@ def config(tmp_path): @fixture -def config_file(config, tmp_path): - path = tmp_path / "config.yaml" - with open(path, "w", encoding="utf-8") as f: - yaml.dump(config, f) - return path - - -@fixture -def driverobj(config_file): - return esg_grid.ESGGrid(config=config_file, batch=True) +def driverobj(config): + return esg_grid.ESGGrid(config=config, batch=True) # Tests diff --git a/src/uwtools/tests/drivers/test_fv3.py b/src/uwtools/tests/drivers/test_fv3.py index 67359f5f4..8c215ce91 100644 --- a/src/uwtools/tests/drivers/test_fv3.py +++ b/src/uwtools/tests/drivers/test_fv3.py @@ -58,22 +58,14 @@ def config(tmp_path): } -@fixture -def config_file(config, tmp_path): - path = tmp_path / "config.yaml" - with open(path, "w", encoding="utf-8") as f: - yaml.dump(config, f) - return path - - @fixture def cycle(): return dt.datetime(2024, 2, 1, 18) @fixture -def driverobj(config_file, cycle): - return fv3.FV3(config=config_file, cycle=cycle, batch=True) +def driverobj(config, cycle): + return fv3.FV3(config=config, cycle=cycle, batch=True) @fixture diff --git a/src/uwtools/tests/drivers/test_global_equiv_resol.py b/src/uwtools/tests/drivers/test_global_equiv_resol.py index ebae11dd5..2e0489645 100644 --- a/src/uwtools/tests/drivers/test_global_equiv_resol.py +++ b/src/uwtools/tests/drivers/test_global_equiv_resol.py @@ -6,7 +6,6 @@ from unittest.mock import DEFAULT as D from unittest.mock import patch -import yaml from pytest import fixture from uwtools.drivers import global_equiv_resol @@ -37,16 +36,8 @@ def config(tmp_path): @fixture -def config_file(config, tmp_path): - path = tmp_path / "config.yaml" - with open(path, "w", encoding="utf-8") as f: - yaml.dump(config, f) - return path - - -@fixture -def driverobj(config_file): - return global_equiv_resol.GlobalEquivResol(config=config_file, batch=True) +def driverobj(config): + return global_equiv_resol.GlobalEquivResol(config=config, batch=True) # Tests diff --git a/src/uwtools/tests/drivers/test_jedi.py b/src/uwtools/tests/drivers/test_jedi.py index 027a93611..ac7616e48 100644 --- a/src/uwtools/tests/drivers/test_jedi.py +++ b/src/uwtools/tests/drivers/test_jedi.py @@ -23,6 +23,8 @@ @fixture def config(tmp_path): + base_file = tmp_path / "base.yaml" + base_file.write_text("foo: bar") return { "jedi": { "execution": { @@ -41,7 +43,7 @@ def config(tmp_path): "mpicmd": "srun", }, "configuration_file": { - "base_file": str(tmp_path / "base.yaml"), + "base_file": str(base_file), "update_values": {"baz": "qux"}, }, "files_to_copy": { @@ -61,22 +63,14 @@ def config(tmp_path): } -@fixture -def config_file(config, tmp_path): - path = tmp_path / "base.yaml" - with open(path, "w", encoding="utf-8") as f: - yaml.dump(config, f) - return path - - @fixture def cycle(): return dt.datetime(2024, 2, 1, 18) @fixture -def driverobj(config_file, cycle): - return jedi.JEDI(config=config_file, cycle=cycle, batch=True) +def driverobj(config, cycle): + return jedi.JEDI(config=config, cycle=cycle, batch=True) # Tests diff --git a/src/uwtools/tests/drivers/test_make_hgrid.py b/src/uwtools/tests/drivers/test_make_hgrid.py index e696be91d..4acf88d04 100644 --- a/src/uwtools/tests/drivers/test_make_hgrid.py +++ b/src/uwtools/tests/drivers/test_make_hgrid.py @@ -5,7 +5,6 @@ from unittest.mock import DEFAULT as D from unittest.mock import patch -import yaml from pytest import fixture from uwtools.drivers import make_hgrid @@ -20,8 +19,8 @@ def config(tmp_path): "make_hgrid": { "config": { "grid_type": "gnomonic_ed", - "nest_grids": 1, "halo": 1, + "nest_grids": 1, "parent_tile": [6], "verbose": True, }, @@ -42,16 +41,8 @@ def config(tmp_path): @fixture -def config_file(config, tmp_path): - path = tmp_path / "config.yaml" - with open(path, "w", encoding="utf-8") as f: - yaml.dump(config, f) - return path - - -@fixture -def driverobj(config_file): - return make_hgrid.MakeHgrid(config=config_file, batch=True) +def driverobj(config): + return make_hgrid.MakeHgrid(config=config, batch=True) # Tests @@ -72,12 +63,15 @@ def test_MakeHgrid_provisioned_run_directory(driverobj): def test_MakeHgrid__runcmd(driverobj): - cmd = driverobj._runcmd - assert ( - cmd - == "/path/to/make_hgrid --grid_type gnomonic_ed --halo 1 \ ---nest_grids 1 --parent_tile 6 --verbose" - ) + expected = [ + "/path/to/make_hgrid", + "--grid_type gnomonic_ed", + "--halo 1", + "--nest_grids 1", + "--parent_tile 6", + "--verbose", + ] + assert driverobj._runcmd == " ".join(expected) def test_MakeHgrid_run_batch(driverobj): diff --git a/src/uwtools/tests/drivers/test_make_solo_mosaic.py b/src/uwtools/tests/drivers/test_make_solo_mosaic.py index 3f5738b25..1e5d5fafa 100644 --- a/src/uwtools/tests/drivers/test_make_solo_mosaic.py +++ b/src/uwtools/tests/drivers/test_make_solo_mosaic.py @@ -4,7 +4,6 @@ """ from unittest.mock import patch -import yaml from pytest import fixture from uwtools.drivers import make_solo_mosaic @@ -38,16 +37,8 @@ def config(tmp_path): @fixture -def config_file(config, tmp_path): - path = tmp_path / "config.yaml" - with open(path, "w", encoding="utf-8") as f: - yaml.dump(config, f) - return path - - -@fixture -def driverobj(config_file): - return make_solo_mosaic.MakeSoloMosaic(config=config_file, batch=True) +def driverobj(config): + return make_solo_mosaic.MakeSoloMosaic(config=config, batch=True) # Tests diff --git a/src/uwtools/tests/drivers/test_mpas.py b/src/uwtools/tests/drivers/test_mpas.py index 1b3758908..c875e52b9 100644 --- a/src/uwtools/tests/drivers/test_mpas.py +++ b/src/uwtools/tests/drivers/test_mpas.py @@ -20,6 +20,30 @@ from uwtools.scheduler import Slurm from uwtools.tests.support import fixture_path, logged, regex_logged +# Helpers + + +def streams_file(config, driverobj, drivername): + array_elements = {"file", "stream", "var", "var_array", "var_struct"} + array_elements_tested = set() + driverobj.streams_file() + path = Path(driverobj._driver_config["run_dir"]) / driverobj._streams_fn + with open(path, "r", encoding="utf-8") as f: + xml = etree.parse(f).getroot() + assert xml.tag == "streams" + for child in xml.getchildren(): # type: ignore + block = config[drivername]["streams"][child.get("name")] + for k, v in block.items(): + if k not in [*[f"{e}s" for e in array_elements], "mutable"]: + assert child.get(k) == v + assert child.tag == "stream" if block["mutable"] else "immutable_stream" + for e in array_elements: + for name in block.get(f"{e}s", []): + assert child.xpath(f"//{e}[@name='{name}']") + array_elements_tested.add(e) + assert array_elements_tested == array_elements + + # Fixtures @@ -81,46 +105,14 @@ def config(tmp_path): } -@fixture -def config_file(config, tmp_path): - path = tmp_path / "config.yaml" - with open(path, "w", encoding="utf-8") as f: - yaml.dump(config, f) - return path - - @fixture def cycle(): return dt.datetime(2024, 3, 22, 6) @fixture -def driverobj(config_file, cycle): - return mpas.MPAS(config=config_file, cycle=cycle, batch=True) - - -# Helpers - - -def streams_file(config, driverobj, drivername): - array_elements = {"file", "stream", "var", "var_array", "var_struct"} - array_elements_tested = set() - driverobj.streams_file() - path = Path(driverobj._driver_config["run_dir"]) / driverobj._streams_fn - with open(path, "r", encoding="utf-8") as f: - xml = etree.parse(f).getroot() - assert xml.tag == "streams" - for child in xml.getchildren(): # type: ignore - block = config[drivername]["streams"][child.get("name")] - for k, v in block.items(): - if k not in [*[f"{e}s" for e in array_elements], "mutable"]: - assert child.get(k) == v - assert child.tag == "stream" if block["mutable"] else "immutable_stream" - for e in array_elements: - for name in block.get(f"{e}s", []): - assert child.xpath(f"//{e}[@name='{name}']") - array_elements_tested.add(e) - assert array_elements_tested == array_elements +def driverobj(config, cycle): + return mpas.MPAS(config=config, cycle=cycle, batch=True) # Tests diff --git a/src/uwtools/tests/drivers/test_mpas_init.py b/src/uwtools/tests/drivers/test_mpas_init.py index b6f913b65..9c09ffe50 100644 --- a/src/uwtools/tests/drivers/test_mpas_init.py +++ b/src/uwtools/tests/drivers/test_mpas_init.py @@ -10,7 +10,6 @@ import f90nml # type: ignore import pytest -import yaml from iotaa import refs from pytest import fixture @@ -88,22 +87,14 @@ def config(tmp_path): } -@fixture -def config_file(config, tmp_path): - path = tmp_path / "config.yaml" - with open(path, "w", encoding="utf-8") as f: - yaml.dump(config, f) - return path - - @fixture def cycle(): return dt.datetime(2024, 2, 1, 18) @fixture -def driverobj(config_file, cycle): - return mpas_init.MPASInit(config=config_file, cycle=cycle, batch=True) +def driverobj(config, cycle): + return mpas_init.MPASInit(config=config, cycle=cycle, batch=True) # Tests diff --git a/src/uwtools/tests/drivers/test_orog_gsl.py b/src/uwtools/tests/drivers/test_orog_gsl.py new file mode 100644 index 000000000..b3b0e5364 --- /dev/null +++ b/src/uwtools/tests/drivers/test_orog_gsl.py @@ -0,0 +1,131 @@ +# pylint: disable=missing-function-docstring,protected-access,redefined-outer-name +""" +orog_gsl driver tests. +""" +from pathlib import Path +from unittest.mock import DEFAULT as D +from unittest.mock import patch + +from pytest import fixture + +from uwtools.drivers import orog_gsl +from uwtools.scheduler import Slurm + +# Fixtures + + +@fixture +def config(tmp_path): + afile = tmp_path / "afile" + afile.touch() + return { + "orog_gsl": { + "config": { + "halo": 4, + "input_grid_file": str(afile), + "resolution": 403, + "tile": 7, + "topo_data_2p5m": str(afile), + "topo_data_30s": str(afile), + }, + "execution": { + "batchargs": { + "walltime": "00:01:00", + }, + "executable": "/path/to/orog_gsl", + }, + "run_dir": str(tmp_path), + }, + "platform": { + "account": "me", + "scheduler": "slurm", + }, + } + + +@fixture +def driverobj(config): + return orog_gsl.OrogGSL(config=config, batch=True) + + +# Tests + + +def test_OrogGSL(driverobj): + assert isinstance(driverobj, orog_gsl.OrogGSL) + + +def test_OrogGSL_input_grid_file(driverobj): + path = Path(driverobj._driver_config["run_dir"]) / "C403_grid.tile7.halo4.nc" + assert not path.is_file() + driverobj.input_grid_file() + assert path.is_symlink() + + +def test_OrogGSL_provisioned_run_directory(driverobj): + with patch.multiple( + driverobj, input_grid_file=D, runscript=D, topo_data_2p5m=D, topo_data_30s=D + ) as mocks: + driverobj.provisioned_run_directory() + for m in mocks: + mocks[m].assert_called_once_with() + + +def test_OrogGSL_run_batch(driverobj): + with patch.object(driverobj, "_run_via_batch_submission") as func: + driverobj.run() + func.assert_called_once_with() + + +def test_OrogGSL_run_local(driverobj): + driverobj._batch = False + with patch.object(driverobj, "_run_via_local_execution") as func: + driverobj.run() + func.assert_called_once_with() + + +def test_OrogGSL_topo_data_2p5m(driverobj): + path = Path(driverobj._driver_config["run_dir"]) / "geo_em.d01.lat-lon.2.5m.HGT_M.nc" + assert not path.is_file() + driverobj.topo_data_2p5m() + assert path.is_symlink() + + +def test_OrogGSL_topo_data_3os(driverobj): + path = Path(driverobj._driver_config["run_dir"]) / "HGT.Beljaars_filtered.lat-lon.30s_res.nc" + assert not path.is_file() + driverobj.topo_data_30s() + assert path.is_symlink() + + +def test_OrogGSL__runcmd(driverobj): + inputs = [str(driverobj._driver_config["config"][k]) for k in ("tile", "resolution", "halo")] + assert driverobj._runcmd == "echo '%s' | %s" % ( + "\n".join(inputs), + driverobj._driver_config["execution"]["executable"], + ) + + +def test_OrogGSL_runscript(driverobj): + with patch.object(driverobj, "_runscript") as runscript: + driverobj.runscript() + runscript.assert_called_once() + args = ("envcmds", "envvars", "execution", "scheduler") + types = [list, dict, list, Slurm] + assert [type(runscript.call_args.kwargs[x]) for x in args] == types + + +def test_OrogGSL__driver_config(driverobj): + assert driverobj._driver_config == driverobj._config["orog_gsl"] + + +def test_OrogGSL__runscript_path(driverobj): + assert driverobj._runscript_path == driverobj._rundir / "runscript.orog_gsl" + + +def test_OrogGSL__taskname(driverobj): + assert driverobj._taskname("foo") == "orog_gsl foo" + + +def test_OrogGSL__validate(driverobj): + driverobj._validate() diff --git a/src/uwtools/tests/drivers/test_sfc_climo_gen.py b/src/uwtools/tests/drivers/test_sfc_climo_gen.py index b7a3e988e..b1cafd6e9 100644 --- a/src/uwtools/tests/drivers/test_sfc_climo_gen.py +++ b/src/uwtools/tests/drivers/test_sfc_climo_gen.py @@ -8,7 +8,6 @@ from unittest.mock import patch import f90nml # type: ignore -import yaml from iotaa import asset, external, refs from pytest import fixture @@ -17,12 +16,21 @@ from uwtools.scheduler import Slurm from uwtools.tests.support import logged +# Helpers + + +@external +def ready(x): + yield x + yield asset(x, lambda: True) + + # Fixtures @fixture -def config_file(tmp_path): - config: dict = { +def config(tmp_path): + return { "sfc_climo_gen": { "execution": { "batchargs": { @@ -58,32 +66,18 @@ def config_file(tmp_path): }, "validate": True, }, - "run_dir": "/path/to/dir", + "run_dir": str(tmp_path), }, "platform": { "account": "me", "scheduler": "slurm", }, } - path = tmp_path / "config.yaml" - config["sfc_climo_gen"]["run_dir"] = tmp_path.as_posix() - with open(path, "w", encoding="utf-8") as f: - yaml.dump(config, f) - return path @fixture -def driverobj(config_file): - return sfc_climo_gen.SfcClimoGen(config=config_file, batch=True) - - -# Helpers - - -@external -def ready(x): - yield x - yield asset(x, lambda: True) +def driverobj(config): + return sfc_climo_gen.SfcClimoGen(config=config, batch=True) # Tests diff --git a/src/uwtools/tests/drivers/test_shave.py b/src/uwtools/tests/drivers/test_shave.py index 8a99d896f..39309cd75 100644 --- a/src/uwtools/tests/drivers/test_shave.py +++ b/src/uwtools/tests/drivers/test_shave.py @@ -5,13 +5,12 @@ from unittest.mock import DEFAULT as D from unittest.mock import patch -import yaml from pytest import fixture from uwtools.drivers import shave from uwtools.scheduler import Slurm -# Driver fixtures +# Fixtures @fixture @@ -43,19 +42,11 @@ def config(tmp_path): @fixture -def config_file(config, tmp_path): - path = tmp_path / "config.yaml" - with open(path, "w", encoding="utf-8") as f: - yaml.dump(config, f) - return path +def driverobj(config): + return shave.Shave(config=config, batch=True) -@fixture -def driverobj(config_file): - return shave.Shave(config=config_file, batch=True) - - -# Driver tests +# Tests def test_Shave(driverobj): diff --git a/src/uwtools/tests/drivers/test_ungrib.py b/src/uwtools/tests/drivers/test_ungrib.py index f3fac989b..ed8291082 100644 --- a/src/uwtools/tests/drivers/test_ungrib.py +++ b/src/uwtools/tests/drivers/test_ungrib.py @@ -7,7 +7,6 @@ from unittest.mock import patch import f90nml # type: ignore -import yaml from pytest import fixture from uwtools.drivers import ungrib @@ -43,22 +42,14 @@ def config(tmp_path): } -@fixture -def config_file(config, tmp_path): - path = tmp_path / "config.yaml" - with open(path, "w", encoding="utf-8") as f: - yaml.dump(config, f) - return path - - @fixture def cycle(): return dt.datetime(2024, 2, 1, 18) @fixture -def driverobj(config_file, cycle): - return ungrib.Ungrib(config=config_file, cycle=cycle, batch=True) +def driverobj(config, cycle): + return ungrib.Ungrib(config=config, cycle=cycle, batch=True) # Tests diff --git a/src/uwtools/tests/drivers/test_upp.py b/src/uwtools/tests/drivers/test_upp.py index 95fefd846..ee1790ace 100644 --- a/src/uwtools/tests/drivers/test_upp.py +++ b/src/uwtools/tests/drivers/test_upp.py @@ -9,7 +9,6 @@ from unittest.mock import patch import f90nml # type: ignore -import yaml from iotaa import refs from pytest import fixture @@ -60,22 +59,14 @@ def config(tmp_path): } -@fixture -def config_file(config, tmp_path): - path = tmp_path / "config.yaml" - with open(path, "w", encoding="utf-8") as f: - yaml.dump(config, f) - return path - - @fixture def cycle(): return dt.datetime(2024, 5, 6, 12) @fixture -def driverobj(config_file, cycle, leadtime): - return upp.UPP(config=config_file, cycle=cycle, leadtime=leadtime, batch=True) +def driverobj(config, cycle, leadtime): + return upp.UPP(config=config, cycle=cycle, leadtime=leadtime, batch=True) @fixture diff --git a/src/uwtools/tests/drivers/test_ww3.py b/src/uwtools/tests/drivers/test_ww3.py index da74ef31d..0ded48568 100644 --- a/src/uwtools/tests/drivers/test_ww3.py +++ b/src/uwtools/tests/drivers/test_ww3.py @@ -29,22 +29,14 @@ def config(tmp_path): } -@fixture -def config_file(config, tmp_path): - path = tmp_path / "config.yaml" - with open(path, "w", encoding="utf-8") as f: - yaml.dump(config, f) - return path - - @fixture def cycle(): return dt.datetime(2024, 2, 1, 18) @fixture -def driverobj(config_file, cycle): - return ww3.WaveWatchIII(config=config_file, cycle=cycle, batch=True) +def driverobj(config, cycle): + return ww3.WaveWatchIII(config=config, cycle=cycle, batch=True) # Tests diff --git a/src/uwtools/tests/test_schemas.py b/src/uwtools/tests/test_schemas.py index c6ddb7477..a70be57c6 100644 --- a/src/uwtools/tests/test_schemas.py +++ b/src/uwtools/tests/test_schemas.py @@ -531,7 +531,7 @@ def test_schema_fv3_run_dir(fv3_prop): assert "88 is not of type 'string'" in errors(88) -# global_equiv_resol +# global-equiv-resol def test_schema_global_equiv_resol(): @@ -602,7 +602,7 @@ def test_schema_jedi_run_dir(jedi_prop): assert "88 is not of type 'string'" in errors(88) -# make_hgrid +# make-hgrid def test_schema_make_hgrid(): @@ -664,7 +664,7 @@ def test_schema_make_hgrid_run_dir(make_hgrid_prop): assert "88 is not of type 'string'" in errors(88) -# make_solo_mosaic +# make-solo-mosaic def test_schema_make_solo_mosaic(): @@ -801,7 +801,7 @@ def test_schema_mpas_run_dir(mpas_prop): assert "88 is not of type 'string'" in errors(88) -# mpas_init +# mpas-init def test_schema_mpas_init(mpas_streams): @@ -1031,6 +1031,49 @@ def test_schema_namelist(): assert "[] is not of type 'object'" in errors({"namelist": []}) +# orog-gsl + + +def test_schema_orog_gsl(): + config = { + "config": { + "halo": 4, + "input_grid_file": "/path/to/gridfile", + "resolution": 304, + "tile": 7, + "topo_data_2p5m": "/path/to/topo2p5m", + "topo_data_30s": "/path/to/topo30s", + }, + "execution": { + "executable": "/path/to/orog_gsl", + }, + "run_dir": "/path/to/run/dir", + } + errors = schema_validator("orog-gsl", "properties", "orog_gsl") + # Basic correctness: + assert not errors(config) + # All config keys are requried: + for key in ["halo", "input_grid_file", "resolution", "tile", "topo_data_2p5m", "topo_data_30s"]: + assert f"'{key}' is a required property" in errors(with_del(config, "config", key)) + # Other config keys are not allowed: + assert "Additional properties are not allowed" in errors( + with_set(config, "bar", "config", "foo") + ) + # Some config keys require integer values: + for key in ["halo", "resolution", "tile"]: + assert "is not of type 'integer'" in errors(with_set(config, None, "config", key)) + # Some config keys require string values: + for key in ["input_grid_file", "topo_data_2p5m", "topo_data_30s"]: + assert "is not of type 'string'" in errors(with_set(config, None, "config", key)) + # Some top level keys are required: + for key in ["config", "execution", "run_dir"]: + 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")) + # Top-level run_dir key requires a string value: + assert "is not of type 'string'" in errors(with_set(config, None, "run_dir")) + + # platform