From 4e67e51fd30af252498d1639212e761511fbf5db Mon Sep 17 00:00:00 2001
From: Paul Madden <136389411+maddenp-noaa@users.noreply.github.com>
Date: Tue, 10 Sep 2024 08:35:39 -0600
Subject: [PATCH 1/3] UW-653 controller key path (#604)
---
src/uwtools/drivers/driver.py | 37 +++++++++++++++---------
src/uwtools/tests/drivers/test_driver.py | 11 +++++--
2 files changed, 33 insertions(+), 15 deletions(-)
diff --git a/src/uwtools/drivers/driver.py b/src/uwtools/drivers/driver.py
index 750a44b3d..c98775fb7 100644
--- a/src/uwtools/drivers/driver.py
+++ b/src/uwtools/drivers/driver.py
@@ -49,7 +49,7 @@ def __init__(
dry_run: bool = False,
key_path: Optional[list[str]] = None,
schema_file: Optional[Path] = None,
- controller: Optional[str] = None,
+ controller: Optional[list[str]] = None,
) -> None:
config_input = config if isinstance(config, YAMLConfig) else YAMLConfig(config=config)
config_input.dereference(
@@ -65,8 +65,7 @@ def __init__(
self._config: dict = self._config_intermediate[self.driver_name()]
except KeyError as e:
raise UWConfigError("Required '%s' block missing in config" % self.driver_name()) from e
- if controller:
- self._config[STR.rundir] = self._config_intermediate[controller][STR.rundir]
+ self._delegate(controller, STR.rundir)
self.schema_file = schema_file
self._validate()
dryrun(enable=dry_run)
@@ -167,6 +166,19 @@ def _create_user_updated_config(
else:
log.debug(f"Failed to validate {path}")
+ def _delegate(self, controller: Optional[list[str]], config_key: str) -> None:
+ """
+ Selectively delegate config to controller.
+
+ :param controller: Key(s) leading to block in config controlling run-time values.
+ :param config_key: Name of config item to delegate to controller.
+ """
+ if controller:
+ val = self._config_intermediate[controller[0]]
+ for key in controller[1:]:
+ val = val[key]
+ self._config[config_key] = val[config_key]
+
# Public helper methods
@classmethod
@@ -241,7 +253,7 @@ def __init__(
dry_run: bool = False,
key_path: Optional[list[str]] = None,
schema_file: Optional[Path] = None,
- controller: Optional[str] = None,
+ controller: Optional[list[str]] = None,
):
super().__init__(
cycle=cycle,
@@ -274,7 +286,7 @@ def __init__(
dry_run: bool = False,
key_path: Optional[list[str]] = None,
schema_file: Optional[Path] = None,
- controller: Optional[str] = None,
+ controller: Optional[list[str]] = None,
):
super().__init__(
cycle=cycle,
@@ -314,7 +326,7 @@ def __init__(
dry_run: bool = False,
key_path: Optional[list[str]] = None,
schema_file: Optional[Path] = None,
- controller: Optional[str] = None,
+ controller: Optional[list[str]] = None,
):
super().__init__(
config=config,
@@ -339,7 +351,7 @@ def __init__(
key_path: Optional[list[str]] = None,
batch: bool = False,
schema_file: Optional[Path] = None,
- controller: Optional[str] = None,
+ controller: Optional[list[str]] = None,
):
super().__init__(
cycle=cycle,
@@ -351,8 +363,7 @@ def __init__(
controller=controller,
)
self._batch = batch
- if controller:
- self._config[STR.execution] = self.config_full[controller][STR.execution]
+ self._delegate(controller, STR.execution)
# Workflow tasks
@@ -541,7 +552,7 @@ def __init__(
key_path: Optional[list[str]] = None,
batch: bool = False,
schema_file: Optional[Path] = None,
- controller: Optional[str] = None,
+ controller: Optional[list[str]] = None,
):
super().__init__(
cycle=cycle,
@@ -576,7 +587,7 @@ def __init__(
key_path: Optional[list[str]] = None,
batch: bool = False,
schema_file: Optional[Path] = None,
- controller: Optional[str] = None,
+ controller: Optional[list[str]] = None,
):
super().__init__(
cycle=cycle,
@@ -618,7 +629,7 @@ def __init__(
key_path: Optional[list[str]] = None,
batch: bool = False,
schema_file: Optional[Path] = None,
- controller: Optional[str] = None,
+ controller: Optional[list[str]] = None,
):
super().__init__(
config=config,
@@ -650,7 +661,7 @@ def _add_docstring(class_: type, omit: Optional[list[str]] = None) -> None:
:param key_path: Keys leading through the config to the driver's configuration block.
:param batch: Run component via the batch system?
:param schema_file: Path to schema file to use to validate an external driver.
- :param controller: Name of block in config controlling run-time values.
+ :param controller: Key(s) leading to block in config controlling run-time values.
"""
setattr(
class_,
diff --git a/src/uwtools/tests/drivers/test_driver.py b/src/uwtools/tests/drivers/test_driver.py
index 48d72c67e..4a910f9bc 100644
--- a/src/uwtools/tests/drivers/test_driver.py
+++ b/src/uwtools/tests/drivers/test_driver.py
@@ -195,7 +195,7 @@ def test_Assets_controller(config, controller_schema):
with raises(UWConfigError):
ConcreteAssetsTimeInvariant(config=config, schema_file=controller_schema)
assert ConcreteAssetsTimeInvariant(
- config=config, schema_file=controller_schema, controller="controller"
+ config=config, schema_file=controller_schema, controller=["controller"]
)
@@ -285,6 +285,13 @@ def test_Assets__create_user_updated_config_base_file(
assert updated == expected
+def test_Assets__delegate(driverobj):
+ assert "roses" not in driverobj.config
+ driverobj._config_intermediate["plants"] = {"flowers": {"roses": "red"}}
+ driverobj._delegate(["plants", "flowers"], "roses")
+ assert driverobj.config["roses"] == "red"
+
+
def test_Assets__rundir(assetsobj):
assert assetsobj.rundir == Path(assetsobj.config["rundir"])
@@ -342,7 +349,7 @@ def test_Driver_controller(config, controller_schema):
with raises(UWConfigError):
ConcreteDriverTimeInvariant(config=config, schema_file=controller_schema)
assert ConcreteDriverTimeInvariant(
- config=config, schema_file=controller_schema, controller="controller"
+ config=config, schema_file=controller_schema, controller=["controller"]
)
From afce2f2b57a171edb7ade1c55f1c7ecf25ee4c3d Mon Sep 17 00:00:00 2001
From: Paul Madden <136389411+maddenp-noaa@users.noreply.github.com>
Date: Thu, 12 Sep 2024 13:52:57 -0600
Subject: [PATCH 2/3] UW-639 Mixed cyclestr / text support (#606)
---
docs/sections/user_guide/yaml/rocoto.rst | 24 +++++++-
.../resources/jsonschema/rocoto.jsonschema | 56 ++++++++++++-------
src/uwtools/rocoto.py | 17 +++---
src/uwtools/tests/test_rocoto.py | 37 +++++++++---
4 files changed, 93 insertions(+), 41 deletions(-)
diff --git a/docs/sections/user_guide/yaml/rocoto.rst b/docs/sections/user_guide/yaml/rocoto.rst
index a26c334a5..a655b7e11 100644
--- a/docs/sections/user_guide/yaml/rocoto.rst
+++ b/docs/sections/user_guide/yaml/rocoto.rst
@@ -86,12 +86,30 @@ In the example, the resulting log would appear in the XML file as:
.. code-block:: xml
-
- /some/path/to/&FOO;
-
+ /some/path/to/&FOO;
The ``attrs:`` block is optional within the ``cyclestr:`` block and can be used to specify the cycle offset.
+Wherever a ``cyclestr:`` block is accepted, a YAML sequence mixing text and ``cyclestr:`` blocks may also be provided. For example,
+
+.. code-block:: yaml
+
+ log:
+ - cyclestr:
+ value: "%Y%m%d%H"
+ - -through-
+ - cyclestr:
+ attrs:
+ offset: "06:00:00"
+ value: "%Y%m%d%H"
+ - .log
+
+would be rendered as
+
+.. code-block:: xml
+
+ %Y%m%d%H-through-%Y%m%d%H.log
+
Tasks Section
-------------
diff --git a/src/uwtools/resources/jsonschema/rocoto.jsonschema b/src/uwtools/resources/jsonschema/rocoto.jsonschema
index 1b3bcbf0f..882ca19b7 100644
--- a/src/uwtools/resources/jsonschema/rocoto.jsonschema
+++ b/src/uwtools/resources/jsonschema/rocoto.jsonschema
@@ -1,44 +1,60 @@
{
"$defs": {
"compoundTimeString": {
- "anyOf": [
+ "oneOf": [
{
- "type": "integer"
+ "$ref": "#/$defs/compoundTimeStringElement"
},
{
- "type": "string"
+ "items": {
+ "$ref": "#/$defs/compoundTimeStringElement"
+ },
+ "type": "array"
+ }
+ ]
+ },
+ "compoundTimeStringElement": {
+ "oneOf": [
+ {
+ "$ref": "#/$defs/cycleString"
},
{
+ "type": "integer"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "cycleString": {
+ "additionalProperties": false,
+ "properties": {
+ "cyclestr": {
"additionalProperties": false,
"properties": {
- "cyclestr": {
+ "attrs": {
"additionalProperties": false,
"properties": {
- "attrs": {
- "additionalProperties": false,
- "properties": {
- "offset": {
- "$ref": "#/$defs/time"
- }
- },
- "type": "object"
- },
- "value": {
- "type": "string"
+ "offset": {
+ "$ref": "#/$defs/time"
}
},
- "required": [
- "value"
- ],
"type": "object"
+ },
+ "value": {
+ "type": "string"
}
},
"required": [
- "cyclestr"
+ "value"
],
"type": "object"
}
- ]
+ },
+ "required": [
+ "cyclestr"
+ ],
+ "type": "object"
},
"dependency": {
"additionalProperties": false,
diff --git a/src/uwtools/rocoto.py b/src/uwtools/rocoto.py
index 6d5c1c6e3..73d8ee39f 100644
--- a/src/uwtools/rocoto.py
+++ b/src/uwtools/rocoto.py
@@ -9,6 +9,7 @@
from typing import Any, Optional, Union
from lxml import etree
+from lxml.builder import E # type: ignore
from lxml.etree import Element, SubElement, _Element
from uwtools.config.formats.yaml import YAMLConfig
@@ -113,16 +114,12 @@ def _add_compound_time_string(self, e: _Element, config: Any, tag: str) -> _Elem
:param tag: Name of child element to add.
:return: The child element.
"""
- e = SubElement(e, tag)
- if isinstance(config, dict):
- self._set_attrs(e, config)
- if subconfig := config.get(STR.cyclestr, {}):
- cyclestr = SubElement(e, STR.cyclestr)
- cyclestr.text = subconfig[STR.value]
- self._set_attrs(cyclestr, subconfig)
- else:
- e.text = str(config)
- return e
+ config = config if isinstance(config, list) else [config]
+ cyclestr = lambda x: E.cyclestr(x["cyclestr"]["value"], **x["cyclestr"].get("attrs", {}))
+ items = [cyclestr(x) if isinstance(x, dict) else str(x) for x in [tag, *config]]
+ child: _Element = E(*items) # pylint: disable=not-callable
+ e.append(child)
+ return child
def _add_metatask(self, e: _Element, config: dict, name_attr: str) -> None:
"""
diff --git a/src/uwtools/tests/test_rocoto.py b/src/uwtools/tests/test_rocoto.py
index d7e5323b2..9bd86eb96 100644
--- a/src/uwtools/tests/test_rocoto.py
+++ b/src/uwtools/tests/test_rocoto.py
@@ -6,6 +6,7 @@
from unittest.mock import DEFAULT as D
from unittest.mock import PropertyMock, patch
+from lxml import etree
from pytest import fixture, mark, raises
from uwtools import rocoto
@@ -110,22 +111,42 @@ def test_instantiate_from_cfgobj(self, assets):
cfgfile, _ = assets
assert rocoto._RocotoXML(config=YAMLConfig(cfgfile))._root.tag == "workflow"
- def test__add_compound_time_string_basic(self, instance, root):
- config = "bar"
+ @mark.parametrize("config", ["bar", 42])
+ def test__add_compound_time_string_basic(self, config, instance, root):
instance._add_compound_time_string(e=root, config=config, tag="foo")
child = root[0]
assert child.tag == "foo"
- assert child.text == "bar"
+ assert child.text == str(config)
def test__add_compound_time_string_cyclestr(self, instance, root):
- config = {"attrs": {"bar": "42"}, "cyclestr": {"attrs": {"baz": "43"}, "value": "qux"}}
+ config = {"cyclestr": {"attrs": {"baz": "42"}, "value": "qux"}}
instance._add_compound_time_string(e=root, config=config, tag="foo")
- child = root[0]
- assert child.get("bar") == "42"
- cyclestr = child[0]
- assert cyclestr.get("baz") == "43"
+ cyclestr = root[0][0]
+ assert cyclestr.get("baz") == "42"
assert cyclestr.text == "qux"
+ def test__add_compound_time_string_list(self, instance, root):
+ config = [
+ "cycle-",
+ {"cyclestr": {"value": "%s"}},
+ "-valid-",
+ {"cyclestr": {"value": "%s", "attrs": {"offset": "00:06:00"}}},
+ ".log",
+ ]
+ xml = "{}".format(
+ "".join(
+ [
+ "cycle-",
+ "%s",
+ "-valid-",
+ '%s',
+ ".log",
+ ]
+ )
+ )
+ instance._add_compound_time_string(e=root, config=config, tag="a")
+ assert etree.tostring(root[0]).decode("utf-8") == xml
+
def test__add_metatask(self, instance, root):
config = {
"metatask_foo": "1",
From b7ed02f20e87a92cc5a17facff17bafb53874e1b Mon Sep 17 00:00:00 2001
From: Christina Holt <56881914+christinaholtNOAA@users.noreply.github.com>
Date: Wed, 18 Sep 2024 10:50:37 -0600
Subject: [PATCH 3/3] Fixes from integration with SRW's make_orog (#603)
A set of changes to the drivers run as part of SRW's make_orog task.
- Run commands seemed not to work as expected, so changed them to use config files.
- More information was needed in the UW YAML in a couple of instances.
Also ordered the tests in each of the drivers.
---
.../cli/drivers/filter_topo/help.out | 2 +
.../cli/drivers/filter_topo/show-schema.out | 2 +-
.../user_guide/cli/drivers/orog_gsl/help.out | 2 +
.../user_guide/cli/drivers/shave/help.out | 2 +
.../yaml/components/filter_topo.rst | 8 ++++
.../user_guide/yaml/components/shave.rst | 6 ++-
docs/shared/filter_topo.yaml | 2 +
docs/shared/orog.yaml | 12 +++---
docs/shared/shave.yaml | 3 +-
src/uwtools/drivers/filter_topo.py | 16 ++++++-
src/uwtools/drivers/orog.py | 23 ++++++++--
src/uwtools/drivers/orog_gsl.py | 25 ++++++++++-
src/uwtools/drivers/shave.py | 43 +++++++++++++++----
.../jsonschema/filter-topo.jsonschema | 10 ++++-
.../resources/jsonschema/orog.jsonschema | 2 +-
.../resources/jsonschema/shave.jsonschema | 12 ++++--
src/uwtools/tests/drivers/test_cdeps.py | 8 ++--
src/uwtools/tests/drivers/test_chgres_cube.py | 8 ++--
src/uwtools/tests/drivers/test_driver.py | 38 ++++++++--------
src/uwtools/tests/drivers/test_esg_grid.py | 8 ++--
src/uwtools/tests/drivers/test_filter_topo.py | 23 +++++++---
src/uwtools/tests/drivers/test_fv3.py | 8 ++--
.../tests/drivers/test_global_equiv_resol.py | 8 ++--
src/uwtools/tests/drivers/test_ioda.py | 12 +++---
src/uwtools/tests/drivers/test_jedi.py | 16 +++----
src/uwtools/tests/drivers/test_make_hgrid.py | 8 ++--
.../tests/drivers/test_make_solo_mosaic.py | 12 +++---
src/uwtools/tests/drivers/test_mpas.py | 26 +++++------
src/uwtools/tests/drivers/test_mpas_init.py | 8 ++--
src/uwtools/tests/drivers/test_orog.py | 27 ++++++++----
src/uwtools/tests/drivers/test_orog_gsl.py | 30 +++++++++----
src/uwtools/tests/drivers/test_schism.py | 8 ++--
.../tests/drivers/test_sfc_climo_gen.py | 8 ++--
src/uwtools/tests/drivers/test_shave.py | 37 ++++++++++------
src/uwtools/tests/drivers/test_support.py | 8 ++--
src/uwtools/tests/drivers/test_ungrib.py | 16 +++----
src/uwtools/tests/drivers/test_upp.py | 12 +++---
src/uwtools/tests/drivers/test_ww3.py | 8 ++--
src/uwtools/tests/test_schemas.py | 18 +++++---
39 files changed, 345 insertions(+), 180 deletions(-)
diff --git a/docs/sections/user_guide/cli/drivers/filter_topo/help.out b/docs/sections/user_guide/cli/drivers/filter_topo/help.out
index 1c426c0f4..80ff221b5 100644
--- a/docs/sections/user_guide/cli/drivers/filter_topo/help.out
+++ b/docs/sections/user_guide/cli/drivers/filter_topo/help.out
@@ -12,6 +12,8 @@ Optional arguments:
Positional arguments:
TASK
+ filtered_output_file
+ The filtered output file staged from raw input
input_grid_file
The input grid file
namelist_file
diff --git a/docs/sections/user_guide/cli/drivers/filter_topo/show-schema.out b/docs/sections/user_guide/cli/drivers/filter_topo/show-schema.out
index d1ff44ffc..990d2cbed 100644
--- a/docs/sections/user_guide/cli/drivers/filter_topo/show-schema.out
+++ b/docs/sections/user_guide/cli/drivers/filter_topo/show-schema.out
@@ -6,7 +6,7 @@
"config": {
"additionalProperties": false,
"properties": {
- "input_grid_file": {
+ "filtered_orog": {
"type": "string"
...
"rundir"
diff --git a/docs/sections/user_guide/cli/drivers/orog_gsl/help.out b/docs/sections/user_guide/cli/drivers/orog_gsl/help.out
index b49285d3d..fde960dcd 100644
--- a/docs/sections/user_guide/cli/drivers/orog_gsl/help.out
+++ b/docs/sections/user_guide/cli/drivers/orog_gsl/help.out
@@ -12,6 +12,8 @@ Optional arguments:
Positional arguments:
TASK
+ input_config_file
+ The input config file
input_grid_file
The input grid file
provisioned_rundir
diff --git a/docs/sections/user_guide/cli/drivers/shave/help.out b/docs/sections/user_guide/cli/drivers/shave/help.out
index 7cd91374e..75ffdf963 100644
--- a/docs/sections/user_guide/cli/drivers/shave/help.out
+++ b/docs/sections/user_guide/cli/drivers/shave/help.out
@@ -12,6 +12,8 @@ Optional arguments:
Positional arguments:
TASK
+ input_config_file
+ The input config file
provisioned_rundir
Run directory provisioned with all required content
run
diff --git a/docs/sections/user_guide/yaml/components/filter_topo.rst b/docs/sections/user_guide/yaml/components/filter_topo.rst
index 638a76c73..4932c7235 100644
--- a/docs/sections/user_guide/yaml/components/filter_topo.rst
+++ b/docs/sections/user_guide/yaml/components/filter_topo.rst
@@ -20,10 +20,18 @@ config:
Configuration parameters for the ``orog_gsl`` component.
+ **filtered_orog:**
+
+ Name of the filtered output file.
+
**input_grid_file:**
Path to the tiled input grid file.
+ **input_raw_orog:**
+
+ Path to the raw orography file. The output of the ``orog`` driver.
+
namelist:
^^^^^^^^^
diff --git a/docs/sections/user_guide/yaml/components/shave.rst b/docs/sections/user_guide/yaml/components/shave.rst
index e29ad3920..9c3ff4c06 100644
--- a/docs/sections/user_guide/yaml/components/shave.rst
+++ b/docs/sections/user_guide/yaml/components/shave.rst
@@ -27,7 +27,7 @@ Describes the required parameters to run a ``shave`` configuration.
Name of the grid file with extra points to be shaved.
- **nh4:**
+ **nhalo:**
The number of halo rows/columns.
@@ -39,6 +39,10 @@ Describes the required parameters to run a ``shave`` configuration.
The j/y dimensions of the compute domain (not including halo)
+ **output_grid_file:**
+
+ The path to the output file.
+
rundir:
^^^^^^^
diff --git a/docs/shared/filter_topo.yaml b/docs/shared/filter_topo.yaml
index c3be0fa05..73fb28bbb 100644
--- a/docs/shared/filter_topo.yaml
+++ b/docs/shared/filter_topo.yaml
@@ -1,6 +1,8 @@
filter_topo:
config:
+ filtered_orog: C403_filtered_orog.tile7.nc
input_grid_file: /path/to/C403_grid.tile7.halo6.nc
+ input_raw_orog: /path/to/out.oro.nc
execution:
batchargs:
cores: 1
diff --git a/docs/shared/orog.yaml b/docs/shared/orog.yaml
index 46df4f687..9fbdef97d 100644
--- a/docs/shared/orog.yaml
+++ b/docs/shared/orog.yaml
@@ -4,12 +4,12 @@ orog:
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
+ 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/shave.yaml b/docs/shared/shave.yaml
index 18b55ea46..feacb1941 100644
--- a/docs/shared/shave.yaml
+++ b/docs/shared/shave.yaml
@@ -1,9 +1,10 @@
shave:
config:
input_grid_file: /path/to/input/grid/file
- nh4: 1
+ nhalo: 0
nx: 214
ny: 128
+ output_grid_file: /path/to/C403_oro_data.tile7.halo0.nc
execution:
batchargs:
cores: 1
diff --git a/src/uwtools/drivers/filter_topo.py b/src/uwtools/drivers/filter_topo.py
index 68261a5f7..849942034 100644
--- a/src/uwtools/drivers/filter_topo.py
+++ b/src/uwtools/drivers/filter_topo.py
@@ -10,7 +10,7 @@
from uwtools.drivers.driver import DriverTimeInvariant
from uwtools.drivers.support import set_driver_docstring
from uwtools.strings import STR
-from uwtools.utils.tasks import symlink
+from uwtools.utils.tasks import filecopy, symlink
class FilterTopo(DriverTimeInvariant):
@@ -27,10 +27,21 @@ def input_grid_file(self):
"""
src = Path(self.config["config"]["input_grid_file"])
dst = Path(self.config[STR.rundir], src.name)
- yield self.taskname("Input grid")
+ yield self.taskname(f"Input grid {str(src)}")
yield asset(dst, dst.is_file)
yield symlink(target=src, linkname=dst)
+ @task
+ def filtered_output_file(self):
+ """
+ The filtered output file staged from raw input.
+ """
+ src = Path(self.config["config"]["input_raw_orog"])
+ dst = self.rundir / self.config["config"]["filtered_orog"]
+ yield self.taskname(f"Raw orog input {str(dst)}")
+ yield asset(dst, dst.is_file)
+ yield filecopy(src=src, dst=dst)
+
@task
def namelist_file(self):
"""
@@ -56,6 +67,7 @@ def provisioned_rundir(self):
yield self.taskname("provisioned run directory")
yield [
self.input_grid_file(),
+ self.filtered_output_file(),
self.namelist_file(),
self.runscript(),
]
diff --git a/src/uwtools/drivers/orog.py b/src/uwtools/drivers/orog.py
index b82c56857..6a1d58d96 100644
--- a/src/uwtools/drivers/orog.py
+++ b/src/uwtools/drivers/orog.py
@@ -37,7 +37,7 @@ def grid_file(self):
The input grid file.
"""
grid_file = Path(self.config["grid_file"])
- yield self.taskname("Input grid file")
+ yield self.taskname(f"Input grid file {grid_file}")
yield asset(grid_file, grid_file.is_file) if str(grid_file) != "none" else None
@task
@@ -62,8 +62,9 @@ def input_config_file(self):
"blat",
]
inputs = " ".join([str(inputs[i]) for i in ordered_entries])
- outgrid = self.config["grid_file"]
- orogfile = self.config.get("orog_file")
+ outgrid = "'{}'".format(self.config["grid_file"])
+ if orogfile := self.config.get("orog_file"):
+ orogfile = "'{}'".format(orogfile)
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]
@@ -82,6 +83,22 @@ def provisioned_rundir(self):
self.runscript(),
]
+ @task
+ def runscript(self):
+ """
+ The runscript.
+ """
+ path = self._runscript_path
+ yield self.taskname(path.name)
+ yield asset(path, path.is_file)
+ yield None
+ envvars = {
+ "KMP_AFFINITY": "disabled",
+ "OMP_NUM_THREADS": self.config.get(STR.execution, {}).get(STR.threads, 1),
+ "OMP_STACKSIZE": "2048m",
+ }
+ self._write_runscript(path=path, envvars=envvars)
+
# Public helper methods
@classmethod
diff --git a/src/uwtools/drivers/orog_gsl.py b/src/uwtools/drivers/orog_gsl.py
index f9dcdb6fe..8bf41c19a 100644
--- a/src/uwtools/drivers/orog_gsl.py
+++ b/src/uwtools/drivers/orog_gsl.py
@@ -9,6 +9,7 @@
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
@@ -19,6 +20,19 @@ class OrogGSL(DriverTimeInvariant):
# Workflow tasks
+ @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 None
+ inputs = [str(self.config["config"][k]) for k in ("tile", "resolution", "halo")]
+ with writable(path) as f:
+ print("\n".join(inputs), file=f)
+
@task
def input_grid_file(self):
"""
@@ -40,6 +54,7 @@ def provisioned_rundir(self):
"""
yield self.taskname("provisioned run directory")
yield [
+ self.input_config_file(),
self.input_grid_file(),
self.runscript(),
self.topo_data_2p5m(),
@@ -81,14 +96,20 @@ def driver_name(cls) -> str:
# Private helper methods
+ @property
+ def _input_config_path(self) -> Path:
+ """
+ Path to the input config file.
+ """
+ return self.rundir / "orog_gsl.cfg"
+
@property
def _runcmd(self):
"""
The full command-line component invocation.
"""
- inputs = [str(self.config["config"][k]) for k in ("tile", "resolution", "halo")]
executable = self.config[STR.execution][STR.executable]
- return "echo '%s' | %s" % ("\n".join(inputs), executable)
+ return "%s < %s" % (executable, self._input_config_path.name)
set_driver_docstring(OrogGSL)
diff --git a/src/uwtools/drivers/shave.py b/src/uwtools/drivers/shave.py
index 868d38982..1f809e714 100644
--- a/src/uwtools/drivers/shave.py
+++ b/src/uwtools/drivers/shave.py
@@ -2,11 +2,15 @@
A driver for shave.
"""
-from iotaa import tasks
+from pathlib import Path
+
+from iotaa import asset, 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 file
class Shave(DriverTimeInvariant):
@@ -16,13 +20,34 @@ class Shave(DriverTimeInvariant):
# Workflow tasks
+ @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)
+ config = self.config["config"]
+ input_file = Path(config["input_grid_file"])
+ yield file(path=input_file)
+ flags = [
+ config[key] for key in ["nx", "ny", "nhalo", "input_grid_file", "output_grid_file"]
+ ]
+ content = "{} {} {} '{}' '{}'".format(*flags)
+ with writable(path) as f:
+ print(content, file=f)
+
@tasks
def provisioned_rundir(self):
"""
Run directory provisioned with all required content.
"""
yield self.taskname("provisioned run directory")
- yield self.runscript()
+ yield [
+ self.input_config_file(),
+ self.runscript(),
+ ]
# Public helper methods
@@ -35,18 +60,20 @@ def driver_name(cls) -> str:
# Private helper methods
+ @property
+ def _input_config_path(self) -> Path:
+ """
+ Path to the input config file.
+ """
+ return self.rundir / "shave.cfg"
+
@property
def _runcmd(self):
"""
The full command-line component invocation.
"""
executable = self.config[STR.execution][STR.executable]
- config = self.config["config"]
- input_file = config["input_grid_file"]
- output_file = input_file.replace(".nc", "_NH0.nc")
- 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)}"
+ return "%s < %s" % (executable, self._input_config_path.name)
set_driver_docstring(Shave)
diff --git a/src/uwtools/resources/jsonschema/filter-topo.jsonschema b/src/uwtools/resources/jsonschema/filter-topo.jsonschema
index 0f88216b3..a988a1fde 100644
--- a/src/uwtools/resources/jsonschema/filter-topo.jsonschema
+++ b/src/uwtools/resources/jsonschema/filter-topo.jsonschema
@@ -6,12 +6,20 @@
"config": {
"additionalProperties": false,
"properties": {
+ "filtered_orog": {
+ "type": "string"
+ },
"input_grid_file": {
"type": "string"
+ },
+ "input_raw_orog": {
+ "type": "string"
}
},
"required": [
- "input_grid_file"
+ "filtered_orog",
+ "input_grid_file",
+ "input_raw_orog"
]
},
"execution": {
diff --git a/src/uwtools/resources/jsonschema/orog.jsonschema b/src/uwtools/resources/jsonschema/orog.jsonschema
index 6798fb1cd..6474b0445 100644
--- a/src/uwtools/resources/jsonschema/orog.jsonschema
+++ b/src/uwtools/resources/jsonschema/orog.jsonschema
@@ -4,7 +4,7 @@
"additionalProperties": false,
"properties": {
"execution": {
- "$ref": "urn:uwtools:execution-serial"
+ "$ref": "urn:uwtools:execution"
},
"files_to_link": {
"$ref": "urn:uwtools:files-to-stage"
diff --git a/src/uwtools/resources/jsonschema/shave.jsonschema b/src/uwtools/resources/jsonschema/shave.jsonschema
index 7426e0b25..ad3b9f741 100644
--- a/src/uwtools/resources/jsonschema/shave.jsonschema
+++ b/src/uwtools/resources/jsonschema/shave.jsonschema
@@ -9,8 +9,8 @@
"input_grid_file": {
"type": "string"
},
- "nh4": {
- "minimum": 1,
+ "nhalo": {
+ "minimum": 0,
"type": "integer"
},
"nx": {
@@ -20,13 +20,17 @@
"ny": {
"minimum": 1,
"type": "integer"
+ },
+ "output_grid_file": {
+ "type": "string"
}
},
"required": [
"input_grid_file",
- "nh4",
+ "nhalo",
"nx",
- "ny"
+ "ny",
+ "output_grid_file"
]
},
"execution": {
diff --git a/src/uwtools/tests/drivers/test_cdeps.py b/src/uwtools/tests/drivers/test_cdeps.py
index 66ad90867..6d7d5a378 100644
--- a/src/uwtools/tests/drivers/test_cdeps.py
+++ b/src/uwtools/tests/drivers/test_cdeps.py
@@ -43,6 +43,10 @@ def test_CDEPS_atm(driverobj):
atm_nml.assert_called_once_with()
+def test_CDEPS_driver_name(driverobj):
+ assert driverobj.driver_name() == CDEPS.driver_name() == "cdeps"
+
+
@mark.parametrize("group", ["atm", "ocn"])
def test_CDEPS_nml(caplog, driverobj, group):
log.setLevel(logging.DEBUG)
@@ -111,10 +115,6 @@ def test_CDEPS_streams(driverobj, group):
assert f.read().strip() == dedent(expected).strip()
-def test_CDEPS_driver_name(driverobj):
- assert driverobj.driver_name() == CDEPS.driver_name() == "cdeps"
-
-
def test_CDEPS__model_namelist_file(driverobj):
group = "atm_in"
path = Path("/path/to/some.nml")
diff --git a/src/uwtools/tests/drivers/test_chgres_cube.py b/src/uwtools/tests/drivers/test_chgres_cube.py
index de399e7fe..447037cca 100644
--- a/src/uwtools/tests/drivers/test_chgres_cube.py
+++ b/src/uwtools/tests/drivers/test_chgres_cube.py
@@ -103,6 +103,10 @@ def test_ChgresCube(method):
assert getattr(ChgresCube, method) is getattr(Driver, method)
+def test_ChgresCube_driver_name(driverobj):
+ assert driverobj.driver_name() == ChgresCube.driver_name() == "chgres_cube"
+
+
def test_ChgresCube_namelist_file(caplog, driverobj):
log.setLevel(logging.DEBUG)
dst = driverobj.rundir / "fort.41"
@@ -147,9 +151,5 @@ def test_ChgresCube_runscript(driverobj):
assert [type(runscript.call_args.kwargs[x]) for x in args] == types
-def test_ChgresCube_driver_name(driverobj):
- assert driverobj.driver_name() == ChgresCube.driver_name() == "chgres_cube"
-
-
def test_ChgresCube_taskname(driverobj):
assert driverobj.taskname("foo") == "20240201 18Z chgres_cube foo"
diff --git a/src/uwtools/tests/drivers/test_driver.py b/src/uwtools/tests/drivers/test_driver.py
index 4a910f9bc..9b5b11a3f 100644
--- a/src/uwtools/tests/drivers/test_driver.py
+++ b/src/uwtools/tests/drivers/test_driver.py
@@ -36,13 +36,13 @@ def atask(self):
yield "atask"
yield asset("atask", lambda: True)
- def provisioned_rundir(self):
- pass
-
@classmethod
def driver_name(cls) -> str:
return "concrete"
+ def provisioned_rundir(self):
+ pass
+
def _validate(self, schema_file: Optional[Path] = None) -> None:
pass
@@ -532,6 +532,10 @@ def test_Driver__runscript(driverobj):
)
+def test_Driver__runscript_done_file(driverobj):
+ assert driverobj._runscript_done_file == "runscript.concrete.done"
+
+
def test_Driver__runscript_execution_only(driverobj):
expected = """
#!/bin/bash
@@ -542,10 +546,6 @@ def test_Driver__runscript_execution_only(driverobj):
assert driverobj._runscript(execution=["foo", "bar"]) == dedent(expected).strip()
-def test_Driver__runscript_done_file(driverobj):
- assert driverobj._runscript_done_file == "runscript.concrete.done"
-
-
def test_Driver__runscript_path(driverobj):
rundir = Path(driverobj.config["rundir"])
assert driverobj._runscript_path == rundir / "runscript.concrete"
@@ -558,6 +558,18 @@ def test_Driver__scheduler(driverobj):
JobScheduler.get_scheduler.assert_called_with(driverobj._run_resources)
+def test_Driver__validate_external(config):
+ schema_file = Path("/path/to/jsonschema")
+ with patch.object(ConcreteAssetsTimeInvariant, "_validate", driver.Driver._validate):
+ with patch.object(driver, "validate_external") as validate_external:
+ assetsobj = ConcreteAssetsTimeInvariant(schema_file=schema_file, config=config)
+ assert validate_external.call_args_list[0].kwargs == {
+ "schema_file": schema_file,
+ "desc": "concrete config",
+ "config": assetsobj.config_full,
+ }
+
+
def test_Driver__validate_internal(assetsobj):
with patch.object(assetsobj, "_validate", driver.Driver._validate):
with patch.object(driver, "validate_internal") as validate_internal:
@@ -574,18 +586,6 @@ def test_Driver__validate_internal(assetsobj):
}
-def test_Driver__validate_external(config):
- schema_file = Path("/path/to/jsonschema")
- with patch.object(ConcreteAssetsTimeInvariant, "_validate", driver.Driver._validate):
- with patch.object(driver, "validate_external") as validate_external:
- assetsobj = ConcreteAssetsTimeInvariant(schema_file=schema_file, config=config)
- assert validate_external.call_args_list[0].kwargs == {
- "schema_file": schema_file,
- "desc": "concrete config",
- "config": assetsobj.config_full,
- }
-
-
def test_Driver__write_runscript(driverobj):
rundir = driverobj.config["rundir"]
path = Path(rundir, "runscript")
diff --git a/src/uwtools/tests/drivers/test_esg_grid.py b/src/uwtools/tests/drivers/test_esg_grid.py
index 950ce2d6d..f7d891698 100644
--- a/src/uwtools/tests/drivers/test_esg_grid.py
+++ b/src/uwtools/tests/drivers/test_esg_grid.py
@@ -84,6 +84,10 @@ def test_ESGGrid(method):
assert getattr(ESGGrid, method) is getattr(Driver, method)
+def test_ESGGrid_driver_name(driverobj):
+ assert driverobj.driver_name() == ESGGrid.driver_name() == "esg_grid"
+
+
def test_ESGGrid_namelist_file(caplog, driverobj):
log.setLevel(logging.DEBUG)
dst = driverobj.rundir / "regional_grid.nml"
@@ -121,7 +125,3 @@ def test_ESGGrid_provisioned_rundir(driverobj):
driverobj.provisioned_rundir()
for m in mocks:
mocks[m].assert_called_once_with()
-
-
-def test_FilterTopo_driver_name(driverobj):
- assert driverobj.driver_name() == ESGGrid.driver_name() == "esg_grid"
diff --git a/src/uwtools/tests/drivers/test_filter_topo.py b/src/uwtools/tests/drivers/test_filter_topo.py
index 54b243fdf..490ca3695 100644
--- a/src/uwtools/tests/drivers/test_filter_topo.py
+++ b/src/uwtools/tests/drivers/test_filter_topo.py
@@ -21,10 +21,14 @@
def config(tmp_path):
input_grid_file = tmp_path / "C403_grid.tile7.halo4.nc"
input_grid_file.touch()
+ orog_output = tmp_path / "out.oro.nc"
+ orog_output.touch()
return {
"filter_topo": {
"config": {
+ "filtered_orog": "C403_filtered_orog.tile7.nc",
"input_grid_file": str(input_grid_file),
+ "input_raw_orog": str(orog_output),
},
"execution": {
"executable": "/path/to/orog_gsl",
@@ -79,6 +83,17 @@ def test_FilterTopo(method):
assert getattr(FilterTopo, method) is getattr(Driver, method)
+def test_FilterTopo_driver_name(driverobj):
+ assert driverobj.driver_name() == FilterTopo.driver_name() == "filter_topo"
+
+
+def test_FilterTopo_filtered_output_file(driverobj):
+ path = Path(driverobj.config["rundir"], "C403_filtered_orog.tile7.nc")
+ assert not path.is_file()
+ driverobj.filtered_output_file()
+ assert path.is_file()
+
+
def test_FilterTopo_input_grid_file(driverobj):
path = Path(driverobj.config["rundir"], "C403_grid.tile7.halo4.nc")
assert not path.is_file()
@@ -94,11 +109,9 @@ def test_FilterTopo_namelist_file(driverobj):
def test_FilterTopo_provisioned_rundir(driverobj):
- with patch.multiple(driverobj, input_grid_file=D, namelist_file=D, runscript=D) as mocks:
+ with patch.multiple(
+ driverobj, input_grid_file=D, filtered_output_file=D, namelist_file=D, runscript=D
+ ) as mocks:
driverobj.provisioned_rundir()
for m in mocks:
mocks[m].assert_called_once_with()
-
-
-def test_FilterTopo_driver_name(driverobj):
- assert driverobj.driver_name() == FilterTopo.driver_name() == "filter_topo"
diff --git a/src/uwtools/tests/drivers/test_fv3.py b/src/uwtools/tests/drivers/test_fv3.py
index ebc2a08bf..2c7cf069a 100644
--- a/src/uwtools/tests/drivers/test_fv3.py
+++ b/src/uwtools/tests/drivers/test_fv3.py
@@ -126,6 +126,10 @@ def test_FV3_diag_table_warn(caplog, driverobj):
assert logged(caplog, "No 'diag_table' defined in config")
+def test_FV3_driver_name(driverobj):
+ assert driverobj.driver_name() == FV3.driver_name() == "fv3"
+
+
def test_FV3_field_table(driverobj):
src = driverobj.rundir / "field_table.in"
src.touch()
@@ -247,9 +251,5 @@ def test_FV3_runscript(driverobj):
assert [type(runscript.call_args.kwargs[x]) for x in args] == types
-def test_FV3_driver_name(driverobj):
- assert driverobj.driver_name() == FV3.driver_name() == "fv3"
-
-
def test_FV3_taskname(driverobj):
assert driverobj.taskname("foo") == "20240201 18Z fv3 foo"
diff --git a/src/uwtools/tests/drivers/test_global_equiv_resol.py b/src/uwtools/tests/drivers/test_global_equiv_resol.py
index f95201940..bd993f14a 100644
--- a/src/uwtools/tests/drivers/test_global_equiv_resol.py
+++ b/src/uwtools/tests/drivers/test_global_equiv_resol.py
@@ -64,6 +64,10 @@ def test_GlobalEquivResol(method):
assert getattr(GlobalEquivResol, method) is getattr(Driver, method)
+def test_GlobalEquivResol_driver_name(driverobj):
+ assert driverobj.driver_name() == GlobalEquivResol.driver_name() == "global_equiv_resol"
+
+
def test_GlobalEquivResol_input_file(driverobj):
path = Path(driverobj.config["input_grid_file"])
assert not driverobj.input_file().ready()
@@ -83,10 +87,6 @@ def test_GlobalEquivResol_provisioned_rundir(driverobj):
mocks[m].assert_called_once_with()
-def test_FilterTopo_driver_name(driverobj):
- assert driverobj.driver_name() == GlobalEquivResol.driver_name() == "global_equiv_resol"
-
-
def test_GlobalEquivResol__runcmd(driverobj):
cmd = driverobj._runcmd
input_file_path = driverobj.config["input_grid_file"]
diff --git a/src/uwtools/tests/drivers/test_ioda.py b/src/uwtools/tests/drivers/test_ioda.py
index 65527f86c..50936d051 100644
--- a/src/uwtools/tests/drivers/test_ioda.py
+++ b/src/uwtools/tests/drivers/test_ioda.py
@@ -87,6 +87,10 @@ def test_IODA(method):
assert getattr(IODA, method) is getattr(JEDIBase, method)
+def test_IODA_driver_name(driverobj):
+ assert driverobj.driver_name() == IODA.driver_name() == "ioda"
+
+
def test_IODA_provisioned_rundir(driverobj):
with patch.multiple(
driverobj,
@@ -100,8 +104,8 @@ def test_IODA_provisioned_rundir(driverobj):
mocks[m].assert_called_once_with()
-def test_IODA_driver_name(driverobj):
- assert driverobj.driver_name() == IODA.driver_name() == "ioda"
+def test_IODA_taskname(driverobj):
+ assert driverobj.taskname("foo") == "20240501 06Z ioda foo"
def test_IODA__config_fn(driverobj):
@@ -111,7 +115,3 @@ def test_IODA__config_fn(driverobj):
def test_IODA__runcmd(driverobj):
config = str(driverobj.rundir / driverobj._config_fn)
assert driverobj._runcmd == f"/path/to/bufr2ioda.x {config}"
-
-
-def test_IODA_taskname(driverobj):
- assert driverobj.taskname("foo") == "20240501 06Z ioda foo"
diff --git a/src/uwtools/tests/drivers/test_jedi.py b/src/uwtools/tests/drivers/test_jedi.py
index ebd7ecfcc..d5378e281 100644
--- a/src/uwtools/tests/drivers/test_jedi.py
+++ b/src/uwtools/tests/drivers/test_jedi.py
@@ -121,6 +121,10 @@ def test_JEDI_configuration_file_missing_base_file(caplog, driverobj):
assert regex_logged(caplog, f"{base_file}: State: Not Ready (external asset)")
+def test_JEDI_driver_name(driverobj):
+ assert driverobj.driver_name() == JEDI.driver_name() == "jedi"
+
+
def test_JEDI_files_copied(driverobj):
with patch.object(jedi_base, "filecopy") as filecopy:
driverobj._config["rundir"] = "/path/to/run"
@@ -164,6 +168,10 @@ def test_JEDI_provisioned_rundir(driverobj):
mocks[m].assert_called_once_with()
+def test_JEDI_taskname(driverobj):
+ assert driverobj.taskname("foo") == "20240201 18Z jedi foo"
+
+
def test_JEDI_validate_only(caplog, driverobj):
@external
@@ -188,10 +196,6 @@ def file(path: Path):
assert regex_logged(caplog, "Config is valid")
-def test_JEDI_driver_name(driverobj):
- assert driverobj.driver_name() == JEDI.driver_name() == "jedi"
-
-
def test_JEDI__config_fn(driverobj):
assert driverobj._config_fn == "jedi.yaml"
@@ -202,7 +206,3 @@ def test_JEDI__runcmd(driverobj):
assert (
driverobj._runcmd == f"srun --export=ALL --ntasks $SLURM_CPUS_ON_NODE {executable} {config}"
)
-
-
-def test_JEDI_taskname(driverobj):
- assert driverobj.taskname("foo") == "20240201 18Z jedi foo"
diff --git a/src/uwtools/tests/drivers/test_make_hgrid.py b/src/uwtools/tests/drivers/test_make_hgrid.py
index 74874e40d..bcf90d86c 100644
--- a/src/uwtools/tests/drivers/test_make_hgrid.py
+++ b/src/uwtools/tests/drivers/test_make_hgrid.py
@@ -69,6 +69,10 @@ def test_MakeHgrid(method):
assert getattr(MakeHgrid, method) is getattr(Driver, method)
+def test_MakeHgrid_driver_name(driverobj):
+ assert driverobj.driver_name() == MakeHgrid.driver_name() == "make_hgrid"
+
+
def test_MakeHgrid_provisioned_rundir(driverobj):
with patch.multiple(driverobj, runscript=D) as mocks:
driverobj.provisioned_rundir()
@@ -76,10 +80,6 @@ def test_MakeHgrid_provisioned_rundir(driverobj):
mocks[m].assert_called_once_with()
-def test_MakeHgrid_driver_name(driverobj):
- assert driverobj.driver_name() == MakeHgrid.driver_name() == "make_hgrid"
-
-
def test_MakeHgrid__runcmd(driverobj):
expected = [
"/path/to/make_hgrid",
diff --git a/src/uwtools/tests/drivers/test_make_solo_mosaic.py b/src/uwtools/tests/drivers/test_make_solo_mosaic.py
index bdd0f4573..54ad6f5a6 100644
--- a/src/uwtools/tests/drivers/test_make_solo_mosaic.py
+++ b/src/uwtools/tests/drivers/test_make_solo_mosaic.py
@@ -64,14 +64,18 @@ def test_MakeSoloMosaic(method):
assert getattr(MakeSoloMosaic, method) is getattr(Driver, method)
+def test_MakeSoloMosaic_driver_name(driverobj):
+ assert driverobj.driver_name() == MakeSoloMosaic.driver_name() == "make_solo_mosaic"
+
+
def test_MakeSoloMosaic_provisioned_rundir(driverobj):
with patch.object(driverobj, "runscript") as runscript:
driverobj.provisioned_rundir()
runscript.assert_called_once_with()
-def test_MakeSoloMosaic_driver_name(driverobj):
- assert driverobj.driver_name() == MakeSoloMosaic.driver_name() == "make_solo_mosaic"
+def test_MakeSoloMosaic_taskname(driverobj):
+ assert driverobj.taskname("foo") == "make_solo_mosaic foo"
def test_MakeSoloMosaic__runcmd(driverobj):
@@ -80,9 +84,5 @@ def test_MakeSoloMosaic__runcmd(driverobj):
assert cmd == f"/path/to/make_solo_mosaic.exe --dir {dir_path} --num_tiles 1"
-def test_MakeSoloMosaic_taskname(driverobj):
- assert driverobj.taskname("foo") == "make_solo_mosaic foo"
-
-
def test_MakeSoloMosaic__validate(driverobj):
driverobj._validate()
diff --git a/src/uwtools/tests/drivers/test_mpas.py b/src/uwtools/tests/drivers/test_mpas.py
index b85a55588..d1a85ac23 100644
--- a/src/uwtools/tests/drivers/test_mpas.py
+++ b/src/uwtools/tests/drivers/test_mpas.py
@@ -156,6 +156,10 @@ def test_MPAS_boundary_files(driverobj, cycle):
assert all(link.is_symlink() for link in links)
+def test_MPAS_driver_name(driverobj):
+ assert driverobj.driver_name() == MPAS.driver_name() == "mpas"
+
+
@mark.parametrize(
"key,task,test",
[("files_to_copy", "files_copied", "is_file"), ("files_to_link", "files_linked", "is_symlink")],
@@ -189,6 +193,15 @@ def test_MPAS_namelist_file(caplog, driverobj):
assert isinstance(nml, f90nml.Namelist)
+def test_MPAS_namelist_file_fails_validation(caplog, driverobj):
+ log.setLevel(logging.DEBUG)
+ driverobj._config["namelist"]["update_values"]["nhyd_model"]["foo"] = None
+ path = Path(refs(driverobj.namelist_file()))
+ assert not path.exists()
+ assert logged(caplog, f"Failed to validate {path}")
+ assert logged(caplog, " None is not of type 'array', 'boolean', 'number', 'string'")
+
+
def test_MPAS_namelist_file_long_duration(caplog, config, cycle):
log.setLevel(logging.DEBUG)
config["mpas"]["length"] = 120
@@ -203,15 +216,6 @@ def test_MPAS_namelist_file_long_duration(caplog, config, cycle):
assert nml["nhyd_model"]["config_run_duration"] == "5_0:00:00"
-def test_MPAS_namelist_file_fails_validation(caplog, driverobj):
- log.setLevel(logging.DEBUG)
- driverobj._config["namelist"]["update_values"]["nhyd_model"]["foo"] = None
- path = Path(refs(driverobj.namelist_file()))
- assert not path.exists()
- assert logged(caplog, f"Failed to validate {path}")
- assert logged(caplog, " None is not of type 'array', 'boolean', 'number', 'string'")
-
-
def test_MPAS_namelist_file_missing_base_file(caplog, driverobj):
log.setLevel(logging.DEBUG)
base_file = str(Path(driverobj.config["rundir"], "missing.nml"))
@@ -236,10 +240,6 @@ def test_MPAS_provisioned_rundir(driverobj):
mocks[m].assert_called_once_with()
-def test_MPAS_driver_name(driverobj):
- assert driverobj.driver_name() == MPAS.driver_name() == "mpas"
-
-
def test_MPAS_streams_file(config, driverobj):
streams_file(config, driverobj, "mpas")
diff --git a/src/uwtools/tests/drivers/test_mpas_init.py b/src/uwtools/tests/drivers/test_mpas_init.py
index f9784755c..cac08798a 100644
--- a/src/uwtools/tests/drivers/test_mpas_init.py
+++ b/src/uwtools/tests/drivers/test_mpas_init.py
@@ -137,6 +137,10 @@ def test_MPASInit_boundary_files(cycle, driverobj):
assert all(link.is_symlink() for link in links)
+def test_MPASInit_driver_name(driverobj):
+ assert driverobj.driver_name() == MPASInit.driver_name() == "mpas_init"
+
+
@mark.parametrize(
"key,task,test",
[("files_to_copy", "files_copied", "is_file"), ("files_to_link", "files_linked", "is_symlink")],
@@ -211,10 +215,6 @@ def test_MPASInit_provisioned_rundir(driverobj):
mocks[m].assert_called_once_with()
-def test_MPASInit_driver_name(driverobj):
- assert driverobj.driver_name() == MPASInit.driver_name() == "mpas_init"
-
-
def test_MPASInit_streams_file(config, driverobj):
streams_file(config, driverobj, "mpas_init")
diff --git a/src/uwtools/tests/drivers/test_orog.py b/src/uwtools/tests/drivers/test_orog.py
index 2092dc82e..d4a231d56 100644
--- a/src/uwtools/tests/drivers/test_orog.py
+++ b/src/uwtools/tests/drivers/test_orog.py
@@ -12,6 +12,7 @@
from uwtools.drivers.driver import Driver
from uwtools.drivers.orog import Orog
from uwtools.logging import log
+from uwtools.scheduler import Slurm
from uwtools.tests.support import regex_logged
# Fixtures
@@ -76,7 +77,6 @@ def driverobj(config):
"_validate",
"_write_runscript",
"run",
- "runscript",
"taskname",
],
)
@@ -84,6 +84,10 @@ def test_Orog(method):
assert getattr(Orog, method) is getattr(Driver, method)
+def test_Orog_driver_name(driverobj):
+ assert driverobj.driver_name() == Orog.driver_name() == "orog"
+
+
def test_Orog_files_linked(driverobj):
for _, src in driverobj.config["files_to_link"].items():
Path(src).touch()
@@ -98,10 +102,10 @@ def test_Orog_files_linked(driverobj):
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)"
+ status = f"Input grid file {str(grid_file)}: State: Not Ready (external asset)"
if exist:
grid_file.touch()
- status = "Input grid file: State: Ready"
+ status = f"Input grid file {str(grid_file)}: State: Ready"
driverobj.grid_file()
assert regex_logged(caplog, status)
@@ -110,7 +114,7 @@ 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")
+ assert regex_logged(caplog, "Input grid file none: State: Ready")
def test_Orog_input_config_file_new(driverobj):
@@ -123,7 +127,7 @@ def test_Orog_input_config_file_new(driverobj):
content = inps.readlines()
content = [l.strip("\n") for l in content]
assert len(content) == 3
- assert content[0] == driverobj.config["grid_file"]
+ assert content[0] == "'{}'".format(driverobj.config["grid_file"])
assert content[1] == ".false."
assert content[2] == "none"
@@ -137,8 +141,8 @@ def test_Orog_input_config_file_old(driverobj):
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[1] == "'{}'".format(driverobj.config["grid_file"])
+ assert content[2] == "'{}'".format(driverobj.config["orog_file"])
assert content[3] == ".false."
assert content[4] == "none"
@@ -150,8 +154,13 @@ def test_Orog_provisioned_rundir(driverobj):
mocks[m].assert_called_once_with()
-def test_Orog_driver_name(driverobj):
- assert driverobj.driver_name() == Orog.driver_name() == "orog"
+def test_Orog_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_Orog__runcmd(driverobj):
diff --git a/src/uwtools/tests/drivers/test_orog_gsl.py b/src/uwtools/tests/drivers/test_orog_gsl.py
index a942aa687..3fa2fc6ad 100644
--- a/src/uwtools/tests/drivers/test_orog_gsl.py
+++ b/src/uwtools/tests/drivers/test_orog_gsl.py
@@ -72,6 +72,20 @@ def test_OrogGSL(method):
assert getattr(OrogGSL, method) is getattr(Driver, method)
+def test_OrogGSL_driver_name(driverobj):
+ assert driverobj.driver_name() == OrogGSL.driver_name() == "orog_gsl"
+
+
+def test_OrogGSL_input_config_file(driverobj):
+ driverobj.input_config_file()
+ inputs = [str(driverobj.config["config"][k]) for k in ("tile", "resolution", "halo")]
+ with open(driverobj._input_config_path, "r", encoding="utf-8") as cfg_file:
+ content = cfg_file.readlines()
+ content = [l.strip("\n") for l in content]
+ assert len(content) == 3
+ assert content == inputs
+
+
def test_OrogGSL_input_grid_file(driverobj):
path = Path(driverobj.config["rundir"], "C403_grid.tile7.halo4.nc")
assert not path.is_file()
@@ -81,7 +95,12 @@ def test_OrogGSL_input_grid_file(driverobj):
def test_OrogGSL_provisioned_rundir(driverobj):
with patch.multiple(
- driverobj, input_grid_file=D, runscript=D, topo_data_2p5m=D, topo_data_30s=D
+ driverobj,
+ input_config_file=D,
+ input_grid_file=D,
+ runscript=D,
+ topo_data_2p5m=D,
+ topo_data_30s=D,
) as mocks:
driverobj.provisioned_rundir()
for m in mocks:
@@ -102,13 +121,8 @@ def test_OrogGSL_topo_data_3os(driverobj):
assert path.is_symlink()
-def test_OrogGSL_driver_name(driverobj):
- assert driverobj.driver_name() == OrogGSL.driver_name() == "orog_gsl"
-
-
def test_OrogGSL__runcmd(driverobj):
- inputs = [str(driverobj.config["config"][k]) for k in ("tile", "resolution", "halo")]
- assert driverobj._runcmd == "echo '%s' | %s" % (
- "\n".join(inputs),
+ assert driverobj._runcmd == "%s < %s" % (
driverobj.config["execution"]["executable"],
+ driverobj._input_config_path.name,
)
diff --git a/src/uwtools/tests/drivers/test_schism.py b/src/uwtools/tests/drivers/test_schism.py
index 0c5fea845..8800174f1 100644
--- a/src/uwtools/tests/drivers/test_schism.py
+++ b/src/uwtools/tests/drivers/test_schism.py
@@ -51,6 +51,10 @@ def test_SCHISM(method):
assert getattr(SCHISM, method) is getattr(AssetsCycleBased, method)
+def test_SCHISM_driver_name(driverobj):
+ assert driverobj.driver_name() == SCHISM.driver_name() == "schism"
+
+
def test_SCHISM_namelist_file(driverobj):
src = driverobj.config["namelist"]["template_file"]
with open(src, "w", encoding="utf-8") as f:
@@ -69,7 +73,3 @@ def test_SCHISM_provisioned_rundir(driverobj):
driverobj.provisioned_rundir()
for m in mocks:
mocks[m].assert_called_once_with()
-
-
-def test_SCHISM_driver_name(driverobj):
- assert driverobj.driver_name() == SCHISM.driver_name() == "schism"
diff --git a/src/uwtools/tests/drivers/test_sfc_climo_gen.py b/src/uwtools/tests/drivers/test_sfc_climo_gen.py
index a9355f42d..c5c9d8d21 100644
--- a/src/uwtools/tests/drivers/test_sfc_climo_gen.py
+++ b/src/uwtools/tests/drivers/test_sfc_climo_gen.py
@@ -106,6 +106,10 @@ def test_SfcClimoGen(method):
assert getattr(SfcClimoGen, method) is getattr(Driver, method)
+def test_SfcClimoGen_driver_name(driverobj):
+ assert driverobj.driver_name() == SfcClimoGen.driver_name() == "sfc_climo_gen"
+
+
def test_SfcClimoGen_namelist_file(caplog, driverobj):
log.setLevel(logging.DEBUG)
dst = driverobj.rundir / "fort.41"
@@ -136,7 +140,3 @@ def test_SfcClimoGen_provisioned_rundir(driverobj):
driverobj.provisioned_rundir()
for m in mocks:
mocks[m].assert_called_once_with()
-
-
-def test_SfcClimoGen_driver_name(driverobj):
- assert driverobj.driver_name() == SfcClimoGen.driver_name() == "sfc_climo_gen"
diff --git a/src/uwtools/tests/drivers/test_shave.py b/src/uwtools/tests/drivers/test_shave.py
index b6ac9c2a2..951594f32 100644
--- a/src/uwtools/tests/drivers/test_shave.py
+++ b/src/uwtools/tests/drivers/test_shave.py
@@ -2,6 +2,7 @@
"""
Shave driver tests.
"""
+from pathlib import Path
from unittest.mock import DEFAULT as D
from unittest.mock import patch
@@ -27,8 +28,9 @@ def config(tmp_path):
"executable": "/path/to/shave",
},
"config": {
- "input_grid_file": "/path/to/input/grid/file.nc",
- "nh4": 1,
+ "input_grid_file": str(tmp_path / "input_file.nc"),
+ "output_grid_file": "/path/to/input/grid/file.nc",
+ "nhalo": 1,
"nx": 214,
"ny": 128,
},
@@ -70,9 +72,29 @@ def test_Shave(method):
assert getattr(Shave, method) is getattr(Driver, method)
+def test_Shave_driver_name(driverobj):
+ assert driverobj.driver_name() == Shave.driver_name() == "shave"
+
+
+def test_Shave_input_config_file(driverobj):
+ nx = driverobj.config["config"]["nx"]
+ ny = driverobj.config["config"]["ny"]
+ nhalo = driverobj.config["config"]["nhalo"]
+ input_file_path = driverobj._config["config"]["input_grid_file"]
+ Path(input_file_path).touch()
+ output_file_path = driverobj._config["config"]["output_grid_file"]
+ driverobj.input_config_file()
+ with open(driverobj._input_config_path, "r", encoding="utf-8") as cfg_file:
+ content = cfg_file.readlines()
+ content = [l.strip("\n") for l in content]
+ assert len(content) == 1
+ assert content[0] == f"{nx} {ny} {nhalo} '{input_file_path}' '{output_file_path}'"
+
+
def test_Shave_provisioned_rundir(driverobj):
with patch.multiple(
driverobj,
+ input_config_file=D,
runscript=D,
) as mocks:
driverobj.provisioned_rundir()
@@ -80,15 +102,6 @@ def test_Shave_provisioned_rundir(driverobj):
mocks[m].assert_called_once_with()
-def test_Shave_driver_name(driverobj):
- assert driverobj.driver_name() == Shave.driver_name() == "shave"
-
-
def test_Shave__runcmd(driverobj):
cmd = driverobj._runcmd
- nx = driverobj.config["config"]["nx"]
- ny = driverobj.config["config"]["ny"]
- nh4 = driverobj.config["config"]["nh4"]
- input_file_path = driverobj._config["config"]["input_grid_file"]
- output_file_path = input_file_path.replace(".nc", "_NH0.nc")
- assert cmd == f"/path/to/shave {nx} {ny} {nh4} {input_file_path} {output_file_path}"
+ assert cmd == "/path/to/shave < shave.cfg"
diff --git a/src/uwtools/tests/drivers/test_support.py b/src/uwtools/tests/drivers/test_support.py
index d9cf9545b..05886c0ac 100644
--- a/src/uwtools/tests/drivers/test_support.py
+++ b/src/uwtools/tests/drivers/test_support.py
@@ -41,6 +41,10 @@ class Child(Parent):
def test_tasks():
class SomeDriver(DriverTimeInvariant):
+ @classmethod
+ def driver_name(cls):
+ pass
+
def provisioned_rundir(self):
pass
@@ -59,10 +63,6 @@ def t2(self):
def t3(self):
"@tasks t3"
- @classmethod
- def driver_name(cls):
- pass
-
@property
def _resources(self):
pass
diff --git a/src/uwtools/tests/drivers/test_ungrib.py b/src/uwtools/tests/drivers/test_ungrib.py
index be2a15388..9827483ae 100644
--- a/src/uwtools/tests/drivers/test_ungrib.py
+++ b/src/uwtools/tests/drivers/test_ungrib.py
@@ -77,6 +77,10 @@ def test_Ungrib(method):
assert getattr(Ungrib, method) is getattr(Driver, method)
+def test_Ungrib_driver_name(driverobj):
+ assert driverobj.driver_name() == Ungrib.driver_name() == "ungrib"
+
+
def test_Ungrib_gribfiles(driverobj, tmp_path):
links = []
cycle_hr = 12
@@ -115,6 +119,10 @@ def test_Ungrib_provisioned_rundir(driverobj):
mocks[m].assert_called_once_with()
+def test_Ungrib_taskname(driverobj):
+ assert driverobj.taskname("foo") == "20240201 18Z ungrib foo"
+
+
def test_Ungrib_vtable(driverobj):
src = driverobj.rundir / "Vtable.GFS.in"
src.touch()
@@ -125,10 +133,6 @@ def test_Ungrib_vtable(driverobj):
assert dst.is_symlink()
-def test_Ungrib_driver_name(driverobj):
- assert driverobj.driver_name() == Ungrib.driver_name() == "ungrib"
-
-
def test_Ungrib__gribfile(driverobj):
src = driverobj.rundir / "GRIBFILE.AAA.in"
src.touch()
@@ -138,10 +142,6 @@ def test_Ungrib__gribfile(driverobj):
assert dst.is_symlink()
-def test_Ungrib_taskname(driverobj):
- assert driverobj.taskname("foo") == "20240201 18Z ungrib foo"
-
-
def test__ext():
assert ungrib._ext(0) == "AAA"
assert ungrib._ext(26) == "ABA"
diff --git a/src/uwtools/tests/drivers/test_upp.py b/src/uwtools/tests/drivers/test_upp.py
index bd7712c0f..1d869a5c4 100644
--- a/src/uwtools/tests/drivers/test_upp.py
+++ b/src/uwtools/tests/drivers/test_upp.py
@@ -97,6 +97,10 @@ def test_UPP(method):
assert getattr(UPP, method) is getattr(Driver, method)
+def test_UPP_driver_name(driverobj):
+ assert driverobj.driver_name() == UPP.driver_name() == "upp"
+
+
def test_UPP_files_copied(driverobj):
for _, src in driverobj.config["files_to_copy"].items():
Path(src).touch()
@@ -167,8 +171,8 @@ def test_UPP_provisioned_rundir(driverobj):
mocks[m].assert_called_once_with()
-def test_UPP_driver_name(driverobj):
- assert driverobj.driver_name() == UPP.driver_name() == "upp"
+def test_UPP_taskname(driverobj):
+ assert driverobj.taskname("foo") == "20240507 12:00:00 upp foo"
def test_UPP__namelist_path(driverobj):
@@ -177,7 +181,3 @@ def test_UPP__namelist_path(driverobj):
def test_UPP__runcmd(driverobj):
assert driverobj._runcmd == "%s < itag" % driverobj.config["execution"]["executable"]
-
-
-def test_UPP_taskname(driverobj):
- assert driverobj.taskname("foo") == "20240507 12:00:00 upp foo"
diff --git a/src/uwtools/tests/drivers/test_ww3.py b/src/uwtools/tests/drivers/test_ww3.py
index 253080387..04a4aea90 100644
--- a/src/uwtools/tests/drivers/test_ww3.py
+++ b/src/uwtools/tests/drivers/test_ww3.py
@@ -51,6 +51,10 @@ def test_WaveWatchIII(method):
assert getattr(WaveWatchIII, method) is getattr(AssetsCycleBased, method)
+def test_WaveWatchIII_driver_name(driverobj):
+ assert driverobj.driver_name() == WaveWatchIII.driver_name() == "ww3"
+
+
def test_WaveWatchIII_namelist_file(driverobj):
src = driverobj.config["namelist"]["template_file"]
with open(src, "w", encoding="utf-8") as f:
@@ -77,7 +81,3 @@ def test_WaveWatchIII_restart_directory(driverobj):
assert not path.is_dir()
driverobj.restart_directory()
assert path.is_dir()
-
-
-def test_WaveWatchIII_driver_name(driverobj):
- assert driverobj.driver_name() == WaveWatchIII.driver_name() == "ww3"
diff --git a/src/uwtools/tests/test_schemas.py b/src/uwtools/tests/test_schemas.py
index b2d616209..811f24776 100644
--- a/src/uwtools/tests/test_schemas.py
+++ b/src/uwtools/tests/test_schemas.py
@@ -758,6 +758,8 @@ def test_schema_filter_topo():
config = {
"config": {
"input_grid_file": "/path/to/grid/file",
+ "filtered_orog": "/path/to/filtered/orog/file",
+ "input_raw_orog": "/path/to/raw/orog/file",
},
"execution": {
"executable": "/path/to/filter_topo",
@@ -791,7 +793,7 @@ def test_schema_filter_topo():
# Top-level rundir key requires a string value:
assert "is not of type 'string'\n" in errors(with_set(config, None, "rundir"))
# All config keys are requried:
- for key in ["input_grid_file"]:
+ for key in ["filtered_orog", "input_grid_file", "input_raw_orog"]:
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(
@@ -1869,9 +1871,10 @@ def test_schema_shave():
config = {
"config": {
"input_grid_file": "/path/to/input_grid_file",
+ "output_grid_file": "/path/to/output_grid_file",
"nx": 42,
"ny": 42,
- "nh4": 1,
+ "nhalo": 1,
},
"execution": {"executable": "shave"},
"rundir": "/tmp",
@@ -1889,16 +1892,19 @@ def test_schema_shave():
def test_schema_shave_config_properties():
# Get errors function from schema_validator
errors = schema_validator("shave", "properties", "shave", "properties", "config")
- for key in ("input_grid_file", "nx", "ny", "nh4"):
+ for key in ("input_grid_file", "nx", "ny", "nhalo"):
# All config keys are required:
assert f"'{key}' is a required property" in errors({})
# A string value is ok for input_grid_file:
if key == "input_grid_file":
assert "not of type 'string'" in str(errors({key: 42}))
- # nx, ny, and nh4 must be positive integers:
- elif key in ["nx", "ny", "nh4"]:
+ # nx, ny, and nhalo must be integers >= their respective minimum values:
+ elif key in (keyvals := {"nx": 1, "ny": 1, "nhalo": 0}):
+ minval = keyvals[key]
assert "not of type 'integer'" in str(errors({key: "/path/"}))
- assert "0 is less than the minimum of 1" in str(errors({key: 0}))
+ assert f"{minval - 1} is less than the minimum of {minval}" in str(
+ errors({key: minval - 1})
+ )
# It is an error for the value to be a floating-point value:
assert "not of type" in str(errors({key: 3.14}))
# It is an error not to supply a value: