From 4c043331d555c8d3dcbfaf424b50e364c5bd5d03 Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Tue, 7 Oct 2025 10:23:51 -0600 Subject: [PATCH 01/12] Add capability to parse nested suites using a new version of the XML schema --- schema/suite_v2_0.xsd | 159 ++++++++++++++++++++ scripts/ccpp_datafile.py | 5 +- scripts/ccpp_suite.py | 61 +++++--- scripts/parse_tools/__init__.py | 5 +- scripts/parse_tools/xml_tools.py | 239 ++++++++++++++++++++----------- 5 files changed, 360 insertions(+), 109 deletions(-) create mode 100644 schema/suite_v2_0.xsd diff --git a/schema/suite_v2_0.xsd b/schema/suite_v2_0.xsd new file mode 100644 index 00000000..6a20020b --- /dev/null +++ b/schema/suite_v2_0.xsd @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/ccpp_datafile.py b/scripts/ccpp_datafile.py index 2318143e..e02061fc 100755 --- a/scripts/ccpp_datafile.py +++ b/scripts/ccpp_datafile.py @@ -25,7 +25,7 @@ from framework_env import CCPPFrameworkEnv from metadata_table import UNKNOWN_PROCESS_TYPE from metavar import Var -from parse_tools import read_xml_file, PrettyElementTree +from parse_tools import read_xml_file, write_xml_file from parse_tools import ParseContext, ParseSource from suite_objects import VerticalLoop, Subcycle @@ -1182,8 +1182,7 @@ def generate_ccpp_datatable(run_env, host_model, api, scheme_headers, # end for _add_dependencies(datatable, scheme_depends, host_depends) # Write tree - datatable_tree = PrettyElementTree(datatable) - datatable_tree.write(run_env.datatable_file) + write_xml_file(datatable, run_env.datatable_file) ############################################################################### diff --git a/scripts/ccpp_suite.py b/scripts/ccpp_suite.py index 1182fd43..d19cd712 100644 --- a/scripts/ccpp_suite.py +++ b/scripts/ccpp_suite.py @@ -19,7 +19,8 @@ from metavar import Var, VarDictionary, ccpp_standard_var from parse_tools import ParseContext, ParseSource from parse_tools import ParseInternalError, CCPPError -from parse_tools import read_xml_file, validate_xml_file, find_schema_version +from parse_tools import read_xml_file, validate_xml_file, write_xml_file +from parse_tools import find_schema_version, expand_nested_suites from parse_tools import init_log, set_log_to_null from suite_objects import CallList, Group, Scheme from metavar import CCPP_LOOP_VAR_STDNAMES @@ -82,7 +83,7 @@ class Suite(VarDictionary): __scheme_template = '{}' - def __init__(self, filename, api, run_env): + def __init__(self, filename, suite_xml, api, run_env): """Initialize this Suite object from the SDF, . serves as the Suite's parent.""" self.__run_env = run_env @@ -114,7 +115,7 @@ def __init__(self, filename, api, run_env): raise CCPPError(emsg.format(self.__sdf_name)) # end if # Parse the SDF - self.parse(run_env) + self.parse(suite_xml, run_env) @property def name(self): @@ -186,21 +187,11 @@ def new_group_from_name(self, group_name, run_env): group_xml = ''.format(group_name) return self.new_group(group_xml, group_name, run_env) - def parse(self, run_env): + def parse(self, suite_xml, run_env): """Parse the suite definition file.""" success = True - - _, suite_xml = read_xml_file(self.__sdf_name, run_env.logger) # We do not have line number information for the XML file self.__context = ParseContext(filename=self.__sdf_name) - # Validate the XML file - version = find_schema_version(suite_xml) - res = validate_xml_file(self.__sdf_name, 'suite', version, - run_env.logger) - if not res: - emsg = "Invalid suite definition file, '{}'" - raise CCPPError(emsg.format(self.__sdf_name)) - # end if self.__name = suite_xml.get('name') self.__module = 'ccpp_{}_cap'.format(self.name) lmsg = "Reading suite definition file for '{}'" @@ -679,13 +670,47 @@ def __init__(self, sdfs, host_model, scheme_headers, run_env): raise CCPPError(errmsg.format(header.title)) # end if # end for + # Turn the SDF files into Suites for sdf in sdfs: - suite = Suite(sdf, self, run_env) - suite.analyze(self.host_model, scheme_library, - self.__ddt_lib, run_env) - self.__suites.append(suite) + # Load the suite definition file to determine the schema version, + # validate the file, and expand nested suites if applicable + _, xml_root = read_xml_file(sdf, run_env.logger) + # We do not have line number information for the XML file + self.__context = ParseContext(filename=sdf) + # Validate the XML file + schema_version = find_schema_version(xml_root) + res = validate_xml_file(sdf, 'suite', schema_version, run_env.logger) + if not res: + raise CCPPError(f"Invalid suite definition file, '{sdf}'") + + # Processing the sdf depends on the schema version + if xml_root.tag.lower() == "suite" and schema_version[0] == 1: + suite = Suite(sdf, xml_root, self, run_env) + suite.analyze(self.host_model, scheme_library, + self.__ddt_lib, run_env) + self.__suites.append(suite) + elif xml_root.tag.lower() == "suites" and schema_version[0] == 2: + # Preprocess the sdf to expand nested suites + expand_nested_suites(xml_root, logger=run_env.logger) + # Write the expanded sdf to the capgen output directory; + # this file isn't used by capgen (everything is in memory + # from here onwards), but it is useful for developers/users + sdf_expanded = os.path.join(run_env.output_dir, + os.path.split(sdf)[1].replace(".xml", "_expanded.xml")) + write_xml_file(xml_root, sdf_expanded, run_env.logger) + for suite_item in xml_root: + suite = Suite(sdf, suite_item, self, run_env) + suite.analyze(self.host_model, scheme_library, + self.__ddt_lib, run_env) + self.__suites.append(suite) + else: + errmsg = f"Suite XML schema not supported: " + \ + "root={xml_root.tag}, version={schema_version}" + raise CCPPError(errmsg) + # end if # end for + # We will need the correct names for errmsg and errcode evar = self.host_model.find_variable(standard_name='ccpp_error_message') if evar is not None: diff --git a/scripts/parse_tools/__init__.py b/scripts/parse_tools/__init__.py index bfd2cfbf..6590e3f7 100644 --- a/scripts/parse_tools/__init__.py +++ b/scripts/parse_tools/__init__.py @@ -30,7 +30,7 @@ from preprocess import PreprocStack from xml_tools import find_schema_file, find_schema_version from xml_tools import read_xml_file, validate_xml_file -from xml_tools import PrettyElementTree +from xml_tools import expand_nested_suites, write_xml_file from fortran_conditional import FORTRAN_CONDITIONAL_REGEX_WORDS, FORTRAN_CONDITIONAL_REGEX # pylint: enable=wrong-import-position @@ -51,6 +51,7 @@ 'check_valid_values', 'check_molar_mass', 'context_string', + 'expand_nested_suites', 'find_schema_file', 'find_schema_version', 'flush_log', @@ -65,7 +66,6 @@ 'ParseSyntaxError', 'ParseObject', 'PreprocStack', - 'PrettyElementTree', 'read_xml_file', 'register_fortran_ddt_name', 'registered_fortran_ddt_name', @@ -78,6 +78,7 @@ 'type_name', 'unique_standard_name', 'validate_xml_file', + 'write_xml_file', 'FORTRAN_CONDITIONAL_REGEX_WORDS', 'FORTRAN_CONDITIONAL_REGEX' ] diff --git a/scripts/parse_tools/xml_tools.py b/scripts/parse_tools/xml_tools.py index 164b169b..15e93cdb 100644 --- a/scripts/parse_tools/xml_tools.py +++ b/scripts/parse_tools/xml_tools.py @@ -12,6 +12,7 @@ import subprocess import sys import xml.etree.ElementTree as ET +import xml.dom.minidom sys.path.insert(0, os.path.dirname(__file__)) # CCPP framework imports from parse_source import CCPPError @@ -212,6 +213,7 @@ def validate_xml_file(filename, schema_root, version, logger, logger.debug("Checking file {} against schema {}".format(filename, schema_file)) cmd = [_XMLLINT, '--noout', '--schema', schema_file, filename] + logger.debug(f"Executing command '{cmd}'") result = call_command(cmd, logger) return result # end if @@ -247,96 +249,161 @@ def read_xml_file(filename, logger=None): return tree, root ############################################################################### +def load_suite_by_name(suite_name, group_name, main_root, file=None, logger=None): +############################################################################### + """Load a suite by its name, or a group of a suite by the suite + and group names. If the optional file argument is provided, look + for the object in that file, otherwise search the current main_root.""" + if file: + _, root = read_xml_file(file, logger) + schema_version = find_schema_version(root) + if schema_version[0] < 2: + raise CCPPError(f"XML schema version {schema_version} " + \ + f"invalid for nested suite {suite_name}") + res = validate_xml_file(file, 'suite', schema_version, logger) + if not res: + raise CCPPError(f"Invalid suite definition file, '{sdf}'") + else: + root = main_root + for suite in root.findall("suite"): + print("ABC: {suite.attrib.get('name')}") + if suite.attrib.get("name") == suite_name: + if group_name: + for group in suite.findall("group"): + if group.attrib.get("name") == group_name: + return group + else: + return suite + emsg = f"Nested suite {suite_name}" + (f", group {group_name}," if group_name else "") \ + + " not found" + (f" in file {file}" if file else "") + raise CCPPError(emsg) -class PrettyElementTree(ET.ElementTree): - """An ElementTree subclass with nice formatting when writing to a file""" +############################################################################### +def expand_nested_suites(root, logger=None): +############################################################################### + """Iterate over the root element until all nested suites (single, double, + triple, ...) are replaced with the actual content of the nested suite.""" + # Keep track of any nested suites defined under the same root + # that need to be removed at the end of this function. + # This happens all in memory, it does not alter files on disk. + expanded_suites_to_remove = list() + # Iteratively expand nested suites until they are all gone + keep_expanding = True + while keep_expanding: + keep_expanding = False + for suite in root.findall("suite"): + # First, search all groups for nested_suite elements + groups = suite.findall("group") + for group in groups: + nested_suites = group.findall("nested_suite") + for nested in nested_suites: + suite_name = nested.attrib.get("name") + group_name = nested.attrib.get("group") + file = nested.attrib.get("file") + # This check is redundant, because the XML schema ensures + # that nested_suite elements inside a group have a group name + if not group_name: + CCPPError(f"Required attribute group not found for nested suite {suite_name}") + referenced_suite = load_suite_by_name(suite_name, group_name, root, + file=file, logger=logger) + # Deep copy to avoid modifying the original + imported_content = [ET.fromstring(ET.tostring(child)) for child in referenced_suite] + # Swap nested suite with imported content + for item in imported_content: + # If the imported content comes from a separate file and has + # nested suites that are within that separate file, then we + # need to inject the file attribute here. + if item.tag == "nested_suite": + if file and not item.attrib.get("file"): + item.set("file", file) + group.insert(list(group).index(nested), item) + group.remove(nested) + # Need another pass over the root element + keep_expanding = True + # If the nested suite resides in the same file, remove it + if not file: + expanded_suites_to_remove.append(suite_name) + if logger: + msg = f"Expanded nested suite '{suite_name}', group '{group_name}'" + if file: + msg += f", in file '{file}'" + logger.debug(msg) + # Second, search all suites for nested_suite elements + nested_suites = suite.findall("nested_suite") + for nested in nested_suites: + suite_name = nested.attrib.get("name") + group_name = nested.attrib.get("group") + # This check is redundant, because the XML schema ensures + # that nested_suite elements at the suite level have no group name + if group_name: + CCPPError("Nested suite {suite_name} cannot have attribute group") + file = nested.attrib.get("file") + referenced_suite = load_suite_by_name(suite_name, group_name, root, + file=file, logger=logger) + # Deep copy to avoid modifying the original + imported_content = [ET.fromstring(ET.tostring(child)) for child in referenced_suite] + # Swap nested suite with imported content + for item in imported_content: + # If the imported content comes from a separate file and has + # nested suites that are within that separate file, then we + # need to inject the file attribute here. + if item.tag == "nested_suite": + if file and not item.attrib.get("file"): + item.set("file", file) + suite.insert(list(suite).index(nested), item) + suite.remove(nested) + # Need another pass over the root element + keep_expanding = True + # If the nested suite resides in the same file, remove it + if not file: + expanded_suites_to_remove.append(suite_name) + if logger: + msg = f"Expanded nested suite '{suite_name}'" + if file: + msg += f" in file '{file}'" + logger.debug(msg) - def __init__(self, element=None, file=None): - """Initialize a PrettyElementTree object""" - super().__init__(element, file) + # Remove expanded suites + for suite in root.findall("suite"): + suite_name = suite.attrib["name"] + if suite_name in expanded_suites_to_remove: + root.remove(suite) + if logger: + msg = f"Removed nested suite '{suite_name}' from root element" + logger.debug(msg) - def _write(self, outfile, line, indent, eol=os.linesep): - """Write as an ASCII string to """ - outfile.write('{}{}{}'.format(_INDENT_STR*indent, line, eol)) +############################################################################### +def write_xml_file(root, file_path, logger=None): +############################################################################### + """Pretty-prints an ElementTree to an ASCII file using xml.dom.minidom""" - @staticmethod - def _inc_pos(outstr, text, txt_beg): - """Return a position increment based on the length of - or raise an exception if is empty. - and are used to provide some context for the error.""" - if outstr: - return len(outstr) - # end if - txt_end = text[txt_beg].find(">") + txt_beg + 1 - if txt_end <= txt_beg: - txt_end = txt_beg + 256 - # end if - emsg = "No output at {} of {}\n{}".format(txt_beg, len(text), - text[txt_beg:txt_end]) - raise XMLToolsInternalError(emsg) + def remove_whitespace_nodes(node): + """Helper function to recursively remove all text nodes that contain + only whitespace, which eliminates blank lines in the output.""" + for child in list(node.childNodes): + if child.nodeType == child.TEXT_NODE and not child.data.strip(): + node.removeChild(child) + elif child.hasChildNodes(): + remove_whitespace_nodes(child) - def write(self, file, encoding="us-ascii", xml_declaration=None, - default_namespace=None, method="xml", - short_empty_elements=True): - """Subclassed write method to format output.""" - et_str = ET.tostring(self.getroot(), - encoding=encoding, method=method, - xml_declaration=xml_declaration, - default_namespace=default_namespace, - short_empty_elements=short_empty_elements) - # end if - fmode = 'wt' - root = str(et_str, encoding="utf-8") - indent = 0 - last_write_text = False - with open(file, fmode) as outfile: - inline = root.strip() - istart = 0 # Current start pos - iend = len(inline) - while istart < iend: - bmatch = beg_tag_re.match(inline[istart:]) - ematch = end_tag_re.match(inline[istart:]) - smatch = simple_tag_re.match(inline[istart:]) - if bmatch is not None: - outstr = bmatch.group(1) - if inline[istart + len(bmatch.group(1))] != '<': - # Print text on same line - self._write(outfile, outstr, indent, eol='') - else: - self._write(outfile, outstr, indent) - # end if - indent += 1 - istart += self._inc_pos(outstr, inline, istart) - last_write_text = False - elif ematch is not None: - outstr = ematch.group(1) - indent -= 1 - if last_write_text: - self._write(outfile, outstr, 0) - last_write_text = False - else: - self._write(outfile, outstr, indent) - # end if - istart += self._inc_pos(outstr, inline, istart) - elif smatch is not None: - outstr = smatch.group(1) - self._write(outfile, outstr, indent) - istart += self._inc_pos(outstr, inline, istart) - last_write_text = False - else: - # No tag, just output text - end_index = inline[istart:].find('<') - if end_index < 0: - end_index = iend - else: - end_index += istart - # end if - outstr = inline[istart:end_index] - self._write(outfile, outstr.strip(), 0, eol='') - last_write_text = True - istart += self._inc_pos(outstr, inline, istart) - # end if - # end while - # end with + # Convert ElementTree to a byte string + byte_string = ET.tostring(root, 'us-ascii') + + # Parse string using minidom for pretty printing + reparsed = xml.dom.minidom.parseString(byte_string) + + # Clean whitespace-only text nodes + remove_whitespace_nodes(reparsed) + + # Generate pretty-printed XML string + pretty_xml = reparsed.toprettyxml(indent=" ") + + # Write to file + with open(file_path, 'w', errors='xmlcharrefreplace') as f: + f.write(pretty_xml) + + # Tell everyone! + if logger: + logger.debug(f"Wrote {root} to {file_path}") ############################################################################## From bb5d19ef992cdcf4f3d629928bae7a5133e2fc0f Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Tue, 7 Oct 2025 10:24:19 -0600 Subject: [PATCH 02/12] Add nested_suite_test --- CMakeLists.txt | 2 + test/CMakeLists.txt | 3 + test/nested_suite_test/CMakeLists.txt | 50 + test/nested_suite_test/README.md | 18 + test/nested_suite_test/ccpp_kinds.F90 | 27 + .../nested_suite_test/ccpp_main_suite_cap.F90 | 1060 +++++++++++++++++ test/nested_suite_test/effr_calc.F90 | 84 ++ test/nested_suite_test/effr_calc.meta | 163 +++ test/nested_suite_test/effr_diag.F90 | 68 ++ test/nested_suite_test/effr_diag.meta | 65 + test/nested_suite_test/effr_post.F90 | 61 + test/nested_suite_test/effr_post.meta | 65 + test/nested_suite_test/effr_pre.F90 | 60 + test/nested_suite_test/effr_pre.meta | 66 + test/nested_suite_test/effrs_calc.F90 | 32 + test/nested_suite_test/effrs_calc.meta | 25 + test/nested_suite_test/main_suite.xml | 30 + test/nested_suite_test/module_rad_ddt.F90 | 23 + test/nested_suite_test/module_rad_ddt.meta | 40 + .../nested_suite_test_reports.py | 116 ++ test/nested_suite_test/rad_lw.F90 | 35 + test/nested_suite_test/rad_lw.meta | 35 + test/nested_suite_test/rad_sw.F90 | 35 + test/nested_suite_test/rad_sw.meta | 41 + test/nested_suite_test/radiation2_suite.xml | 17 + .../nested_suite_test/radiation3_subsuite.xml | 9 + test/nested_suite_test/test_host.F90 | 266 +++++ test/nested_suite_test/test_host.meta | 38 + test/nested_suite_test/test_host_data.F90 | 102 ++ test/nested_suite_test/test_host_data.meta | 128 ++ test/nested_suite_test/test_host_mod.F90 | 126 ++ test/nested_suite_test/test_host_mod.meta | 42 + .../test_nested_suite_integration.F90 | 88 ++ 33 files changed, 3020 insertions(+) create mode 100644 test/nested_suite_test/CMakeLists.txt create mode 100644 test/nested_suite_test/README.md create mode 100644 test/nested_suite_test/ccpp_kinds.F90 create mode 100644 test/nested_suite_test/ccpp_main_suite_cap.F90 create mode 100644 test/nested_suite_test/effr_calc.F90 create mode 100644 test/nested_suite_test/effr_calc.meta create mode 100644 test/nested_suite_test/effr_diag.F90 create mode 100644 test/nested_suite_test/effr_diag.meta create mode 100644 test/nested_suite_test/effr_post.F90 create mode 100644 test/nested_suite_test/effr_post.meta create mode 100644 test/nested_suite_test/effr_pre.F90 create mode 100644 test/nested_suite_test/effr_pre.meta create mode 100644 test/nested_suite_test/effrs_calc.F90 create mode 100644 test/nested_suite_test/effrs_calc.meta create mode 100644 test/nested_suite_test/main_suite.xml create mode 100644 test/nested_suite_test/module_rad_ddt.F90 create mode 100644 test/nested_suite_test/module_rad_ddt.meta create mode 100755 test/nested_suite_test/nested_suite_test_reports.py create mode 100644 test/nested_suite_test/rad_lw.F90 create mode 100644 test/nested_suite_test/rad_lw.meta create mode 100644 test/nested_suite_test/rad_sw.F90 create mode 100644 test/nested_suite_test/rad_sw.meta create mode 100644 test/nested_suite_test/radiation2_suite.xml create mode 100644 test/nested_suite_test/radiation3_subsuite.xml create mode 100644 test/nested_suite_test/test_host.F90 create mode 100644 test/nested_suite_test/test_host.meta create mode 100644 test/nested_suite_test/test_host_data.F90 create mode 100644 test/nested_suite_test/test_host_data.meta create mode 100644 test/nested_suite_test/test_host_mod.F90 create mode 100644 test/nested_suite_test/test_host_mod.meta create mode 100644 test/nested_suite_test/test_nested_suite_integration.F90 diff --git a/CMakeLists.txt b/CMakeLists.txt index e0316793..c0aa6cee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,7 @@ option(CCPP_RUN_ADVECTION_TEST "Enable advection regression test" OFF) option(CCPP_RUN_CAPGEN_TEST "Enable capgen regression test" OFF) option(CCPP_RUN_DDT_HOST_TEST "Enable ddt host regression test" OFF) option(CCPP_RUN_VAR_COMPATIBILITY_TEST "Enable variable compatibility regression test" OFF) +option(CCPP_RUN_NESTED_SUITE_TEST "Enable nested suite regression test" OFF) message("") message("OPENMP .............................. ${OPENMP}") @@ -31,6 +32,7 @@ message("CCPP_RUN_ADVECTION_TEST ............. ${CCPP_RUN_ADVECTION_TEST}") message("CCPP_RUN_CAPGEN_TEST ................ ${CCPP_RUN_CAPGEN_TEST}") message("CCPP_RUN_DDT_HOST_TEST .............. ${CCPP_RUN_DDT_HOST_TEST}") message("CCPP_RUN_VAR_COMPATIBILITY_TEST ..... ${CCPP_RUN_VAR_COMPATIBILITY_TEST}") +message("CCPP_RUN_NESTED_SUITE_TEST .......... ${CCPP_RUN_NESTED_SUITE_TEST}") message("") set(CCPP_VERBOSITY "0" CACHE STRING "Verbosity level of output (default: 0)") diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 5666599d..342ddad8 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -12,3 +12,6 @@ endif() if(CCPP_FRAMEWORK_ENABLE_TESTS OR CCPP_RUN_VAR_COMPATIBILITY_TEST) add_subdirectory(var_compatibility_test) endif() +if(CCPP_FRAMEWORK_ENABLE_TESTS OR CCPP_RUN_NESTED_SUITE_TEST) + add_subdirectory(nested_suite_test) +endif() diff --git a/test/nested_suite_test/CMakeLists.txt b/test/nested_suite_test/CMakeLists.txt new file mode 100644 index 00000000..491a8fb4 --- /dev/null +++ b/test/nested_suite_test/CMakeLists.txt @@ -0,0 +1,50 @@ + +#------------------------------------------------------------------------------ +# +# Create list of SCHEME_FILES, HOST_FILES, and SUITE_FILES +# Paths should be relative to CMAKE_SOURCE_DIR (this file's directory) +# +#------------------------------------------------------------------------------ +set(SCHEME_FILES "effr_calc" "effrs_calc" "effr_diag" "effr_pre" "effr_post" "rad_lw" "rad_sw") +set(HOST_FILES "module_rad_ddt" "test_host_data" "test_host_mod") +set(SUITE_FILES "main_suite.xml") +# HOST is the name of the executable we will build. +# We assume there are files ${HOST}.meta and ${HOST}.F90 in CMAKE_SOURCE_DIR +set(HOST "test_host") + +# By default, generated caps go in ccpp subdir +set(CCPP_CAP_FILES "${CMAKE_CURRENT_BINARY_DIR}/ccpp") + +# Create lists for Fortran and meta data files from file names +list(TRANSFORM SCHEME_FILES APPEND ".F90" OUTPUT_VARIABLE SCHEME_FORTRAN_FILES) +list(TRANSFORM SCHEME_FILES APPEND ".meta" OUTPUT_VARIABLE SCHEME_META_FILES) +list(TRANSFORM HOST_FILES APPEND ".F90" OUTPUT_VARIABLE NESTED_SUITE_HOST_FORTRAN_FILES) +list(TRANSFORM HOST_FILES APPEND ".meta" OUTPUT_VARIABLE NESTED_SUITE_HOST_METADATA_FILES) + +list(APPEND NESTED_SUITE_HOST_METADATA_FILES "${HOST}.meta") + +# Run ccpp_capgen +ccpp_capgen(CAPGEN_DEBUG ON + VERBOSITY ${CCPP_VERBOSITY} + HOSTFILES ${NESTED_SUITE_HOST_METADATA_FILES} + SCHEMEFILES ${SCHEME_META_FILES} + SUITES ${SUITE_FILES} + HOST_NAME ${HOST} + OUTPUT_ROOT "${CCPP_CAP_FILES}") + +# Retrieve the list of Fortran files required for test host from datatable.xml and set to CCPP_CAPS_LIST +ccpp_datafile(DATATABLE "${CCPP_CAP_FILES}/datatable.xml" + REPORT_NAME "--ccpp-files") + +# Create test host library +add_library(NESTED_SUITE_TESTLIB OBJECT ${SCHEME_FORTRAN_FILES} + ${NESTED_SUITE_HOST_FORTRAN_FILES} + ${CCPP_CAPS_LIST}) + +# Setup test executable with needed dependencies +add_executable(nested_suite_host_integration test_nested_suite_integration.F90 ${HOST}.F90) +target_link_libraries(nested_suite_host_integration PRIVATE NESTED_SUITE_TESTLIB test_utils) +target_include_directories(nested_suite_host_integration PRIVATE "$") + +# Add executable to be called with ctest +add_test(NAME ctest_nested_suite_host_integration COMMAND nested_suite_host_integration) diff --git a/test/nested_suite_test/README.md b/test/nested_suite_test/README.md new file mode 100644 index 00000000..0c2033db --- /dev/null +++ b/test/nested_suite_test/README.md @@ -0,0 +1,18 @@ +# Nested Suite Test + +Tests the capability to process nested suites: +- Inherited from the variable compatibility test as of 2025/10/01 + - Perform same tests as variable compatibility test at that date +- Parse new XML schema 2.0 +- Expand nested suites at the group level and inside groups + +## Building/Running + +To explicitly build/run the nested suite test host, run: + +```bash +$ cmake -S -B -DCCPP_RUN_NESTED_SUITE_TEST=ON +$ cd +$ make +$ ctest +``` diff --git a/test/nested_suite_test/ccpp_kinds.F90 b/test/nested_suite_test/ccpp_kinds.F90 new file mode 100644 index 00000000..b2923935 --- /dev/null +++ b/test/nested_suite_test/ccpp_kinds.F90 @@ -0,0 +1,27 @@ +! +! This work (Common Community Physics Package Framework), identified by +! NOAA, NCAR, CU/CIRES, is free of known copyright restrictions and is +! placed in the public domain. +! +! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +! THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +! IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +! CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +!> +!! @brief Auto-generated kinds for CCPP +!! +! +module ccpp_kinds + + use ISO_FORTRAN_ENV, only: kind_phys => REAL64 + + implicit none + private + + public :: kind_phys + +end module ccpp_kinds diff --git a/test/nested_suite_test/ccpp_main_suite_cap.F90 b/test/nested_suite_test/ccpp_main_suite_cap.F90 new file mode 100644 index 00000000..4d1c1d00 --- /dev/null +++ b/test/nested_suite_test/ccpp_main_suite_cap.F90 @@ -0,0 +1,1060 @@ +! +! This work (Common Community Physics Package Framework), identified by +! NOAA, NCAR, CU/CIRES, is free of known copyright restrictions and is +! placed in the public domain. +! +! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +! THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +! IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +! CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +!> +!! @brief Auto-generated CCPP Suite Cap for main_suite +!! +! +module ccpp_main_suite_cap + + use ccpp_kinds + use ccpp_constituent_prop_mod, only: ccpp_constituent_properties_t + + implicit none + private + + ! Suite interfaces + + character(len=16) :: ccpp_suite_state = 'uninitialized' + + public :: main_suite_register + public :: main_suite_initialize + public :: main_suite_timestep_initial + public :: main_suite_radiation1 + public :: main_suite_rad_lw_group + public :: main_suite_rad_sw_group + public :: main_suite_timestep_final + public :: main_suite_finalize + ! Public interfaces for handling constituents + ! Return the number of constituents for this suite + public :: main_suite_constituents_num_consts + ! Return the name of a constituent + public :: main_suite_constituents_const_name + ! Copy the data for a constituent + public :: main_suite_constituents_copy_const + ! Private constituent module data + logical, private :: ccpp_constituents_initialized = .false. + ! Private interface for constituents + private :: ccpp_create_constituent_array + + ! Private suite variables + +CONTAINS + + subroutine main_suite_register(errflg, errmsg) + + + ! Dummy arguments + integer, intent(out) :: errflg + character(len=512), intent(out) :: errmsg + + ! Initialize ccpp error handling + errflg = 0 + errmsg = '' + + ! Output threaded region check +#ifdef _OPENMP + if (omp_get_thread_num() > 1) then + errflg = 1 + errmsg = "Cannot call register routine from a threaded region" + return + end if +#endif + ! Check state machine + if (trim(ccpp_suite_state) /= 'uninitialized') then + errflg = 1 + write(errmsg, '(3a)') "Invalid initial CCPP state, '", trim(ccpp_suite_state), & + "' in main_suite_register" + return + end if + ! Set horizontal loop extent + + ! Allocate local arrays + ! Suite state does not change + + end subroutine main_suite_register + + ! ======================================================================== + + + subroutine main_suite_initialize(errflg, errmsg, scheme_order) + + use effr_calc, only: effr_calc_init + use effr_diag, only: effr_diag_init + use effr_post, only: effr_post_init + use mod_effr_pre, only: effr_pre_init + + ! Dummy arguments + integer, intent(out) :: errflg + character(len=512), intent(out) :: errmsg + integer, intent(inout) :: scheme_order + + ! Local Variables + type(integer) :: internal_var_integer + + ! Initialize ccpp error handling + errflg = 0 + errmsg = '' + + ! Output threaded region check +#ifdef _OPENMP + if (omp_get_thread_num() > 1) then + errflg = 1 + errmsg = "Cannot call initialize routine from a threaded region" + return + end if +#endif + ! Check state machine + if (trim(ccpp_suite_state) /= 'uninitialized') then + errflg = 1 + write(errmsg, '(3a)') "Invalid initial CCPP state, '", trim(ccpp_suite_state), & + "' in main_suite_initialize" + return + end if + ! Set horizontal loop extent + + ! Allocate local arrays + + ! Allocate suite_vars + + if (errflg == 0) then + ! ################################################################## + ! Begin debug tests + ! ################################################################## + + ! Assign value of scheme_order to internal_var_integer + internal_var_integer = scheme_order + + ! ################################################################## + ! End debug tests + ! ################################################################## + + + + ! Call scheme + call effr_pre_init(scheme_order=scheme_order, errmsg=errmsg, errflg=errflg) + + + + end if + + if (errflg == 0) then + ! ################################################################## + ! Begin debug tests + ! ################################################################## + + ! Assign value of scheme_order to internal_var_integer + internal_var_integer = scheme_order + + ! ################################################################## + ! End debug tests + ! ################################################################## + + + + ! Call scheme + call effr_calc_init(scheme_order=scheme_order, errmsg=errmsg, errflg=errflg) + + + + end if + + if (errflg == 0) then + ! ################################################################## + ! Begin debug tests + ! ################################################################## + + ! Assign value of scheme_order to internal_var_integer + internal_var_integer = scheme_order + + ! ################################################################## + ! End debug tests + ! ################################################################## + + + + ! Call scheme + call effr_post_init(scheme_order=scheme_order, errmsg=errmsg, errflg=errflg) + + + + end if + + if (errflg == 0) then + ! ################################################################## + ! Begin debug tests + ! ################################################################## + + ! Assign value of scheme_order to internal_var_integer + internal_var_integer = scheme_order + + ! ################################################################## + ! End debug tests + ! ################################################################## + + + + ! Call scheme + call effr_diag_init(scheme_order=scheme_order, errmsg=errmsg, errflg=errflg) + + + + end if + ccpp_suite_state = 'initialized' + + end subroutine main_suite_initialize + + ! ======================================================================== + + + subroutine main_suite_timestep_initial(errflg, errmsg) + + + ! Dummy arguments + integer, intent(out) :: errflg + character(len=512), intent(out) :: errmsg + + ! Initialize ccpp error handling + errflg = 0 + errmsg = '' + + ! Output threaded region check +#ifdef _OPENMP + if (omp_get_thread_num() > 1) then + errflg = 1 + errmsg = "Cannot call timestep_initial routine from a threaded region" + return + end if +#endif + ! Check state machine + if (trim(ccpp_suite_state) /= 'initialized') then + errflg = 1 + write(errmsg, '(3a)') "Invalid initial CCPP state, '", trim(ccpp_suite_state), & + "' in main_suite_timestep_initial" + return + end if + ! Set horizontal loop extent + + ! Allocate local arrays + + ! Allocate suite_vars + ccpp_suite_state = 'in_time_step' + + end subroutine main_suite_timestep_initial + + ! ======================================================================== + + + subroutine main_suite_radiation1(num_subcycles, errflg, errmsg, ncols, pver, effrr_inout, & + scalar_var, has_graupel, effrg_in, ncg_in, has_ice, nci_out, effrl_inout, effri_out, & + effrs_inout, col_start, col_end, scalar_var1, tke_inout, tke2_inout, scalar_var2, & + scalar_var3) + + use effr_calc, only: effr_calc_run + use effr_diag, only: effr_diag_run + use effr_post, only: effr_post_run + use effrs_calc, only: effrs_calc_run + use mod_effr_pre, only: effr_pre_run + + ! Dummy arguments + integer, intent(in) :: num_subcycles + integer, intent(out) :: errflg + character(len=512), intent(out) :: errmsg + integer, intent(in) :: ncols + integer, intent(in) :: pver + real(kind_phys), intent(inout) :: effrr_inout(:,:) + real(kind_phys), intent(in) :: scalar_var + logical, intent(in) :: has_graupel + real(kind_phys), intent(in), target, optional :: effrg_in(:,:) + real(kind_phys), intent(in), target, optional :: ncg_in(:,:) + logical, intent(in) :: has_ice + real(kind_phys), intent(out), target, optional :: nci_out(:,:) + real(kind_phys), intent(inout) :: effrl_inout(:,:) + real(kind_phys), intent(out), target, optional :: effri_out(:,:) + real(8), intent(inout) :: effrs_inout(:,:) + integer, intent(in) :: col_start + integer, intent(in) :: col_end + real(kind_phys), intent(inout) :: scalar_var1 + real(kind_phys), intent(inout) :: tke_inout + real(kind_phys), intent(inout) :: tke2_inout + real(kind_phys), intent(in) :: scalar_var2 + integer, intent(in) :: scalar_var3 + + ! Local Variables + integer :: loop0_num_subcycles_for_effr + type(real) :: internal_var_real + integer :: loop1 + integer :: loop2 + integer :: ncol + integer :: internal_var_integer + real(kind_phys) :: internal_var_real_kind_phys + logical :: internal_var_logical + real(kind_phys) :: scalar_var_local + real(kind_phys), allocatable :: effrr_in_local(:,:) + real(kind_phys), allocatable, target :: effrg_in_local(:,:) + real(kind_phys), allocatable :: effrl_inout_local(:,:) + real(kind_phys), allocatable, target :: effri_out_local(:,:) + real(8), allocatable :: effrs_inout_local(:,:) + real(kind_phys), pointer :: effrg_in_ptr(:,:) => null() + real(kind_phys), pointer :: ncg_in_ptr(:,:) => null() + real(kind_phys), pointer :: nci_out_ptr(:,:) => null() + real(kind_phys), pointer :: effri_out_ptr(:,:) => null() + real(kind_phys), pointer :: ncl_out_ptr(:,:) => null() + + ! Initialize ccpp error handling + errflg = 0 + errmsg = '' + + ! Check state machine + if (trim(ccpp_suite_state) /= 'in_time_step') then + errflg = 1 + write(errmsg, '(3a)') "Invalid initial CCPP state, '", trim(ccpp_suite_state), & + "' in main_suite_radiation1" + return + end if + ! Set horizontal loop extent + ncol = col_end - col_start + 1 + + ! Allocate local arrays + allocate(effrg_in_local(1:ncol, 1:pver)) + allocate(effri_out_local(1:ncol, 1:pver)) + allocate(effrl_inout_local(1:ncol, 1:pver)) + allocate(effrr_in_local(1:ncol, 1:pver)) + allocate(effrs_inout_local(1:ncol, 1:pver)) + do loop0_num_subcycles_for_effr = 1, num_subcycles + + if (errflg == 0) then + ! ################################################################## + ! Begin debug tests + ! ################################################################## + + ! Check size of array effrr_inout + if (size(effrr_inout(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_pre_run: for array effrr_inout, expected size ',& + 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effrr_inout) + errflg = 1 + return + end if + + ! Check length of effrr_inout(:,1) + if (size(effrr_inout(:,1)) /= ncol-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_pre_run: for array effrr_inout(:,1), expected size ', ncol-1+1,& + ' but got ', size(effrr_inout(:,1)) + errflg = 1 + return + end if + ! Check length of effrr_inout(1,:) + if (size(effrr_inout(1,:)) /= pver-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_pre_run: for array effrr_inout(1,:), expected size ', pver-1+1,& + ' but got ', size(effrr_inout(1,:)) + errflg = 1 + return + end if + + ! Assign value of scalar_var to internal_var_real + internal_var_real = scalar_var + + ! ################################################################## + ! End debug tests + ! ################################################################## + + + + ! Call scheme + call effr_pre_run(effrr_inout=effrr_inout, scalar_var=scalar_var, errmsg=errmsg, & + errflg=errflg) + + + + end if + do loop1 = 1, 2 + do loop2 = 1, 2 + + if (errflg == 0) then + ! ################################################################## + ! Begin debug tests + ! ################################################################## + + ! Assign value of pver to internal_var_integer + internal_var_integer = pver + + ! Check size of array effrr_inout + if (size(effrr_inout(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_calc_run: for array effrr_inout, expected size ',& + 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effrr_inout) + errflg = 1 + return + end if + + ! Check length of effrr_inout(:,1) + if (size(effrr_inout(:,1)) /= ncol-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_calc_run: for array effrr_inout(:,1), expected size ',& + ncol-1+1, ' but got ', size(effrr_inout(:,1)) + errflg = 1 + return + end if + ! Check length of effrr_inout(1,:) + if (size(effrr_inout(1,:)) /= pver-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_calc_run: for array effrr_inout(1,:), expected size ',& + pver-1+1, ' but got ', size(effrr_inout(1,:)) + errflg = 1 + return + end if + + if (has_graupel) then + ! Check size of array effrg_in + if (size(effrg_in(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_calc_run: for array effrg_in, expected size ',& + 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effrg_in) + errflg = 1 + return + end if + end if + + if (has_graupel) then + ! Check length of effrg_in(:,1) + if (size(effrg_in(:,1)) /= ncol-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_calc_run: for array effrg_in(:,1), expected size ',& + ncol-1+1, ' but got ', size(effrg_in(:,1)) + errflg = 1 + return + end if + ! Check length of effrg_in(1,:) + if (size(effrg_in(1,:)) /= pver-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_calc_run: for array effrg_in(1,:), expected size ',& + pver-1+1, ' but got ', size(effrg_in(1,:)) + errflg = 1 + return + end if + end if + + if (has_graupel) then + ! Check size of array ncg_in + if (size(ncg_in(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_calc_run: for array ncg_in, expected size ',& + 1*(ncol-1+1)*(pver-1+1), ' but got ', size(ncg_in) + errflg = 1 + return + end if + end if + + if (has_graupel) then + ! Check length of ncg_in(:,1) + if (size(ncg_in(:,1)) /= ncol-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_calc_run: for array ncg_in(:,1), expected size ',& + ncol-1+1, ' but got ', size(ncg_in(:,1)) + errflg = 1 + return + end if + ! Check length of ncg_in(1,:) + if (size(ncg_in(1,:)) /= pver-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_calc_run: for array ncg_in(1,:), expected size ',& + pver-1+1, ' but got ', size(ncg_in(1,:)) + errflg = 1 + return + end if + end if + + if (has_ice) then + ! Check size of array nci_out + if (size(nci_out(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_calc_run: for array nci_out, expected size ',& + 1*(ncol-1+1)*(pver-1+1), ' but got ', size(nci_out) + errflg = 1 + return + end if + end if + + ! Check size of array effrl_inout + if (size(effrl_inout(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_calc_run: for array effrl_inout, expected size ',& + 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effrl_inout) + errflg = 1 + return + end if + + ! Check length of effrl_inout(:,1) + if (size(effrl_inout(:,1)) /= ncol-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_calc_run: for array effrl_inout(:,1), expected size ',& + ncol-1+1, ' but got ', size(effrl_inout(:,1)) + errflg = 1 + return + end if + ! Check length of effrl_inout(1,:) + if (size(effrl_inout(1,:)) /= pver-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_calc_run: for array effrl_inout(1,:), expected size ',& + pver-1+1, ' but got ', size(effrl_inout(1,:)) + errflg = 1 + return + end if + + if (has_ice) then + ! Check size of array effri_out + if (size(effri_out(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_calc_run: for array effri_out, expected size ',& + 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effri_out) + errflg = 1 + return + end if + end if + + ! Check size of array effrs_inout + if (size(effrs_inout(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_calc_run: for array effrs_inout, expected size ',& + 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effrs_inout) + errflg = 1 + return + end if + + ! Check length of effrs_inout(:,1) + if (size(effrs_inout(:,1)) /= ncol-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_calc_run: for array effrs_inout(:,1), expected size ',& + ncol-1+1, ' but got ', size(effrs_inout(:,1)) + errflg = 1 + return + end if + ! Check length of effrs_inout(1,:) + if (size(effrs_inout(1,:)) /= pver-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_calc_run: for array effrs_inout(1,:), expected size ',& + pver-1+1, ' but got ', size(effrs_inout(1,:)) + errflg = 1 + return + end if + + ! Assign value of has_graupel to internal_var_logical + internal_var_logical = has_graupel + + ! Assign value of scalar_var1 to internal_var_real + internal_var_real = scalar_var1 + + ! Assign value of tke_inout to internal_var_real + internal_var_real = tke_inout + + ! Assign value of tke2_inout to internal_var_real + internal_var_real = tke2_inout + + ! ################################################################## + ! End debug tests + ! ################################################################## + + ! Compute reverse (pre-scheme) transforms + effrr_in_local(:,1:pver) = 1.0E+6_kind_phys*effrr_inout(:,pver:1:-1) + effrg_in_local(:,1:pver) = 1.0E+6_kind_phys*effrg_in(:,1:pver) + effrl_inout_local(:,1:pver) = 1.0E+6_kind_phys*effrl_inout(:,1:pver) + effrs_inout_local(:,1:pver) = 1.0E+6_8*real(effrs_inout(:,pver:1:-1), 8) + scalar_var_local = 1.0E-3_kind_phys*scalar_var1 + + ! Associate conditional variables + if (has_graupel) then + effrg_in_ptr => effrg_in_local + end if + if (has_graupel) then + ncg_in_ptr => ncg_in + end if + if (has_ice) then + nci_out_ptr => nci_out + end if + if (has_ice) then + effri_out_ptr => effri_out_local + end if + + ! Call scheme + call effr_calc_run(ncol=ncol, nlev=pver, effrr_in=effrr_in_local, & + effrg_in=effrg_in_local, ncg_in=ncg_in_ptr, nci_out=nci_out_ptr, & + effrl_inout=effrl_inout_local, effri_out=effri_out_ptr, & + effrs_inout=effrs_inout_local, ncl_out=ncl_out_ptr, & + has_graupel=has_graupel, scalar_var=scalar_var_local, & + tke_inout=tke_inout, tke2_inout=tke2_inout, errmsg=errmsg, errflg=errflg) + + ! Copy any local pointers to dummy/local variables + if (has_ice) then + nci_out = nci_out_ptr + end if + if (has_ice) then + effri_out_local = effri_out_ptr + end if + + ! Compute forward (post-scheme) transforms + effrl_inout(:,1:pver) = 1.0E-6_kind_phys*effrl_inout_local(:,1:pver) + effri_out(:,1:pver) = 1.0E-6_kind_phys*effri_out_local(:,1:pver) + effrs_inout(:,pver:1:-1) = 1.0E-6_kind_phys*real(effrs_inout_local(:,1:pver), & + kind_phys) + scalar_var1 = 1.0E+3_kind_phys*scalar_var_local + + end if + end do + end do + + if (errflg == 0) then + ! ################################################################## + ! Begin debug tests + ! ################################################################## + + ! Check size of array effrr_inout + if (size(effrr_inout(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_post_run: for array effrr_inout, expected size ',& + 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effrr_inout) + errflg = 1 + return + end if + + ! Check length of effrr_inout(:,1) + if (size(effrr_inout(:,1)) /= ncol-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_post_run: for array effrr_inout(:,1), expected size ', ncol-1+1,& + ' but got ', size(effrr_inout(:,1)) + errflg = 1 + return + end if + ! Check length of effrr_inout(1,:) + if (size(effrr_inout(1,:)) /= pver-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_post_run: for array effrr_inout(1,:), expected size ', pver-1+1,& + ' but got ', size(effrr_inout(1,:)) + errflg = 1 + return + end if + + ! Assign value of scalar_var2 to internal_var_real + internal_var_real = scalar_var2 + + ! ################################################################## + ! End debug tests + ! ################################################################## + + + + ! Call scheme + call effr_post_run(effrr_inout=effrr_inout, scalar_var=scalar_var2, errmsg=errmsg, & + errflg=errflg) + + + + end if + end do + do loop0_num_subcycles_for_effr = 1, num_subcycles + + if (errflg == 0) then + ! ################################################################## + ! Begin debug tests + ! ################################################################## + + ! Check size of array effrs_inout + if (size(effrs_inout(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effrs_calc_run: for array effrs_inout, expected size ',& + 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effrs_inout) + errflg = 1 + return + end if + + ! Check length of effrs_inout(:,1) + if (size(effrs_inout(:,1)) /= ncol-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effrs_calc_run: for array effrs_inout(:,1), expected size ', ncol-1+1,& + ' but got ', size(effrs_inout(:,1)) + errflg = 1 + return + end if + ! Check length of effrs_inout(1,:) + if (size(effrs_inout(1,:)) /= pver-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effrs_calc_run: for array effrs_inout(1,:), expected size ', pver-1+1,& + ' but got ', size(effrs_inout(1,:)) + errflg = 1 + return + end if + + ! ################################################################## + ! End debug tests + ! ################################################################## + + + + ! Call scheme + call effrs_calc_run(effrs_inout=effrs_inout, errmsg=errmsg, errflg=errflg) + + + + end if + end do + + if (errflg == 0) then + ! ################################################################## + ! Begin debug tests + ! ################################################################## + + ! Check size of array effrr_inout + if (size(effrr_inout(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_diag_run: for array effrr_inout, expected size ',& + 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effrr_inout) + errflg = 1 + return + end if + + ! Check length of effrr_inout(:,1) + if (size(effrr_inout(:,1)) /= ncol-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_diag_run: for array effrr_inout(:,1), expected size ', ncol-1+1,& + ' but got ', size(effrr_inout(:,1)) + errflg = 1 + return + end if + ! Check length of effrr_inout(1,:) + if (size(effrr_inout(1,:)) /= pver-1+1) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_radiation1 before effr_diag_run: for array effrr_inout(1,:), expected size ', pver-1+1,& + ' but got ', size(effrr_inout(1,:)) + errflg = 1 + return + end if + + ! Assign value of scalar_var3 to internal_var_integer + internal_var_integer = scalar_var3 + + ! ################################################################## + ! End debug tests + ! ################################################################## + + ! Compute reverse (pre-scheme) transforms + effrr_in_local(:,1:pver) = 1.0E+6_kind_phys*effrr_inout(:,pver:1:-1) + + + ! Call scheme + call effr_diag_run(effrr_in=effrr_in_local, scalar_var=scalar_var3, errmsg=errmsg, & + errflg=errflg) + + + + end if + + ! Deallocate local arrays + if (allocated(effrg_in_local)) deallocate(effrg_in_local) + if (allocated(effri_out_local)) deallocate(effri_out_local) + if (allocated(effrl_inout_local)) deallocate(effrl_inout_local) + if (allocated(effrr_in_local)) deallocate(effrr_in_local) + if (allocated(effrs_inout_local)) deallocate(effrs_inout_local) + + ! Nullify local pointers + if (associated(effrg_in_ptr)) nullify(effrg_in_ptr) + if (associated(ncg_in_ptr)) nullify(ncg_in_ptr) + if (associated(nci_out_ptr)) nullify(nci_out_ptr) + if (associated(effri_out_ptr)) nullify(effri_out_ptr) + if (associated(ncl_out_ptr)) nullify(ncl_out_ptr) + ! Suite state does not change + + end subroutine main_suite_radiation1 + + ! ======================================================================== + + + subroutine main_suite_rad_lw_group(errflg, errmsg, col_start, col_end, fluxlw, ncols) + + use rad_lw, only: rad_lw_run + use mod_rad_ddt, only: ty_rad_lw + + ! Dummy arguments + integer, intent(out) :: errflg + character(len=512), intent(out) :: errmsg + integer, intent(in) :: col_start + integer, intent(in) :: col_end + type(ty_rad_lw), intent(inout) :: fluxlw(:) + integer, intent(in) :: ncols + + ! Local Variables + integer :: ncol + type(ty_rad_lw) :: internal_var_ty_rad_lw + + ! Initialize ccpp error handling + errflg = 0 + errmsg = '' + + ! Check state machine + if (trim(ccpp_suite_state) /= 'in_time_step') then + errflg = 1 + write(errmsg, '(3a)') "Invalid initial CCPP state, '", trim(ccpp_suite_state), & + "' in main_suite_rad_lw_group" + return + end if + ! Set horizontal loop extent + ncol = col_end - col_start + 1 + + ! Allocate local arrays + + if (errflg == 0) then + ! ################################################################## + ! Begin debug tests + ! ################################################################## + + ! ################################################################## + ! End debug tests + ! ################################################################## + + + + ! Call scheme + call rad_lw_run(ncol=ncol, fluxlw=fluxlw, errmsg=errmsg, errflg=errflg) + + + + end if + ! Suite state does not change + + end subroutine main_suite_rad_lw_group + + ! ======================================================================== + + + subroutine main_suite_rad_sw_group(errflg, errmsg, col_start, col_end, sfc_up_sw, ncols, & + sfc_down_sw) + + use rad_sw, only: rad_sw_run + + ! Dummy arguments + integer, intent(out) :: errflg + character(len=512), intent(out) :: errmsg + integer, intent(in) :: col_start + integer, intent(in) :: col_end + real(kind_phys), intent(inout) :: sfc_up_sw(:) + integer, intent(in) :: ncols + real(kind_phys), intent(inout) :: sfc_down_sw(:) + + ! Local Variables + integer :: ncol + type(real) :: internal_var_real + + ! Initialize ccpp error handling + errflg = 0 + errmsg = '' + + ! Check state machine + if (trim(ccpp_suite_state) /= 'in_time_step') then + errflg = 1 + write(errmsg, '(3a)') "Invalid initial CCPP state, '", trim(ccpp_suite_state), & + "' in main_suite_rad_sw_group" + return + end if + ! Set horizontal loop extent + ncol = col_end - col_start + 1 + + ! Allocate local arrays + + if (errflg == 0) then + ! ################################################################## + ! Begin debug tests + ! ################################################################## + + ! Check size of array sfc_up_sw + if (size(sfc_up_sw(:)) /= 1*(ncol-1+1)) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_rad_sw_group before rad_sw_run: for array sfc_up_sw, expected size ', 1*(ncol-1+1),& + ' but got ', size(sfc_up_sw) + errflg = 1 + return + end if + + + ! Check size of array sfc_down_sw + if (size(sfc_down_sw(:)) /= 1*(ncol-1+1)) then + write(errmsg, '(2(a,i8))') & + 'In group main_suite_rad_sw_group before rad_sw_run: for array sfc_down_sw, expected size ', 1*(ncol-1+1),& + ' but got ', size(sfc_down_sw) + errflg = 1 + return + end if + + + ! ################################################################## + ! End debug tests + ! ################################################################## + + + + ! Call scheme + call rad_sw_run(ncol=ncol, sfc_up_sw=sfc_up_sw, sfc_down_sw=sfc_down_sw, errmsg=errmsg, & + errflg=errflg) + + + + end if + ! Suite state does not change + + end subroutine main_suite_rad_sw_group + + ! ======================================================================== + + + subroutine main_suite_timestep_final(errflg, errmsg) + + + ! Dummy arguments + integer, intent(out) :: errflg + character(len=512), intent(out) :: errmsg + + ! Initialize ccpp error handling + errflg = 0 + errmsg = '' + + ! Output threaded region check +#ifdef _OPENMP + if (omp_get_thread_num() > 1) then + errflg = 1 + errmsg = "Cannot call timestep_final routine from a threaded region" + return + end if +#endif + ! Check state machine + if (trim(ccpp_suite_state) /= 'in_time_step') then + errflg = 1 + write(errmsg, '(3a)') "Invalid initial CCPP state, '", trim(ccpp_suite_state), & + "' in main_suite_timestep_final" + return + end if + ! Set horizontal loop extent + + ! Allocate local arrays + ccpp_suite_state = 'initialized' + + end subroutine main_suite_timestep_final + + ! ======================================================================== + + + subroutine main_suite_finalize(errflg, errmsg) + + + ! Dummy arguments + integer, intent(out) :: errflg + character(len=512), intent(out) :: errmsg + + ! Initialize ccpp error handling + errflg = 0 + errmsg = '' + + ! Output threaded region check +#ifdef _OPENMP + if (omp_get_thread_num() > 1) then + errflg = 1 + errmsg = "Cannot call finalize routine from a threaded region" + return + end if +#endif + ! Check state machine + if (trim(ccpp_suite_state) /= 'initialized') then + errflg = 1 + write(errmsg, '(3a)') "Invalid initial CCPP state, '", trim(ccpp_suite_state), & + "' in main_suite_finalize" + return + end if + ! Set horizontal loop extent + + ! Allocate local arrays + ccpp_suite_state = 'uninitialized' + + end subroutine main_suite_finalize + + ! ======================================================================== + + subroutine ccpp_create_constituent_array(errmsg, errflg) + ! Allocate and fill the constituent property array + ! for this suite + ! Dummy arguments + character(len=512), intent(out) :: errmsg + integer, intent(out) :: errflg + errmsg = '' + errflg = 0 + ccpp_constituents_initialized = .true. + end subroutine ccpp_create_constituent_array + + + ! ======================================================================== + + integer function main_suite_constituents_num_consts(errmsg, errflg) + ! Return the number of constituents for this suite + ! Dummy arguments + character(len=512), intent(out) :: errmsg + integer, intent(out) :: errflg + errmsg = '' + errflg = 0 + ! Make sure that our constituent array is initialized + if (.not. ccpp_constituents_initialized) then + call ccpp_create_constituent_array(errflg=errflg, errmsg=errmsg) + end if + main_suite_constituents_num_consts = 0 + end function main_suite_constituents_num_consts + + ! ======================================================================== + + subroutine main_suite_constituents_const_name(index, name_out, errmsg, errflg) + ! Return the name of constituent, + ! Dummy arguments + integer, intent(in) :: index + character(len=*), intent(out) :: name_out + character(len=512), intent(out) :: errmsg + integer, intent(out) :: errflg + + errflg = 0 + errmsg = '' + ! Make sure that our constituent array is initialized + if (.not. ccpp_constituents_initialized) then + errflg = 1 + errmsg = "constituent properties not initialized for suite, main_suite" + end if + errflg = 1 + write(errmsg, '(a,i0,a)') 'ERROR: main_suite_constituents, has no constituents' + end subroutine main_suite_constituents_const_name + + ! ======================================================================== + + subroutine main_suite_constituents_copy_const(index, cnst_out, errmsg, errflg) + ! Copy the data for a constituent + ! Dummy arguments + integer, intent(in) :: index + type(ccpp_constituent_properties_t), intent(out) :: cnst_out + character(len=512), intent(out) :: errmsg + integer, intent(out) :: errflg + + errflg = 0 + errmsg = '' + ! Make sure that our constituent array is initialized + if (.not. ccpp_constituents_initialized) then + errflg = 1 + errmsg = "constituent properties not initialized for suite, main_suite" + end if + errflg = 1 + write(errmsg, '(a,i0,a)') 'ERROR: main_suite_constituents, has no constituents' + end subroutine main_suite_constituents_copy_const + +end module ccpp_main_suite_cap diff --git a/test/nested_suite_test/effr_calc.F90 b/test/nested_suite_test/effr_calc.F90 new file mode 100644 index 00000000..0b626c16 --- /dev/null +++ b/test/nested_suite_test/effr_calc.F90 @@ -0,0 +1,84 @@ +!Test unit conversions for intent in, inout, out variables +! + +module effr_calc + + use ccpp_kinds, only: kind_phys + + implicit none + private + + public :: effr_calc_run, effr_calc_init + + contains + !> \section arg_table_effr_calc_init Argument Table + !! \htmlinclude arg_table_effr_calc_init.html + !! + subroutine effr_calc_init(scheme_order, errmsg, errflg) + character(len=512), intent(out) :: errmsg + integer, intent(out) :: errflg + integer, intent(inout) :: scheme_order + + errmsg = '' + errflg = 0 + + if (scheme_order .ne. 2) then + errflg = 1 + errmsg = 'ERROR: effr_calc_init() needs to be called second' + return + else + scheme_order = scheme_order + 1 + endif + + end subroutine effr_calc_init + + !> \section arg_table_effr_calc_run Argument Table + !! \htmlinclude arg_table_effr_calc_run.html + !! + subroutine effr_calc_run(ncol, nlev, effrr_in, effrg_in, ncg_in, nci_out, & + effrl_inout, effri_out, effrs_inout, ncl_out, & + has_graupel, scalar_var, tke_inout, tke2_inout, & + errmsg, errflg) + + integer, intent(in) :: ncol + integer, intent(in) :: nlev + real(kind_phys), intent(in) :: effrr_in(:,:) + real(kind_phys), intent(in),optional :: effrg_in(:,:) + real(kind_phys), intent(in),optional :: ncg_in(:,:) + real(kind_phys), intent(out),optional :: nci_out(:,:) + real(kind_phys), intent(inout) :: effrl_inout(:,:) + real(kind_phys), intent(out),optional :: effri_out(:,:) + real(8),intent(inout) :: effrs_inout(:,:) + logical, intent(in) :: has_graupel + real(kind_phys), intent(inout) :: scalar_var + character(len=512), intent(out) :: errmsg + integer, intent(out) :: errflg + real(kind_phys), intent(out),optional :: ncl_out(:,:) + real(kind_phys), intent(inout) :: tke_inout + real(kind_phys), intent(inout) :: tke2_inout + + !---------------------------------------------------------------- + + real(kind_phys), parameter :: re_qc_min = 2.5 ! microns + real(kind_phys), parameter :: re_qc_max = 50. ! microns + real(kind_phys), parameter :: re_qi_avg = 75. ! microns + real(kind_phys) :: effrr_local(ncol,nlev) + real(kind_phys) :: effrg_local(ncol,nlev) + real(kind_phys) :: ncg_in_local(ncol,nlev) + real(kind_phys) :: nci_out_local(ncol,nlev) + + errmsg = '' + errflg = 0 + + effrr_local = effrr_in + if (present(effrg_in)) effrg_local = effrg_in + if (present(ncg_in)) ncg_in_local = ncg_in + if (present(nci_out)) nci_out_local = nci_out + effrl_inout = min(max(effrl_inout,re_qc_min),re_qc_max) + if (present(effri_out)) effri_out = re_qi_avg + effrs_inout = effrs_inout + (10.0 / 6.0) ! in micrometer + scalar_var = 2.0 ! in km + + end subroutine effr_calc_run + +end module effr_calc diff --git a/test/nested_suite_test/effr_calc.meta b/test/nested_suite_test/effr_calc.meta new file mode 100644 index 00000000..c3733f13 --- /dev/null +++ b/test/nested_suite_test/effr_calc.meta @@ -0,0 +1,163 @@ +[ccpp-table-properties] + name = effr_calc + type = scheme + dependencies = +######################################################################## +[ccpp-arg-table] + name = effr_calc_init + type = scheme +[ scheme_order ] + standard_name = scheme_order_in_suite + long_name = scheme order in suite definition file + units = None + dimensions = () + type = integer + intent = inout +[ errmsg ] + standard_name = ccpp_error_message + long_name = Error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=512 + intent = out +[ errflg ] + standard_name = ccpp_error_code + long_name = Error flag for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out +######################################################################## +[ccpp-arg-table] + name = effr_calc_run + type = scheme +[ ncol ] + standard_name = horizontal_loop_extent + type = integer + units = count + dimensions = () + intent = in +[ nlev ] + standard_name = vertical_layer_dimension + type = integer + units = count + dimensions = () + intent = in +[effrr_in] + standard_name = effective_radius_of_stratiform_cloud_rain_particle + long_name = effective radius of cloud rain particle in micrometer + units = um + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = in + top_at_one = True +[effrg_in] + standard_name = effective_radius_of_stratiform_cloud_graupel + long_name = effective radius of cloud graupel in micrometer + units = um + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = in + optional = True +[ncg_in] + standard_name = cloud_graupel_number_concentration + long_name = number concentration of cloud graupel + units = kg-1 + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = in + optional = True +[nci_out] + standard_name = cloud_ice_number_concentration + long_name = number concentration of cloud ice + units = kg-1 + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = out + optional = True +[effrl_inout] + standard_name = effective_radius_of_stratiform_cloud_liquid_water_particle + long_name = effective radius of cloud liquid water particle in micrometer + units = um + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = inout +[effri_out] + standard_name = effective_radius_of_stratiform_cloud_ice_particle + long_name = effective radius of cloud ice water particle in micrometer + units = um + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = out + optional = True +[effrs_inout] + standard_name = effective_radius_of_stratiform_cloud_snow_particle + long_name = effective radius of cloud snow particle in micrometer + units = um + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = 8 + intent = inout + top_at_one = True +[ncl_out] + standard_name = cloud_liquid_number_concentration + long_name = number concentration of cloud liquid + units = kg-1 + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = out + optional = True +[has_graupel] + standard_name = flag_indicating_cloud_microphysics_has_graupel + long_name = flag indicating that the cloud microphysics produces graupel + units = flag + dimensions = () + type = logical + intent = in +[ scalar_var ] + standard_name = scalar_variable_for_testing + long_name = scalar variable for testing + units = km + dimensions = () + type = real + kind = kind_phys + intent = inout +[ tke_inout ] + standard_name = turbulent_kinetic_energy + long_name = turbulent_kinetic_energy + units = m2 s-2 + dimensions = () + type = real + kind = kind_phys + intent = inout +[ tke2_inout ] + standard_name = turbulent_kinetic_energy2 + long_name = turbulent_kinetic_energy2 + units = m+2 s-2 + dimensions = () + type = real + kind = kind_phys + intent = inout +[ errmsg ] + standard_name = ccpp_error_message + long_name = Error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=512 + intent = out +[ errflg ] + standard_name = ccpp_error_code + long_name = Error flag for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out diff --git a/test/nested_suite_test/effr_diag.F90 b/test/nested_suite_test/effr_diag.F90 new file mode 100644 index 00000000..409ff2f9 --- /dev/null +++ b/test/nested_suite_test/effr_diag.F90 @@ -0,0 +1,68 @@ +!Test unit conversions for intent in, inout, out variables +! + +module effr_diag + + use ccpp_kinds, only: kind_phys + + implicit none + private + + public :: effr_diag_run, effr_diag_init + +contains + + !> \section arg_table_effr_diag_init Argument Table + !! \htmlinclude arg_table_effr_diag_init.html + !! + subroutine effr_diag_init(scheme_order, errmsg, errflg) + character(len=512), intent(out) :: errmsg + integer, intent(out) :: errflg + integer, intent(inout) :: scheme_order + + errmsg = '' + errflg = 0 + + if (scheme_order .ne. 4) then + errflg = 1 + errmsg = 'ERROR: effr_diag_init() needs to be called fourth' + return + else + scheme_order = scheme_order + 1 + endif + + end subroutine effr_diag_init + + !> \section arg_table_effr_diag_run Argument Table + !! \htmlinclude arg_table_effr_diag_run.html + !! + subroutine effr_diag_run( effrr_in, scalar_var, errmsg, errflg) + + real(kind_phys), intent(in) :: effrr_in(:,:) + integer, intent(in) :: scalar_var + character(len=512), intent(out) :: errmsg + integer, intent(out) :: errflg + !---------------------------------------------------------------- + real(kind_phys) :: effrr_min, effrr_max + + errmsg = '' + errflg = 0 + + call cmp_effr_diag(effrr_in, effrr_min, effrr_max) + + if (scalar_var .ne. 380) then + errmsg = 'ERROR: effr_diag_run(): scalar_var should be 380' + errflg = 1 + endif + end subroutine effr_diag_run + + subroutine cmp_effr_diag(effr, effr_min, effr_max) + real(kind_phys), intent(in) :: effr(:,:) + real(kind_phys), intent(out) :: effr_min, effr_max + + ! Do some diagnostic calcualtions... + effr_min = minval(effr) + effr_max = maxval(effr) + + end subroutine cmp_effr_diag +end module effr_diag diff --git a/test/nested_suite_test/effr_diag.meta b/test/nested_suite_test/effr_diag.meta new file mode 100644 index 00000000..9e0e4fc2 --- /dev/null +++ b/test/nested_suite_test/effr_diag.meta @@ -0,0 +1,65 @@ +[ccpp-table-properties] + name = effr_diag + type = scheme + dependencies = +######################################################################## +[ccpp-arg-table] + name = effr_diag_init + type = scheme +[ scheme_order ] + standard_name = scheme_order_in_suite + long_name = scheme order in suite definition file + units = None + dimensions = () + type = integer + intent = inout +[ errmsg ] + standard_name = ccpp_error_message + long_name = Error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=512 + intent = out +[ errflg ] + standard_name = ccpp_error_code + long_name = Error flag for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out +######################################################################## +[ccpp-arg-table] + name = effr_diag_run + type = scheme +[effrr_in] + standard_name = effective_radius_of_stratiform_cloud_rain_particle + long_name = effective radius of cloud rain particle in micrometer + units = um + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = in + top_at_one = True +[ scalar_var ] + standard_name = scalar_variable_for_testing_c + long_name = unused scalar variable C + units = m + dimensions = () + type = integer + intent = in +[ errmsg ] + standard_name = ccpp_error_message + long_name = Error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=512 + intent = out +[ errflg ] + standard_name = ccpp_error_code + long_name = Error flag for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out diff --git a/test/nested_suite_test/effr_post.F90 b/test/nested_suite_test/effr_post.F90 new file mode 100644 index 00000000..d42a574c --- /dev/null +++ b/test/nested_suite_test/effr_post.F90 @@ -0,0 +1,61 @@ +!Test unit conversions for intent in, inout, out variables +! + +module effr_post + + use ccpp_kinds, only: kind_phys + + implicit none + private + + public :: effr_post_run, effr_post_init + +contains + + !> \section arg_table_effr_post_init Argument Table + !! \htmlinclude arg_table_effr_post_init.html + !! + subroutine effr_post_init(scheme_order, errmsg, errflg) + character(len=512), intent(out) :: errmsg + integer, intent(out) :: errflg + integer, intent(inout) :: scheme_order + + errmsg = '' + errflg = 0 + + if (scheme_order .ne. 3) then + errflg = 1 + errmsg = 'ERROR: effr_post_init() needs to be called third' + return + else + scheme_order = scheme_order + 1 + endif + + end subroutine effr_post_init + + !> \section arg_table_effr_post_run Argument Table + !! \htmlinclude arg_table_effr_post_run.html + !! + subroutine effr_post_run( effrr_inout, scalar_var, errmsg, errflg) + + real(kind_phys), intent(inout) :: effrr_inout(:,:) + real(kind_phys), intent(in) :: scalar_var + character(len=512), intent(out) :: errmsg + integer, intent(out) :: errflg + !---------------------------------------------------------------- + real(kind_phys) :: effrr_min, effrr_max + + errmsg = '' + errflg = 0 + + ! Do some post-processing on effrr... + effrr_inout(:,:) = effrr_inout(:,:)*1._kind_phys + + if (scalar_var .ne. 1013.0) then + errmsg = 'ERROR: effr_post_run(): scalar_var should be 1013.0' + errflg = 1 + endif + + end subroutine effr_post_run + + end module effr_post diff --git a/test/nested_suite_test/effr_post.meta b/test/nested_suite_test/effr_post.meta new file mode 100644 index 00000000..721582a6 --- /dev/null +++ b/test/nested_suite_test/effr_post.meta @@ -0,0 +1,65 @@ +[ccpp-table-properties] + name = effr_post + type = scheme + dependencies = +######################################################################## +[ccpp-arg-table] + name = effr_post_init + type = scheme +[ scheme_order ] + standard_name = scheme_order_in_suite + long_name = scheme order in suite definition file + units = None + dimensions = () + type = integer + intent = inout +[ errmsg ] + standard_name = ccpp_error_message + long_name = Error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=512 + intent = out +[ errflg ] + standard_name = ccpp_error_code + long_name = Error flag for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out +######################################################################## +[ccpp-arg-table] + name = effr_post_run + type = scheme +[effrr_inout] + standard_name = effective_radius_of_stratiform_cloud_rain_particle + long_name = effective radius of cloud rain particle in micrometer + units = m + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = inout +[ scalar_var ] + standard_name = scalar_variable_for_testing_b + long_name = unused scalar variable B + units = m + dimensions = () + type = real + kind = kind_phys + intent = in +[ errmsg ] + standard_name = ccpp_error_message + long_name = Error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=512 + intent = out +[ errflg ] + standard_name = ccpp_error_code + long_name = Error flag for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out diff --git a/test/nested_suite_test/effr_pre.F90 b/test/nested_suite_test/effr_pre.F90 new file mode 100644 index 00000000..17a3b187 --- /dev/null +++ b/test/nested_suite_test/effr_pre.F90 @@ -0,0 +1,60 @@ +!Test unit conversions for intent in, inout, out variables +! + +module mod_effr_pre + + use ccpp_kinds, only: kind_phys + + implicit none + private + + public :: effr_pre_run, effr_pre_init + +contains + !> \section arg_table_effr_pre_init Argument Table + !! \htmlinclude arg_table_effr_pre_init.html + !! + subroutine effr_pre_init(scheme_order, errmsg, errflg) + character(len=512), intent(out) :: errmsg + integer, intent(out) :: errflg + integer, intent(inout) :: scheme_order + + errmsg = '' + errflg = 0 + + if (scheme_order .ne. 1) then + errflg = 1 + errmsg = 'ERROR: effr_pre_init() needs to be called first' + return + else + scheme_order = scheme_order + 1 + endif + + end subroutine effr_pre_init + + !> \section arg_table_effr_pre_run Argument Table + !! \htmlinclude arg_table_effr_pre_run.html + !! + subroutine effr_pre_run( effrr_inout, scalar_var, errmsg, errflg) + + real(kind_phys), intent(inout) :: effrr_inout(:,:) + real(kind_phys), intent(in) :: scalar_var + character(len=512), intent(out) :: errmsg + integer, intent(out) :: errflg + !---------------------------------------------------------------- + real(kind_phys) :: effrr_min, effrr_max + + errmsg = '' + errflg = 0 + + ! Do some pre-processing on effrr... + effrr_inout(:,:) = effrr_inout(:,:)*1._kind_phys + + if (scalar_var .ne. 273.15) then + errmsg = 'ERROR: effr_pre_run(): scalar_var should be 273.15' + errflg = 1 + endif + + end subroutine effr_pre_run + +end module mod_effr_pre diff --git a/test/nested_suite_test/effr_pre.meta b/test/nested_suite_test/effr_pre.meta new file mode 100644 index 00000000..251b4175 --- /dev/null +++ b/test/nested_suite_test/effr_pre.meta @@ -0,0 +1,66 @@ +[ccpp-table-properties] + name = effr_pre + type = scheme + module_name = mod_effr_pre + dependencies = +######################################################################## +[ccpp-arg-table] + name = effr_pre_init + type = scheme +[ scheme_order ] + standard_name = scheme_order_in_suite + long_name = scheme order in suite definition file + units = None + dimensions = () + type = integer + intent = inout +[ errmsg ] + standard_name = ccpp_error_message + long_name = Error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=512 + intent = out +[ errflg ] + standard_name = ccpp_error_code + long_name = Error flag for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out +######################################################################## +[ccpp-arg-table] + name = effr_pre_run + type = scheme +[effrr_inout] + standard_name = effective_radius_of_stratiform_cloud_rain_particle + long_name = effective radius of cloud rain particle in micrometer + units = m + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + type = real + kind = kind_phys + intent = inout +[ scalar_var ] + standard_name = scalar_variable_for_testing_a + long_name = unused scalar variable A + units = m + dimensions = () + type = real + kind = kind_phys + intent = in +[ errmsg ] + standard_name = ccpp_error_message + long_name = Error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=512 + intent = out +[ errflg ] + standard_name = ccpp_error_code + long_name = Error flag for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out diff --git a/test/nested_suite_test/effrs_calc.F90 b/test/nested_suite_test/effrs_calc.F90 new file mode 100644 index 00000000..e9266905 --- /dev/null +++ b/test/nested_suite_test/effrs_calc.F90 @@ -0,0 +1,32 @@ +!Test unit conversions for intent in, inout, out variables +! + +module effrs_calc + + use ccpp_kinds, only: kind_phys + + implicit none + private + + public :: effrs_calc_run + + contains + !> \section arg_table_effrs_calc_run Argument Table + !! \htmlinclude arg_table_effrs_calc_run.html + !! + subroutine effrs_calc_run(effrs_inout, errmsg, errflg) + + real(kind_phys), intent(inout) :: effrs_inout(:,:) + character(len=512), intent(out) :: errmsg + integer, intent(out) :: errflg + + !---------------------------------------------------------------- + + errmsg = '' + errflg = 0 + + effrs_inout = effrs_inout + (10.E-6_kind_phys / 3._kind_phys) ! in meters + + end subroutine effrs_calc_run + +end module effrs_calc diff --git a/test/nested_suite_test/effrs_calc.meta b/test/nested_suite_test/effrs_calc.meta new file mode 100644 index 00000000..9ce7b88e --- /dev/null +++ b/test/nested_suite_test/effrs_calc.meta @@ -0,0 +1,25 @@ +[ccpp-table-properties] + name = effrs_calc + type = scheme + +[ccpp-arg-table] + name = effrs_calc_run + type = scheme +[ effrs_inout ] + standard_name = effective_radius_of_stratiform_cloud_snow_particle + units = m + type = real | kind = kind_phys + dimensions = (horizontal_loop_extent,vertical_layer_dimension) + intent = inout +[ errmsg ] + standard_name = ccpp_error_message + units = none + type = character | kind = len=512 + dimensions = () + intent = out +[ errflg ] + standard_name = ccpp_error_code + units = 1 + type = integer + dimensions = () + intent = out diff --git a/test/nested_suite_test/main_suite.xml b/test/nested_suite_test/main_suite.xml new file mode 100644 index 00000000..e9b96c61 --- /dev/null +++ b/test/nested_suite_test/main_suite.xml @@ -0,0 +1,30 @@ + + + + + + + rad_lw + + + + + + + + + + effr_pre + + + effr_calc + + + effr_post + + + + + + + \ No newline at end of file diff --git a/test/nested_suite_test/module_rad_ddt.F90 b/test/nested_suite_test/module_rad_ddt.F90 new file mode 100644 index 00000000..21a1a0ec --- /dev/null +++ b/test/nested_suite_test/module_rad_ddt.F90 @@ -0,0 +1,23 @@ +module mod_rad_ddt + USE ccpp_kinds, ONLY: kind_phys + implicit none + + public ty_rad_lw, ty_rad_sw + + !> \section arg_table_ty_rad_lw Argument Table + !! \htmlinclude arg_table_ty_rad_lw.html + !! + type ty_rad_lw + real(kind_phys) :: sfc_up_lw + real(kind_phys) :: sfc_down_lw + end type ty_rad_lw + + !> \section arg_table_ty_rad_sw Argument Table + !! \htmlinclude arg_table_ty_rad_sw.html + !! + type ty_rad_sw + real(kind_phys), pointer :: sfc_up_sw(:) => null() + real(kind_phys), pointer :: sfc_down_sw(:) => null() + end type ty_rad_sw + +end module mod_rad_ddt diff --git a/test/nested_suite_test/module_rad_ddt.meta b/test/nested_suite_test/module_rad_ddt.meta new file mode 100644 index 00000000..c4792547 --- /dev/null +++ b/test/nested_suite_test/module_rad_ddt.meta @@ -0,0 +1,40 @@ +[ccpp-table-properties] + name = ty_rad_lw + type = ddt + dependencies = + module_name = mod_rad_ddt +[ccpp-arg-table] + name = ty_rad_lw + type = ddt +[ sfc_up_lw ] + standard_name = surface_upwelling_longwave_radiation_flux + units = W m2 + dimensions = () + type = real + kind = kind_phys +[ sfc_down_lw ] + standard_name = surface_downwelling_longwave_radiation_flux + units = W m2 + dimensions = () + type = real + kind = kind_phys + +[ccpp-table-properties] + name = ty_rad_sw + type = ddt + module_name = mod_rad_ddt +[ccpp-arg-table] + name = ty_rad_sw + type = ddt +[ sfc_up_sw ] + standard_name = surface_upwelling_shortwave_radiation_flux + units = W m2 + dimensions = (horizontal_dimension) + type = real + kind = kind_phys +[ sfc_down_sw ] + standard_name = surface_downwelling_shortwave_radiation_flux + units = W m2 + dimensions = (horizontal_dimension) + type = real + kind = kind_phys diff --git a/test/nested_suite_test/nested_suite_test_reports.py b/test/nested_suite_test/nested_suite_test_reports.py new file mode 100755 index 00000000..b409e65a --- /dev/null +++ b/test/nested_suite_test/nested_suite_test_reports.py @@ -0,0 +1,116 @@ +#! /usr/bin/env python3 +""" +----------------------------------------------------------------------- + Description: Test capgen database report python interface + + Assumptions: + + Command line arguments: build_dir database_filepath + + Usage: python test_reports +----------------------------------------------------------------------- +""" +import os +import unittest + +from test_stub import BaseTests + +_BUILD_DIR = os.path.join(os.path.abspath(os.environ['BUILD_DIR']), "test", "nested_suite_test") +_DATABASE = os.path.abspath(os.path.join(_BUILD_DIR, "ccpp", "datatable.xml")) + +_TEST_DIR = os.path.dirname(os.path.abspath(__file__)) +_FRAMEWORK_DIR = os.path.abspath(os.path.join(_TEST_DIR, os.pardir, os.pardir)) +_SCRIPTS_DIR = os.path.join(_FRAMEWORK_DIR, "scripts") +_SRC_DIR = os.path.join(_FRAMEWORK_DIR, "src") + +# Check data +_HOST_FILES = [os.path.join(_BUILD_DIR, "ccpp", "test_host_ccpp_cap.F90")] +_SUITE_FILES = [os.path.join(_BUILD_DIR, "ccpp", "ccpp_var_compatibility_suite_cap.F90")] +_UTILITY_FILES = [os.path.join(_BUILD_DIR, "ccpp", "ccpp_kinds.F90"), + os.path.join(_SRC_DIR, "ccpp_constituent_prop_mod.F90"), + os.path.join(_SRC_DIR, "ccpp_scheme_utils.F90"), + os.path.join(_SRC_DIR, "ccpp_hashable.F90"), + os.path.join(_SRC_DIR, "ccpp_hash_table.F90")] +_CCPP_FILES = _UTILITY_FILES + \ + [os.path.join(_BUILD_DIR, "ccpp", "test_host_ccpp_cap.F90"), + os.path.join(_BUILD_DIR, "ccpp", "ccpp_var_compatibility_suite_cap.F90")] +_PROCESS_LIST = [""] +_MODULE_LIST = ["effr_calc", "effrs_calc", "effr_diag", "effr_post", "mod_effr_pre", "rad_lw", "rad_sw"] +_SUITE_LIST = ["nested_suite"] +_DEPENDENCIES = [ os.path.join(_TEST_DIR, "module_rad_ddt.F90")] +_INPUT_VARS_VAR_ACTION = ["horizontal_loop_begin", "horizontal_loop_end", "horizontal_dimension", "vertical_layer_dimension", + "effective_radius_of_stratiform_cloud_liquid_water_particle", + "effective_radius_of_stratiform_cloud_rain_particle", + "effective_radius_of_stratiform_cloud_snow_particle", + "effective_radius_of_stratiform_cloud_graupel", + "cloud_graupel_number_concentration", + "scalar_variable_for_testing", + "turbulent_kinetic_energy", + "turbulent_kinetic_energy2", + "scalar_variable_for_testing_a", + "scalar_variable_for_testing_b", + "scalar_variable_for_testing_c", + "scheme_order_in_suite", + "flag_indicating_cloud_microphysics_has_graupel", + "flag_indicating_cloud_microphysics_has_ice", + "surface_downwelling_shortwave_radiation_flux", + "surface_upwelling_shortwave_radiation_flux", + "longwave_radiation_fluxes", + "num_subcycles_for_effr"] +_OUTPUT_VARS_VAR_ACTION = ["ccpp_error_code", "ccpp_error_message", + "effective_radius_of_stratiform_cloud_ice_particle", + "effective_radius_of_stratiform_cloud_liquid_water_particle", + "effective_radius_of_stratiform_cloud_snow_particle", + "cloud_ice_number_concentration", + "effective_radius_of_stratiform_cloud_rain_particle", + "turbulent_kinetic_energy", + "turbulent_kinetic_energy2", + "scalar_variable_for_testing", + "scalar_variable_for_testing", + "surface_downwelling_shortwave_radiation_flux", + "surface_upwelling_shortwave_radiation_flux", + "longwave_radiation_fluxes", + "scheme_order_in_suite"] +_REQUIRED_VARS_VAR_ACTION = _INPUT_VARS_VAR_ACTION + _OUTPUT_VARS_VAR_ACTION + + +class TestVarCompatibilityHostDataTables(unittest.TestCase, BaseTests.TestHostDataTables): + database = _DATABASE + host_files = _HOST_FILES + suite_files = _SUITE_FILES + utility_files = _UTILITY_FILES + ccpp_files = _CCPP_FILES + process_list = _PROCESS_LIST + module_list = _MODULE_LIST + dependencies = _DEPENDENCIES + suite_list = _SUITE_LIST + + +class CommandLineVarCompatibilityHostDatafileRequiredFiles(unittest.TestCase, BaseTests.TestHostCommandLineDataFiles): + database = _DATABASE + host_files = _HOST_FILES + suite_files = _SUITE_FILES + utility_files = _UTILITY_FILES + ccpp_files = _CCPP_FILES + process_list = _PROCESS_LIST + module_list = _MODULE_LIST + dependencies = _DEPENDENCIES + suite_list = _SUITE_LIST + datafile_script = f"{_SCRIPTS_DIR}/ccpp_datafile.py" + + +class TestCapgenDdtSuite(unittest.TestCase, BaseTests.TestSuite): + database = _DATABASE + required_vars = _REQUIRED_VARS_VAR_ACTION + input_vars = _INPUT_VARS_VAR_ACTION + output_vars = _OUTPUT_VARS_VAR_ACTION + suite_name = "nested_suite" + + +class CommandLineCapgenDdtSuite(unittest.TestCase, BaseTests.TestSuiteCommandLine): + database = _DATABASE + required_vars = _REQUIRED_VARS_VAR_ACTION + input_vars = _INPUT_VARS_VAR_ACTION + output_vars = _OUTPUT_VARS_VAR_ACTION + suite_name = "nested_suite" + datafile_script = f"{_SCRIPTS_DIR}/ccpp_datafile.py" diff --git a/test/nested_suite_test/rad_lw.F90 b/test/nested_suite_test/rad_lw.F90 new file mode 100644 index 00000000..5859f8bf --- /dev/null +++ b/test/nested_suite_test/rad_lw.F90 @@ -0,0 +1,35 @@ +module rad_lw + use ccpp_kinds, only: kind_phys + use mod_rad_ddt, only: ty_rad_lw + + implicit none + private + + public :: rad_lw_run + +contains + + !> \section arg_table_rad_lw_run Argument Table + !! \htmlinclude arg_table_rad_lw_run.html + !! + subroutine rad_lw_run(ncol, fluxLW, errmsg, errflg) + + integer, intent(in) :: ncol + type(ty_rad_lw), intent(inout) :: fluxLW(:) + character(len=512), intent(out) :: errmsg + integer, intent(out) :: errflg + + ! Locals + integer :: icol + + errmsg = '' + errflg = 0 + + do icol=1,ncol + fluxLW(icol)%sfc_up_lw = 300._kind_phys + fluxLW(icol)%sfc_down_lw = 50._kind_phys + enddo + + end subroutine rad_lw_run + +end module rad_lw diff --git a/test/nested_suite_test/rad_lw.meta b/test/nested_suite_test/rad_lw.meta new file mode 100644 index 00000000..883edf1b --- /dev/null +++ b/test/nested_suite_test/rad_lw.meta @@ -0,0 +1,35 @@ +[ccpp-table-properties] + name = rad_lw + type = scheme + dependencies = module_rad_ddt.F90 +[ccpp-arg-table] + name = rad_lw_run + type = scheme +[ ncol ] + standard_name = horizontal_loop_extent + type = integer + units = count + dimensions = () + intent = in +[fluxLW] + standard_name = longwave_radiation_fluxes + long_name = longwave radiation fluxes + units = W m-2 + dimensions = (horizontal_loop_extent) + type = ty_rad_lw + intent = inout +[ errmsg ] + standard_name = ccpp_error_message + long_name = Error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=512 + intent = out +[ errflg ] + standard_name = ccpp_error_code + long_name = Error flag for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out diff --git a/test/nested_suite_test/rad_sw.F90 b/test/nested_suite_test/rad_sw.F90 new file mode 100644 index 00000000..ddf35224 --- /dev/null +++ b/test/nested_suite_test/rad_sw.F90 @@ -0,0 +1,35 @@ +module rad_sw + use ccpp_kinds, only: kind_phys + + implicit none + private + + public :: rad_sw_run + +contains + + !> \section arg_table_rad_sw_run Argument Table + !! \htmlinclude arg_table_rad_sw_run.html + !! + subroutine rad_sw_run(ncol, sfc_up_sw, sfc_down_sw, errmsg, errflg) + + integer, intent(in) :: ncol + real(kind_phys), intent(inout) :: sfc_up_sw(:) + real(kind_phys), intent(inout) :: sfc_down_sw(:) + character(len=512), intent(out) :: errmsg + integer, intent(out) :: errflg + + ! Locals + integer :: icol + + errmsg = '' + errflg = 0 + + do icol=1,ncol + sfc_up_sw(icol) = 100._kind_phys + sfc_down_sw(icol) = 400._kind_phys + enddo + + end subroutine rad_sw_run + +end module rad_sw diff --git a/test/nested_suite_test/rad_sw.meta b/test/nested_suite_test/rad_sw.meta new file mode 100644 index 00000000..d88b9acc --- /dev/null +++ b/test/nested_suite_test/rad_sw.meta @@ -0,0 +1,41 @@ +[ccpp-table-properties] + name = rad_sw + type = scheme +[ccpp-arg-table] + name = rad_sw_run + type = scheme +[ ncol ] + standard_name = horizontal_loop_extent + type = integer + units = count + dimensions = () + intent = in +[ sfc_up_sw ] + standard_name = surface_upwelling_shortwave_radiation_flux + units = W m2 + dimensions = (horizontal_loop_extent) + type = real + kind = kind_phys + intent = inout +[ sfc_down_sw ] + standard_name = surface_downwelling_shortwave_radiation_flux + units = W m2 + dimensions = (horizontal_loop_extent) + type = real + kind = kind_phys + intent = inout +[ errmsg ] + standard_name = ccpp_error_message + long_name = Error message for error handling in CCPP + units = none + dimensions = () + type = character + kind = len=512 + intent = out +[ errflg ] + standard_name = ccpp_error_code + long_name = Error flag for error handling in CCPP + units = 1 + dimensions = () + type = integer + intent = out diff --git a/test/nested_suite_test/radiation2_suite.xml b/test/nested_suite_test/radiation2_suite.xml new file mode 100644 index 00000000..6752f7ef --- /dev/null +++ b/test/nested_suite_test/radiation2_suite.xml @@ -0,0 +1,17 @@ + + + + + + effr_diag + + + + + + effrs_calc + + + + + \ No newline at end of file diff --git a/test/nested_suite_test/radiation3_subsuite.xml b/test/nested_suite_test/radiation3_subsuite.xml new file mode 100644 index 00000000..dc5bb983 --- /dev/null +++ b/test/nested_suite_test/radiation3_subsuite.xml @@ -0,0 +1,9 @@ + + + + + + rad_sw + + + diff --git a/test/nested_suite_test/test_host.F90 b/test/nested_suite_test/test_host.F90 new file mode 100644 index 00000000..f6d8059c --- /dev/null +++ b/test/nested_suite_test/test_host.F90 @@ -0,0 +1,266 @@ +module test_prog + + use ccpp_kinds, only: kind_phys + + implicit none + private + + public test_host + + ! Public data and interfaces + integer, public, parameter :: cs = 32 + integer, public, parameter :: cm = 60 + + !> \section arg_table_suite_info Argument Table + !! \htmlinclude arg_table_suite_info.html + !! + type, public :: suite_info + character(len=cs) :: suite_name = '' + character(len=cs), pointer :: suite_parts(:) => NULL() + character(len=cm), pointer :: suite_input_vars(:) => NULL() + character(len=cm), pointer :: suite_output_vars(:) => NULL() + character(len=cm), pointer :: suite_required_vars(:) => NULL() + end type suite_info + +CONTAINS + + logical function check_suite(test_suite) + use test_host_ccpp_cap, only: ccpp_physics_suite_part_list + use test_host_ccpp_cap, only: ccpp_physics_suite_variables + use test_utils, only: check_list + + ! Dummy argument + type(suite_info), intent(in) :: test_suite + ! Local variables + integer :: sind + logical :: check + integer :: errflg + character(len=512) :: errmsg + character(len=128), allocatable :: test_list(:) + + check_suite = .true. + write(6, *) "Checking suite ", trim(test_suite%suite_name) + ! First, check the suite parts + call ccpp_physics_suite_part_list(test_suite%suite_name, test_list, & + errmsg, errflg) + print *, "DH DEBUG test_list:" + print *, test_list + if (errflg == 0) then + check = check_list(test_list, test_suite%suite_parts, 'part names', & + suite_name=test_suite%suite_name) + else + check = .false. + write(6, '(a,i0,2a)') 'ERROR ', errflg, ': ', trim(errmsg) + end if + check_suite = check_suite .and. check + if (allocated(test_list)) then + deallocate(test_list) + end if + ! Check the input variables + call ccpp_physics_suite_variables(test_suite%suite_name, test_list, & + errmsg, errflg, input_vars=.true., output_vars=.false.) + if (errflg == 0) then + check = check_list(test_list, test_suite%suite_input_vars, & + 'input variable names', suite_name=test_suite%suite_name) + else + check = .false. + write(6, '(a,i0,2a)') 'ERROR ', errflg, ': ', trim(errmsg) + end if + check_suite = check_suite .and. check + if (allocated(test_list)) then + deallocate(test_list) + end if + ! Check the output variables + call ccpp_physics_suite_variables(test_suite%suite_name, test_list, & + errmsg, errflg, input_vars=.false., output_vars=.true.) + if (errflg == 0) then + check = check_list(test_list, test_suite%suite_output_vars, & + 'output variable names', suite_name=test_suite%suite_name) + else + check = .false. + write(6, '(a,i0,2a)') 'ERROR ', errflg, ': ', trim(errmsg) + end if + check_suite = check_suite .and. check + if (allocated(test_list)) then + deallocate(test_list) + end if + ! Check all required variables + call ccpp_physics_suite_variables(test_suite%suite_name, test_list, & + errmsg, errflg) + if (errflg == 0) then + check = check_list(test_list, test_suite%suite_required_vars, & + 'required variable names', suite_name=test_suite%suite_name) + else + check = .false. + write(6, '(a,i0,2a)') 'ERROR ', errflg, ': ', trim(errmsg) + end if + check_suite = check_suite .and. check + if (allocated(test_list)) then + deallocate(test_list) + end if + end function check_suite + + + !> \section arg_table_test_host Argument Table + !! \htmlinclude arg_table_test_host.html + !! + subroutine test_host(retval, test_suites) + + use test_host_mod, only: ncols + use test_host_ccpp_cap, only: test_host_ccpp_physics_initialize + use test_host_ccpp_cap, only: test_host_ccpp_physics_timestep_initial + use test_host_ccpp_cap, only: test_host_ccpp_physics_run + use test_host_ccpp_cap, only: test_host_ccpp_physics_timestep_final + use test_host_ccpp_cap, only: test_host_ccpp_physics_finalize + use test_host_ccpp_cap, only: ccpp_physics_suite_list + use test_host_mod, only: init_data, compare_data + use test_utils, only: check_list + + type(suite_info), intent(in) :: test_suites(:) + logical, intent(out) :: retval + + logical :: check + integer :: col_start, col_end + integer :: index, sind + integer :: num_suites + character(len=128), allocatable :: suite_names(:) + character(len=512) :: errmsg + integer :: errflg + + ! Initialize our 'data' + call init_data() + + ! Gather and test the inspection routines + num_suites = size(test_suites) + call ccpp_physics_suite_list(suite_names) + retval = check_list(suite_names, test_suites(:)%suite_name, & + 'suite names') + write(6, *) 'Available suites are:' + do index = 1, size(suite_names) + do sind = 1, num_suites + if (trim(test_suites(sind)%suite_name) == & + trim(suite_names(index))) then + exit + end if + end do + write(6, '(i0,3a,i0,a)') index, ') ', trim(suite_names(index)), & + ' = test_suites(', sind, ')' + end do + if (retval) then + do sind = 1, num_suites + check = check_suite(test_suites(sind)) + retval = retval .and. check + end do + end if + !!! Return here if any check failed + if (.not. retval) then + return + end if + + ! Use the suite information to setup the run + do sind = 1, num_suites + call test_host_ccpp_physics_initialize(test_suites(sind)%suite_name, & + errmsg, errflg) + if (errflg /= 0) then + write(6, '(4a)') 'ERROR in initialize of ', & + trim(test_suites(sind)%suite_name), ': ', trim(errmsg) + end if + end do + + ! Initialize the timestep + do sind = 1, num_suites + if (errflg /= 0) then + exit + end if + if (errflg == 0) then + call test_host_ccpp_physics_timestep_initial( & + test_suites(sind)%suite_name, errmsg, errflg) + end if + if (errflg /= 0) then + write(6, '(3a)') trim(test_suites(sind)%suite_name), ': ', & + trim(errmsg) + exit + end if + if (errflg /= 0) then + exit + end if + end do + + do col_start = 1, ncols, 5 + if (errflg /= 0) then + exit + end if + col_end = MIN(col_start + 4, ncols) + + do sind = 1, num_suites + if (errflg /= 0) then + exit + end if + do index = 1, size(test_suites(sind)%suite_parts) + if (errflg /= 0) then + exit + end if + if (errflg == 0) then + call test_host_ccpp_physics_run( & + test_suites(sind)%suite_name, & + test_suites(sind)%suite_parts(index), & + col_start, col_end, errmsg, errflg) + end if + if (errflg /= 0) then + write(6, '(5a)') trim(test_suites(sind)%suite_name), & + '/', trim(test_suites(sind)%suite_parts(index)), & + ': ', trim(errmsg) + exit + end if + end do + end do + end do + + do sind = 1, num_suites + if (errflg /= 0) then + exit + end if + if (errflg == 0) then + call test_host_ccpp_physics_timestep_final( & + test_suites(sind)%suite_name, errmsg, errflg) + end if + if (errflg /= 0) then + write(6, '(3a)') trim(test_suites(sind)%suite_name), ': ', & + trim(errmsg) + exit + end if + end do + + do sind = 1, num_suites + if (errflg /= 0) then + exit + end if + if (errflg == 0) then + call test_host_ccpp_physics_finalize( & + test_suites(sind)%suite_name, errmsg, errflg) + end if + if (errflg /= 0) then + write(6, '(3a)') test_suites(sind)%suite_parts(index), ': ', & + trim(errmsg) + write(6,'(2a)') 'An error occurred in ccpp_timestep_final, ', & + 'Exiting...' + exit + end if + end do + + if (errflg == 0) then + ! Run finished without error, check answers + if (compare_data()) then + write(6, *) 'Answers are correct!' + errflg = 0 + else + write(6, *) 'Answers are not correct!' + errflg = -1 + end if + end if + + retval = errflg == 0 + + end subroutine test_host + + end module test_prog diff --git a/test/nested_suite_test/test_host.meta b/test/nested_suite_test/test_host.meta new file mode 100644 index 00000000..da71b182 --- /dev/null +++ b/test/nested_suite_test/test_host.meta @@ -0,0 +1,38 @@ +[ccpp-table-properties] + name = suite_info + type = ddt +[ccpp-arg-table] + name = suite_info + type = ddt + +[ccpp-table-properties] + name = test_host + type = host +[ccpp-arg-table] + name = test_host + type = host +[ col_start ] + standard_name = horizontal_loop_begin + type = integer + units = count + dimensions = () + protected = True +[ col_end ] + standard_name = horizontal_loop_end + type = integer + units = count + dimensions = () + protected = True +[ errmsg ] + standard_name = ccpp_error_message + long_name = Error message for error handling in CCPP + units = None + dimensions = () + type = character + kind = len=512 +[ errflg ] + standard_name = ccpp_error_code + long_name = Error flag for error handling in CCPP + units = 1 + dimensions = () + type = integer diff --git a/test/nested_suite_test/test_host_data.F90 b/test/nested_suite_test/test_host_data.F90 new file mode 100644 index 00000000..c46bbfff --- /dev/null +++ b/test/nested_suite_test/test_host_data.F90 @@ -0,0 +1,102 @@ +module test_host_data + + use ccpp_kinds, only: kind_phys + use mod_rad_ddt, only: ty_rad_lw, ty_rad_sw + + implicit none + private + + !> \section arg_table_physics_state Argument Table + !! \htmlinclude arg_table_physics_state.html + type physics_state + real(kind_phys), dimension(:,:), allocatable :: & + effrr, & ! effective radius of cloud rain + effrl, & ! effective radius of cloud liquid water + effri, & ! effective radius of cloud ice + effrg, & ! effective radius of cloud graupel + ncg, & ! number concentration of cloud graupel + nci ! number concentration of cloud ice + real(kind_phys) :: scalar_var + type(ty_rad_lw), dimension(:), allocatable :: & + fluxLW ! Longwave radiation fluxes + type(ty_rad_sw) :: & + fluxSW ! Shortwave radiation fluxes + real(kind_phys) :: scalar_varA + real(kind_phys) :: scalar_varB + real(kind_phys) :: tke, tke2 + integer :: scalar_varC + integer :: scheme_order + integer :: num_subcycles + end type physics_state + + public :: physics_state + public :: allocate_physics_state + +contains + + subroutine allocate_physics_state(cols, levels, state, has_graupel, has_ice) + integer, intent(in) :: cols + integer, intent(in) :: levels + type(physics_state), intent(out) :: state + logical, intent(in) :: has_graupel + logical, intent(in) :: has_ice + + if (allocated(state%effrr)) then + deallocate(state%effrr) + end if + allocate(state%effrr(cols, levels)) + + if (allocated(state%effrl)) then + deallocate(state%effrl) + end if + allocate(state%effrl(cols, levels)) + + if (has_ice) then + if (allocated(state%effri)) then + deallocate(state%effri) + end if + allocate(state%effri(cols, levels)) + endif + + if (has_graupel) then + if (allocated(state%effrg)) then + deallocate(state%effrg) + end if + allocate(state%effrg(cols, levels)) + + if (allocated(state%ncg)) then + deallocate(state%ncg) + end if + allocate(state%ncg(cols, levels)) + endif + + if (has_ice) then + if (allocated(state%nci)) then + deallocate(state%nci) + end if + allocate(state%nci(cols, levels)) + endif + + if (allocated(state%fluxLW)) then + deallocate(state%fluxLW) + end if + allocate(state%fluxLW(cols)) + + if (associated(state%fluxSW%sfc_up_sw)) then + nullify(state%fluxSW%sfc_up_sw) + end if + allocate(state%fluxSW%sfc_up_sw(cols)) + + if (associated(state%fluxSW%sfc_down_sw)) then + nullify(state%fluxSW%sfc_down_sw) + end if + allocate(state%fluxSW%sfc_down_sw(cols)) + + ! Initialize scheme counter. + state%scheme_order = 1 + ! Initialize subcycle counter. + state%num_subcycles = 3 + + end subroutine allocate_physics_state + +end module test_host_data diff --git a/test/nested_suite_test/test_host_data.meta b/test/nested_suite_test/test_host_data.meta new file mode 100644 index 00000000..59a0fb4d --- /dev/null +++ b/test/nested_suite_test/test_host_data.meta @@ -0,0 +1,128 @@ +[ccpp-table-properties] + name = physics_state + type = ddt + dependencies = module_rad_ddt.F90 +[ccpp-arg-table] + name = physics_state + type = ddt +[effrr] + standard_name = effective_radius_of_stratiform_cloud_rain_particle + long_name = effective radius of cloud rain particle in meter + units = m + dimensions = (horizontal_dimension,vertical_layer_dimension) + type = real + kind = kind_phys +[effrl] + standard_name = effective_radius_of_stratiform_cloud_liquid_water_particle + long_name = effective radius of cloud liquid water particle in meter + units = m + dimensions = (horizontal_dimension,vertical_layer_dimension) + type = real + kind = kind_phys +[effri] + standard_name = effective_radius_of_stratiform_cloud_ice_particle + long_name = effective radius of cloud ice water particle in meter + units = m + dimensions = (horizontal_dimension,vertical_layer_dimension) + type = real + kind = kind_phys + active = (flag_indicating_cloud_microphysics_has_ice) +[effrg] + standard_name = effective_radius_of_stratiform_cloud_graupel + long_name = effective radius of cloud graupel in meter + units = m + dimensions = (horizontal_dimension,vertical_layer_dimension) + type = real + kind = kind_phys + active = (flag_indicating_cloud_microphysics_has_graupel) +[ncg] + standard_name = cloud_graupel_number_concentration + long_name = number concentration of cloud graupel + units = kg-1 + dimensions = (horizontal_dimension,vertical_layer_dimension) + type = real + kind = kind_phys + intent = in + active = (flag_indicating_cloud_microphysics_has_graupel) +[nci] + standard_name = cloud_ice_number_concentration + long_name = number concentration of cloud ice + units = kg-1 + dimensions = (horizontal_dimension,vertical_layer_dimension) + type = real + kind = kind_phys + intent = in + active = (flag_indicating_cloud_microphysics_has_ice) +[scalar_var] + standard_name = scalar_variable_for_testing + long_name = unused scalar variable + units = m + dimensions = () + type = real + kind = kind_phys +[ tke ] + standard_name = turbulent_kinetic_energy + long_name = turbulent_kinetic_energy + units = J kg-1 + dimensions = () + type = real + kind = kind_phys +[ tke2 ] + standard_name = turbulent_kinetic_energy2 + long_name = turbulent_kinetic_energy2 + units = m2 s-2 + dimensions = () + type = real + kind = kind_phys +[fluxSW] + standard_name = shortwave_radiation_fluxes + long_name = shortwave radiation fluxes + units = W m-2 + dimensions = () + type = ty_rad_sw +[fluxLW] + standard_name = longwave_radiation_fluxes + long_name = longwave radiation fluxes + units = W m-2 + dimensions = (horizontal_dimension) + type = ty_rad_lw +[scalar_varA] + standard_name = scalar_variable_for_testing_a + long_name = unused scalar variable A + units = m + dimensions = () + type = real + kind = kind_phys +[scalar_varB] + standard_name = scalar_variable_for_testing_b + long_name = unused scalar variable B + units = m + dimensions = () + type = real + kind = kind_phys +[scalar_varC] + standard_name = scalar_variable_for_testing_c + long_name = unused scalar variable C + units = m + dimensions = () + type = integer +[scheme_order] + standard_name = scheme_order_in_suite + long_name = scheme order in suite definition file + units = None + dimensions = () + type = integer +[num_subcycles] + standard_name = num_subcycles_for_effr + long_name = Number of times to subcycle the effr calculation + units = None + dimensions = () + type = integer + +[ccpp-table-properties] + name = test_host_data + type = module + dependencies = module_rad_ddt.F90 +[ccpp-arg-table] + name = test_host_data + type = module diff --git a/test/nested_suite_test/test_host_mod.F90 b/test/nested_suite_test/test_host_mod.F90 new file mode 100644 index 00000000..09d1fdb5 --- /dev/null +++ b/test/nested_suite_test/test_host_mod.F90 @@ -0,0 +1,126 @@ +module test_host_mod + + use ccpp_kinds, only: kind_phys + use test_host_data, only: physics_state, allocate_physics_state + + implicit none + public + + !> \section arg_table_test_host_mod Argument Table + !! \htmlinclude arg_table_test_host_host.html + !! + integer, parameter :: ncols = 12 + integer, parameter :: pver = 4 + type(physics_state) :: phys_state + real(kind_phys) :: effrs(ncols, pver) + logical, parameter :: has_ice = .true. + logical, parameter :: has_graupel = .true. + + public :: init_data + public :: compare_data + +contains + + subroutine init_data() + + ! Allocate and initialize state + call allocate_physics_state(ncols, pver, phys_state, has_graupel, has_ice) + phys_state%effrr = 1.0E-3 ! 1000 microns, in meter + phys_state%effrl = 1.0E-4 ! 100 microns, in meter + phys_state%scalar_var = 1.0 ! in m + phys_state%scalar_varA = 273.15 ! in K + phys_state%scalar_varB = 1013.0 ! in mb + phys_state%scalar_varC = 380 ! in ppmv + effrs = 5.0E-4 ! 500 microns, in meter + if (has_graupel) then + phys_state%effrg = 2.5E-4 ! 250 microns, in meter + phys_state%ncg = 40 + endif + if (has_ice) then + phys_state%effri = 5.0E-5 ! 50 microns, in meter + phys_state%nci = 80 + endif + phys_state%tke = 10.0 !J kg-1 + phys_state%tke2 = 42.0 !J kg-1 + + end subroutine init_data + + logical function compare_data() + + real(kind_phys), parameter :: effrr_expected = 1.0E-3 ! 1000 microns, in meter + real(kind_phys), parameter :: effrl_expected = 5.0E-5 ! 50 microns, in meter + real(kind_phys), parameter :: effri_expected = 7.5E-5 ! 75 microns, in meter + real(kind_phys), parameter :: effrs_expected = 5.3E-4 ! 530 microns, in meter + real(kind_phys), parameter :: scalar_expected = 2.0E3 ! 2 km, in meter + real(kind_phys), parameter :: tke_expected = 10.0 ! 10 J kg-1 + real(kind_phys), parameter :: tolerance = 1.0E-6 ! used as scaling factor for expected value + real(kind_phys), parameter :: sfc_up_sw_expected = 100. ! W/m2 + real(kind_phys), parameter :: sfc_down_sw_expected = 400. ! W/m2 + real(kind_phys), parameter :: sfc_up_lw_expected = 300. ! W/m2 + real(kind_phys), parameter :: sfc_down_lw_expected = 50. ! W/m2 + + compare_data = .true. + + if (maxval(abs(phys_state%effrr - effrr_expected)) > tolerance*effrr_expected) then + write(6, '(a,e16.7,a,e16.7)') 'Error: max diff of phys_state%effrr from expected value exceeds tolerance: ', & + maxval(abs(phys_state%effrr - effrr_expected)), ' > ', tolerance*effrr_expected + compare_data = .false. + end if + + if (maxval(abs(phys_state%effrl - effrl_expected)) > tolerance*effrl_expected) then + write(6, '(a,e16.7,a,e16.7)') 'Error: max diff of phys_state%effrl from expected value exceeds tolerance: ', & + maxval(abs(phys_state%effrl - effrl_expected)), ' > ', tolerance*effrl_expected + compare_data = .false. + end if + + if (maxval(abs(phys_state%effri - effri_expected)) > tolerance*effri_expected) then + write(6, '(a,e16.7,a,e16.7)') 'Error: max diff of phys_state%effri from expected value exceeds tolerance: ', & + maxval(abs(phys_state%effri - effri_expected)), ' > ', tolerance*effri_expected + compare_data = .false. + end if + + if (maxval(abs( effrs - effrs_expected)) > tolerance*effrs_expected) then + write(6, '(a,e16.7,a,e16.7)') 'Error: max diff of effrs from expected value exceeds tolerance: ', & + maxval(abs( effrs - effrs_expected)), ' > ', tolerance*effrs_expected + compare_data = .false. + end if + + if (abs( phys_state%scalar_var - scalar_expected) > tolerance*scalar_expected) then + write(6, '(a,e16.7,a,e16.7)') 'Error: max diff of scalar_var from expected value exceeds tolerance: ', & + abs( phys_state%scalar_var - scalar_expected), ' > ', tolerance*scalar_expected + compare_data = .false. + end if + + if (abs( phys_state%tke - tke_expected) > tolerance*tke_expected) then + write(6, '(a,e16.7,a,e16.7)') 'Error: max diff of tke from expected value exceeds tolerance: ', & + abs( phys_state%tke - tke_expected), ' > ', tolerance*tke_expected + compare_data = .false. + end if + + if (maxval(abs( phys_state%fluxSW%sfc_up_sw - sfc_up_sw_expected)) > tolerance*sfc_up_sw_expected) then + write(6, '(a,e16.7,a,e16.7)') 'Error: max diff of sfc_up_sw from expected value exceeds tolerance: ', & + abs( phys_state%fluxSW%sfc_up_sw - sfc_up_sw_expected), ' > ', tolerance*sfc_up_sw_expected + compare_data = .false. + end if + + if (maxval(abs( phys_state%fluxSW%sfc_down_sw - sfc_down_sw_expected)) > tolerance*sfc_down_sw_expected) then + write(6, '(a,e16.7,a,e16.7)') 'Error: max diff of sfc_down_sw from expected value exceeds tolerance: ', & + abs( phys_state%fluxSW%sfc_down_sw - sfc_down_sw_expected), ' > ', tolerance*sfc_down_sw_expected + compare_data = .false. + end if + + if (maxval(abs( phys_state%fluxLW%sfc_up_lw - sfc_up_lw_expected)) > tolerance*sfc_up_lw_expected) then + write(6, '(a,e16.7,a,e16.7)') 'Error: max diff of sfc_up_lw from expected value exceeds tolerance: ', & + abs( phys_state%fluxLW%sfc_up_lw - sfc_up_lw_expected), ' > ', tolerance*sfc_up_lw_expected + compare_data = .false. + end if + + if (maxval(abs( phys_state%fluxLW%sfc_down_lw - sfc_down_lw_expected)) > tolerance*sfc_down_lw_expected) then + write(6, '(a,e16.7,a,e16.7)') 'Error: max diff of sfc_down_lw from expected value exceeds tolerance: ', & + abs( phys_state%fluxLW%sfc_down_lw - sfc_down_lw_expected), ' > ', tolerance*sfc_down_lw_expected + compare_data = .false. + end if + + end function compare_data + +end module test_host_mod diff --git a/test/nested_suite_test/test_host_mod.meta b/test/nested_suite_test/test_host_mod.meta new file mode 100644 index 00000000..51a2f5c3 --- /dev/null +++ b/test/nested_suite_test/test_host_mod.meta @@ -0,0 +1,42 @@ +[ccpp-table-properties] + name = test_host_mod + type = module +[ccpp-arg-table] + name = test_host_mod + type = module +[ ncols] + standard_name = horizontal_dimension + units = count + type = integer + protected = True + dimensions = () +[ pver ] + standard_name = vertical_layer_dimension + units = count + type = integer + protected = True + dimensions = () +[ phys_state ] + standard_name = physics_state_derived_type + long_name = Physics State DDT + type = physics_state + dimensions = () +[effrs] + standard_name = effective_radius_of_stratiform_cloud_snow_particle + long_name = effective radius of cloud snow particle in meter + units = m + dimensions = (horizontal_dimension,vertical_layer_dimension) + type = real + kind = kind_phys +[has_ice] + standard_name = flag_indicating_cloud_microphysics_has_ice + long_name = flag indicating that the cloud microphysics produces ice + units = flag + dimensions = () + type = logical +[has_graupel] + standard_name = flag_indicating_cloud_microphysics_has_graupel + long_name = flag indicating that the cloud microphysics produces graupel + units = flag + dimensions = () + type = logical diff --git a/test/nested_suite_test/test_nested_suite_integration.F90 b/test/nested_suite_test/test_nested_suite_integration.F90 new file mode 100644 index 00000000..09dfea10 --- /dev/null +++ b/test/nested_suite_test/test_nested_suite_integration.F90 @@ -0,0 +1,88 @@ +program test_nested_suite_integration + use test_prog, only: test_host, suite_info, cm, cs + + implicit none + + character(len=cs), target :: test_parts1(3) = (/ & + 'radiation1 ', & + 'rad_lw_group ', & + 'rad_sw_group '/) + + character(len=cm), target :: test_invars1(18) = (/ & + 'effective_radius_of_stratiform_cloud_rain_particle ', & + 'effective_radius_of_stratiform_cloud_liquid_water_particle', & + 'effective_radius_of_stratiform_cloud_snow_particle ', & + 'effective_radius_of_stratiform_cloud_graupel ', & + 'cloud_graupel_number_concentration ', & + 'scalar_variable_for_testing ', & + 'turbulent_kinetic_energy ', & + 'turbulent_kinetic_energy2 ', & + 'scalar_variable_for_testing_a ', & + 'scalar_variable_for_testing_b ', & + 'scalar_variable_for_testing_c ', & + 'scheme_order_in_suite ', & + 'num_subcycles_for_effr ', & + 'flag_indicating_cloud_microphysics_has_graupel ', & + 'flag_indicating_cloud_microphysics_has_ice ', & + 'surface_downwelling_shortwave_radiation_flux ', & + 'surface_upwelling_shortwave_radiation_flux ', & + 'longwave_radiation_fluxes '/) + + character(len=cm), target :: test_outvars1(14) = (/ & + 'ccpp_error_code ', & + 'ccpp_error_message ', & + 'effective_radius_of_stratiform_cloud_ice_particle ', & + 'effective_radius_of_stratiform_cloud_liquid_water_particle', & + 'effective_radius_of_stratiform_cloud_rain_particle ', & + 'effective_radius_of_stratiform_cloud_snow_particle ', & + 'cloud_ice_number_concentration ', & + 'scalar_variable_for_testing ', & + 'scheme_order_in_suite ', & + 'surface_downwelling_shortwave_radiation_flux ', & + 'surface_upwelling_shortwave_radiation_flux ', & + 'turbulent_kinetic_energy ', & + 'turbulent_kinetic_energy2 ', & + 'longwave_radiation_fluxes '/) + + character(len=cm), target :: test_reqvars1(22) = (/ & + 'ccpp_error_code ', & + 'ccpp_error_message ', & + 'effective_radius_of_stratiform_cloud_rain_particle ', & + 'effective_radius_of_stratiform_cloud_ice_particle ', & + 'effective_radius_of_stratiform_cloud_liquid_water_particle', & + 'effective_radius_of_stratiform_cloud_snow_particle ', & + 'effective_radius_of_stratiform_cloud_graupel ', & + 'cloud_graupel_number_concentration ', & + 'cloud_ice_number_concentration ', & + 'scalar_variable_for_testing ', & + 'turbulent_kinetic_energy ', & + 'turbulent_kinetic_energy2 ', & + 'scalar_variable_for_testing_a ', & + 'scalar_variable_for_testing_b ', & + 'scalar_variable_for_testing_c ', & + 'scheme_order_in_suite ', & + 'num_subcycles_for_effr ', & + 'flag_indicating_cloud_microphysics_has_graupel ', & + 'flag_indicating_cloud_microphysics_has_ice ', & + 'surface_downwelling_shortwave_radiation_flux ', & + 'surface_upwelling_shortwave_radiation_flux ', & + 'longwave_radiation_fluxes '/) + + type(suite_info) :: test_suites(1) + logical :: run_okay + + ! Setup expected test suite info + test_suites(1)%suite_name = 'main_suite' + test_suites(1)%suite_parts => test_parts1 + test_suites(1)%suite_input_vars => test_invars1 + test_suites(1)%suite_output_vars => test_outvars1 + test_suites(1)%suite_required_vars => test_reqvars1 + + call test_host(run_okay, test_suites) + + if (run_okay) then + STOP 0 + else + STOP -1 + end if +end program test_nested_suite_integration From 26bba86ba5d4c021b1bd26bc621f9ba0630b946e Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Tue, 7 Oct 2025 12:29:31 -0600 Subject: [PATCH 03/12] Add doctests to scripts/parse_tools/xml_tools.py, clean up scripts/ccpp_suite.py, scripts/parse_tools/xml_tools.py, and test/nested_suite_test/test_host.F90 --- scripts/ccpp_suite.py | 4 - scripts/parse_tools/xml_tools.py | 300 ++++++++++++++++++++------- test/nested_suite_test/test_host.F90 | 2 - 3 files changed, 228 insertions(+), 78 deletions(-) diff --git a/scripts/ccpp_suite.py b/scripts/ccpp_suite.py index d19cd712..58eadaf9 100644 --- a/scripts/ccpp_suite.py +++ b/scripts/ccpp_suite.py @@ -194,10 +194,6 @@ def parse(self, suite_xml, run_env): self.__context = ParseContext(filename=self.__sdf_name) self.__name = suite_xml.get('name') self.__module = 'ccpp_{}_cap'.format(self.name) - lmsg = "Reading suite definition file for '{}'" - if run_env.logger and run_env.logger.isEnabledFor(logging.INFO): - run_env.logger.info(lmsg.format(self.name)) - # end if gname = Suite.__register_group_name self.__suite_reg_group = self.new_group_from_name(gname, run_env) gname = Suite.__initial_group_name diff --git a/scripts/parse_tools/xml_tools.py b/scripts/parse_tools/xml_tools.py index 15e93cdb..69c27131 100644 --- a/scripts/parse_tools/xml_tools.py +++ b/scripts/parse_tools/xml_tools.py @@ -213,7 +213,6 @@ def validate_xml_file(filename, schema_root, version, logger, logger.debug("Checking file {} against schema {}".format(filename, schema_file)) cmd = [_XMLLINT, '--noout', '--schema', schema_file, filename] - logger.debug(f"Executing command '{cmd}'") result = call_command(cmd, logger) return result # end if @@ -244,16 +243,62 @@ def read_xml_file(filename, logger=None): raise CCPPError(emsg.format(filename)) # end if if logger: - logger.debug("Read XML file, '{}'".format(filename)) + logger.debug(f"Reading XML file {filename}") # end if return tree, root ############################################################################### def load_suite_by_name(suite_name, group_name, main_root, file=None, logger=None): ############################################################################### - """Load a suite by its name, or a group of a suite by the suite - and group names. If the optional file argument is provided, look - for the object in that file, otherwise search the current main_root.""" + """ + Load a suite by its name, or a group of a suite by the suite and group names. + If the optional file argument is provided, look for the object in that file, + otherwise search the current main_root. + + Parameters: + suite_name (str): The name of the suite to find. + group_name (str or None): The name of the group to find within the suite. + main_root (xml.etree.ElementTree.Element): The XML root to search if no file is given. + file (str, optional): The path to an XML file to read and search instead of main_root. + logger (logging.Logger, optional): Logger for warnings/errors. + + Returns: + xml.etree.ElementTree.Element: The matching suite or group element. + + Raises: + CCPPError: If the suite or group is not found, or if the schema is invalid. + + Examples: + >>> import xml.etree.ElementTree as ET + >>> from types import SimpleNamespace + >>> def read_xml_file(file, logger=None): + ... return None, ET.fromstring(file) + >>> def find_schema_version(root): + ... return (2, 0) + >>> def validate_xml_file(file, kind, schema_version, logger=None): + ... return True + >>> xml_content = ''' + ... + ... + ... + ... + ... + ... + ... ''' + >>> root = ET.fromstring(xml_content) + >>> load_suite_by_name("physics_suite", None, root).tag + 'suite' + >>> load_suite_by_name("physics_suite", "dynamics", root).attrib['name'] + 'dynamics' + >>> load_suite_by_name("physics_suite", "missing_group", root) #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + CCPPError: Nested suite physics_suite, group missing_group, not found + >>> load_suite_by_name("missing_suite", None, root) #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + CCPPError: Nested suite missing_suite not found + """ if file: _, root = read_xml_file(file, logger) schema_version = find_schema_version(root) @@ -266,7 +311,6 @@ def load_suite_by_name(suite_name, group_name, main_root, file=None, logger=None else: root = main_root for suite in root.findall("suite"): - print("ABC: {suite.attrib.get('name')}") if suite.attrib.get("name") == suite_name: if group_name: for group in suite.findall("group"): @@ -274,15 +318,179 @@ def load_suite_by_name(suite_name, group_name, main_root, file=None, logger=None return group else: return suite - emsg = f"Nested suite {suite_name}" + (f", group {group_name}," if group_name else "") \ + emsg = f"Nested suite {suite_name}" \ + + (f", group {group_name}," if group_name else "") \ + " not found" + (f" in file {file}" if file else "") raise CCPPError(emsg) +############################################################################### +def replace_nested_suite(element, nested_suite, root, logger): +############################################################################### + """ + Replace a tag with the actual suite or group it references. + + This function looks up a referenced suite or suite group from the main XML tree + or an external file (if specified), deep copies its children, and replaces the + element in the parent `element` with the copied contents. + + If the nested suite being inserted contains its own elements and + within the same external file, the `file` attribute is propagated into those. + + Parameters: + element (xml.etree.ElementTree.Element): The parent element containing the nested suite. + nested_suite (xml.etree.ElementTree.Element): The element to be replaced. + root (xml.etree.ElementTree.Element): The root XML element (used when no file is specified). + logger (logging.Logger or None): Logger to record debug information. + + Returns: + str or None: The name of the suite if the nested suite came from the root XML element + (to delete it later), or None if it came from a separate file. + + Example: + >>> import xml.etree.ElementTree as ET + >>> from types import SimpleNamespace + >>> logger = SimpleNamespace() + >>> logger.debug = print + >>> xml = ''' + ... + ... + ... + ... my_scheme + ... + ... + ... + ... + ... + ... + ... ''' + >>> tree = ET.ElementTree(ET.fromstring(xml)) + >>> root = tree.getroot() + >>> top_suite = root.find("suite[@name='top']") + >>> nested = top_suite.find("nested_suite") + >>> replace_nested_suite(top_suite, nested, root, logger) + Expanded nested suite 'my_suite' + 'my_suite' + >>> [child.tag for child in top_suite] + ['group'] + >>> top_suite.find("group").find("scheme").text + 'my_scheme' + >>> xml = ''' + ... + ... + ... + ... my_scheme + ... + ... + ... + ... + ... + ... + ... + ... + ... ''' + >>> tree = ET.ElementTree(ET.fromstring(xml)) + >>> root = tree.getroot() + >>> top_suite = root.find("suite[@name='top']") + >>> top_group = top_suite.find("group") + >>> nested = top_group.find("nested_suite") + >>> replace_nested_suite(top_group, nested, root, logger) + Expanded nested suite 'my_suite', group 'my_group' + 'my_suite' + >>> [child.tag for child in top_suite] + ['group'] + >>> top_suite.find("group").find("scheme").text + 'my_scheme' + """ + suite_name = nested_suite.attrib.get("name") + group_name = nested_suite.attrib.get("group") + file = nested_suite.attrib.get("file") + referenced_suite = load_suite_by_name(suite_name, group_name, root, + file=file, logger=logger) + # Deep copy to avoid modifying the original + imported_content = [ET.fromstring(ET.tostring(child)) + for child in referenced_suite] + # Swap nested suite with imported content + for item in imported_content: + # If the imported content comes from a separate file and has + # nested suites that are within that separate file, then we + # need to inject the file attribute here. + if item.tag == "nested_suite": + if file and not item.attrib.get("file"): + item.set("file", file) + element.insert(list(element).index(nested_suite), item) + element.remove(nested_suite) + if logger: + msg = f"Expanded nested suite '{suite_name}'" \ + + (f", group '{group_name}'," if group_name else "") \ + + (f" in file '{file}'" if file else "") + logger.debug(msg.rstrip(',')) + # If the nested suite resides in the same file as the root + # element then we need to remove it + return suite_name if not file else None + ############################################################################### def expand_nested_suites(root, logger=None): ############################################################################### - """Iterate over the root element until all nested suites (single, double, - triple, ...) are replaced with the actual content of the nested suite.""" + """ + Recursively expand all elements within the XML elements. + + This function finds elements within or elements, + and replaces them with the corresponding content from another suite. The replacement + is done in memory using the `replace_nested_suite` function (defined elsewhere). + Nested suites from the same XML root are removed after expansion. + + This operation is recursive and will continue expanding until no + elements remain. + + Parameters: + root (xml.etree.ElementTree.Element): The root element containing elements. + logger (logging.Logger, optional): Logger for debug messages. + + Returns: + None. The XML tree is modified in place. + + Example: + >>> import xml.etree.ElementTree as ET + >>> from types import SimpleNamespace + >>> logger = SimpleNamespace() + >>> logger.debug = print + >>> xml = ''' + ... + ... + ... + ... + ... + ... + ... + ... + ... + ... cloud_scheme + ... + ... + ... + ... + ... pbl_scheme + ... + ... + ... + ... ''' + >>> root = ET.fromstring(xml) + >>> expand_nested_suites(root, logger) + Expanded nested suite 'microphysics_suite', group 'micro' + Expanded nested suite 'pbl_suite' + Removed nested suite 'microphysics_suite' from root element + Removed nested suite 'pbl_suite' from root element + >>> len(root.findall("suite")) # Only one suite left + 1 + >>> suite = root.find("suite") + >>> suite.attrib.get("name") + 'physics_suite' + >>> group = suite.find("group") + >>> group.attrib.get("name") + 'main' + >>> group.find("scheme").text + 'cloud_scheme' + """ # Keep track of any nested suites defined under the same root # that need to be removed at the end of this function. # This happens all in memory, it does not alter files on disk. @@ -297,75 +505,23 @@ def expand_nested_suites(root, logger=None): for group in groups: nested_suites = group.findall("nested_suite") for nested in nested_suites: - suite_name = nested.attrib.get("name") - group_name = nested.attrib.get("group") - file = nested.attrib.get("file") - # This check is redundant, because the XML schema ensures - # that nested_suite elements inside a group have a group name - if not group_name: - CCPPError(f"Required attribute group not found for nested suite {suite_name}") - referenced_suite = load_suite_by_name(suite_name, group_name, root, - file=file, logger=logger) - # Deep copy to avoid modifying the original - imported_content = [ET.fromstring(ET.tostring(child)) for child in referenced_suite] - # Swap nested suite with imported content - for item in imported_content: - # If the imported content comes from a separate file and has - # nested suites that are within that separate file, then we - # need to inject the file attribute here. - if item.tag == "nested_suite": - if file and not item.attrib.get("file"): - item.set("file", file) - group.insert(list(group).index(nested), item) - group.remove(nested) - # Need another pass over the root element - keep_expanding = True - # If the nested suite resides in the same file, remove it - if not file: + suite_name = replace_nested_suite(group, nested, root, logger) + if suite_name: expanded_suites_to_remove.append(suite_name) - if logger: - msg = f"Expanded nested suite '{suite_name}', group '{group_name}'" - if file: - msg += f", in file '{file}'" - logger.debug(msg) + # Trigger another pass over the root element + keep_expanding = True # Second, search all suites for nested_suite elements nested_suites = suite.findall("nested_suite") for nested in nested_suites: - suite_name = nested.attrib.get("name") - group_name = nested.attrib.get("group") - # This check is redundant, because the XML schema ensures - # that nested_suite elements at the suite level have no group name - if group_name: - CCPPError("Nested suite {suite_name} cannot have attribute group") - file = nested.attrib.get("file") - referenced_suite = load_suite_by_name(suite_name, group_name, root, - file=file, logger=logger) - # Deep copy to avoid modifying the original - imported_content = [ET.fromstring(ET.tostring(child)) for child in referenced_suite] - # Swap nested suite with imported content - for item in imported_content: - # If the imported content comes from a separate file and has - # nested suites that are within that separate file, then we - # need to inject the file attribute here. - if item.tag == "nested_suite": - if file and not item.attrib.get("file"): - item.set("file", file) - suite.insert(list(suite).index(nested), item) - suite.remove(nested) - # Need another pass over the root element - keep_expanding = True - # If the nested suite resides in the same file, remove it - if not file: + suite_name = replace_nested_suite(suite, nested, root, logger) + if suite_name: expanded_suites_to_remove.append(suite_name) - if logger: - msg = f"Expanded nested suite '{suite_name}'" - if file: - msg += f" in file '{file}'" - logger.debug(msg) - + # Trigger another pass over the root element + keep_expanding = True # Remove expanded suites + expanded_suites_to_remove = list(set(expanded_suites_to_remove)) for suite in root.findall("suite"): - suite_name = suite.attrib["name"] + suite_name = suite.attrib.get("name") if suite_name in expanded_suites_to_remove: root.remove(suite) if logger: @@ -404,6 +560,6 @@ def remove_whitespace_nodes(node): # Tell everyone! if logger: - logger.debug(f"Wrote {root} to {file_path}") + logger.debug(f"Writing XML file {file_path}") ############################################################################## diff --git a/test/nested_suite_test/test_host.F90 b/test/nested_suite_test/test_host.F90 index f6d8059c..f3a389e8 100644 --- a/test/nested_suite_test/test_host.F90 +++ b/test/nested_suite_test/test_host.F90 @@ -43,8 +43,6 @@ logical function check_suite(test_suite) ! First, check the suite parts call ccpp_physics_suite_part_list(test_suite%suite_name, test_list, & errmsg, errflg) - print *, "DH DEBUG test_list:" - print *, test_list if (errflg == 0) then check = check_list(test_list, test_suite%suite_parts, 'part names', & suite_name=test_suite%suite_name) From 7821cef04f1291c54b8f5a23368b133024f60e14 Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Tue, 7 Oct 2025 12:42:26 -0600 Subject: [PATCH 04/12] More cleanups --- scripts/ccpp_suite.py | 3 +- scripts/parse_tools/xml_tools.py | 8 +- .../nested_suite_test/ccpp_main_suite_cap.F90 | 1060 ----------------- test/nested_suite_test/main_suite.xml | 2 +- test/nested_suite_test/radiation2_suite.xml | 2 +- 5 files changed, 7 insertions(+), 1068 deletions(-) delete mode 100644 test/nested_suite_test/ccpp_main_suite_cap.F90 diff --git a/scripts/ccpp_suite.py b/scripts/ccpp_suite.py index 58eadaf9..fef9f393 100644 --- a/scripts/ccpp_suite.py +++ b/scripts/ccpp_suite.py @@ -680,7 +680,7 @@ def __init__(self, sdfs, host_model, scheme_headers, run_env): if not res: raise CCPPError(f"Invalid suite definition file, '{sdf}'") - # Processing the sdf depends on the schema version + # Processing of the sdf depends on the schema version if xml_root.tag.lower() == "suite" and schema_version[0] == 1: suite = Suite(sdf, xml_root, self, run_env) suite.analyze(self.host_model, scheme_library, @@ -692,6 +692,7 @@ def __init__(self, sdfs, host_model, scheme_headers, run_env): # Write the expanded sdf to the capgen output directory; # this file isn't used by capgen (everything is in memory # from here onwards), but it is useful for developers/users + # (although the output can also be found in the datatable). sdf_expanded = os.path.join(run_env.output_dir, os.path.split(sdf)[1].replace(".xml", "_expanded.xml")) write_xml_file(xml_root, sdf_expanded, run_env.logger) diff --git a/scripts/parse_tools/xml_tools.py b/scripts/parse_tools/xml_tools.py index 69c27131..ed77cd84 100644 --- a/scripts/parse_tools/xml_tools.py +++ b/scripts/parse_tools/xml_tools.py @@ -406,7 +406,6 @@ def replace_nested_suite(element, nested_suite, root, logger): file = nested_suite.attrib.get("file") referenced_suite = load_suite_by_name(suite_name, group_name, root, file=file, logger=logger) - # Deep copy to avoid modifying the original imported_content = [ET.fromstring(ET.tostring(child)) for child in referenced_suite] # Swap nested suite with imported content @@ -435,9 +434,8 @@ def expand_nested_suites(root, logger=None): Recursively expand all elements within the XML elements. This function finds elements within or elements, - and replaces them with the corresponding content from another suite. The replacement - is done in memory using the `replace_nested_suite` function (defined elsewhere). - Nested suites from the same XML root are removed after expansion. + and replaces them with the corresponding content from another suite. Nested + suites from the same XML root are removed after expansion. This operation is recursive and will continue expanding until no elements remain. @@ -531,7 +529,7 @@ def expand_nested_suites(root, logger=None): ############################################################################### def write_xml_file(root, file_path, logger=None): ############################################################################### - """Pretty-prints an ElementTree to an ASCII file using xml.dom.minidom""" + """Pretty-prints element root to an ASCII file using xml.dom.minidom""" def remove_whitespace_nodes(node): """Helper function to recursively remove all text nodes that contain diff --git a/test/nested_suite_test/ccpp_main_suite_cap.F90 b/test/nested_suite_test/ccpp_main_suite_cap.F90 deleted file mode 100644 index 4d1c1d00..00000000 --- a/test/nested_suite_test/ccpp_main_suite_cap.F90 +++ /dev/null @@ -1,1060 +0,0 @@ -! -! This work (Common Community Physics Package Framework), identified by -! NOAA, NCAR, CU/CIRES, is free of known copyright restrictions and is -! placed in the public domain. -! -! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -! THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -! IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -! CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -!> -!! @brief Auto-generated CCPP Suite Cap for main_suite -!! -! -module ccpp_main_suite_cap - - use ccpp_kinds - use ccpp_constituent_prop_mod, only: ccpp_constituent_properties_t - - implicit none - private - - ! Suite interfaces - - character(len=16) :: ccpp_suite_state = 'uninitialized' - - public :: main_suite_register - public :: main_suite_initialize - public :: main_suite_timestep_initial - public :: main_suite_radiation1 - public :: main_suite_rad_lw_group - public :: main_suite_rad_sw_group - public :: main_suite_timestep_final - public :: main_suite_finalize - ! Public interfaces for handling constituents - ! Return the number of constituents for this suite - public :: main_suite_constituents_num_consts - ! Return the name of a constituent - public :: main_suite_constituents_const_name - ! Copy the data for a constituent - public :: main_suite_constituents_copy_const - ! Private constituent module data - logical, private :: ccpp_constituents_initialized = .false. - ! Private interface for constituents - private :: ccpp_create_constituent_array - - ! Private suite variables - -CONTAINS - - subroutine main_suite_register(errflg, errmsg) - - - ! Dummy arguments - integer, intent(out) :: errflg - character(len=512), intent(out) :: errmsg - - ! Initialize ccpp error handling - errflg = 0 - errmsg = '' - - ! Output threaded region check -#ifdef _OPENMP - if (omp_get_thread_num() > 1) then - errflg = 1 - errmsg = "Cannot call register routine from a threaded region" - return - end if -#endif - ! Check state machine - if (trim(ccpp_suite_state) /= 'uninitialized') then - errflg = 1 - write(errmsg, '(3a)') "Invalid initial CCPP state, '", trim(ccpp_suite_state), & - "' in main_suite_register" - return - end if - ! Set horizontal loop extent - - ! Allocate local arrays - ! Suite state does not change - - end subroutine main_suite_register - - ! ======================================================================== - - - subroutine main_suite_initialize(errflg, errmsg, scheme_order) - - use effr_calc, only: effr_calc_init - use effr_diag, only: effr_diag_init - use effr_post, only: effr_post_init - use mod_effr_pre, only: effr_pre_init - - ! Dummy arguments - integer, intent(out) :: errflg - character(len=512), intent(out) :: errmsg - integer, intent(inout) :: scheme_order - - ! Local Variables - type(integer) :: internal_var_integer - - ! Initialize ccpp error handling - errflg = 0 - errmsg = '' - - ! Output threaded region check -#ifdef _OPENMP - if (omp_get_thread_num() > 1) then - errflg = 1 - errmsg = "Cannot call initialize routine from a threaded region" - return - end if -#endif - ! Check state machine - if (trim(ccpp_suite_state) /= 'uninitialized') then - errflg = 1 - write(errmsg, '(3a)') "Invalid initial CCPP state, '", trim(ccpp_suite_state), & - "' in main_suite_initialize" - return - end if - ! Set horizontal loop extent - - ! Allocate local arrays - - ! Allocate suite_vars - - if (errflg == 0) then - ! ################################################################## - ! Begin debug tests - ! ################################################################## - - ! Assign value of scheme_order to internal_var_integer - internal_var_integer = scheme_order - - ! ################################################################## - ! End debug tests - ! ################################################################## - - - - ! Call scheme - call effr_pre_init(scheme_order=scheme_order, errmsg=errmsg, errflg=errflg) - - - - end if - - if (errflg == 0) then - ! ################################################################## - ! Begin debug tests - ! ################################################################## - - ! Assign value of scheme_order to internal_var_integer - internal_var_integer = scheme_order - - ! ################################################################## - ! End debug tests - ! ################################################################## - - - - ! Call scheme - call effr_calc_init(scheme_order=scheme_order, errmsg=errmsg, errflg=errflg) - - - - end if - - if (errflg == 0) then - ! ################################################################## - ! Begin debug tests - ! ################################################################## - - ! Assign value of scheme_order to internal_var_integer - internal_var_integer = scheme_order - - ! ################################################################## - ! End debug tests - ! ################################################################## - - - - ! Call scheme - call effr_post_init(scheme_order=scheme_order, errmsg=errmsg, errflg=errflg) - - - - end if - - if (errflg == 0) then - ! ################################################################## - ! Begin debug tests - ! ################################################################## - - ! Assign value of scheme_order to internal_var_integer - internal_var_integer = scheme_order - - ! ################################################################## - ! End debug tests - ! ################################################################## - - - - ! Call scheme - call effr_diag_init(scheme_order=scheme_order, errmsg=errmsg, errflg=errflg) - - - - end if - ccpp_suite_state = 'initialized' - - end subroutine main_suite_initialize - - ! ======================================================================== - - - subroutine main_suite_timestep_initial(errflg, errmsg) - - - ! Dummy arguments - integer, intent(out) :: errflg - character(len=512), intent(out) :: errmsg - - ! Initialize ccpp error handling - errflg = 0 - errmsg = '' - - ! Output threaded region check -#ifdef _OPENMP - if (omp_get_thread_num() > 1) then - errflg = 1 - errmsg = "Cannot call timestep_initial routine from a threaded region" - return - end if -#endif - ! Check state machine - if (trim(ccpp_suite_state) /= 'initialized') then - errflg = 1 - write(errmsg, '(3a)') "Invalid initial CCPP state, '", trim(ccpp_suite_state), & - "' in main_suite_timestep_initial" - return - end if - ! Set horizontal loop extent - - ! Allocate local arrays - - ! Allocate suite_vars - ccpp_suite_state = 'in_time_step' - - end subroutine main_suite_timestep_initial - - ! ======================================================================== - - - subroutine main_suite_radiation1(num_subcycles, errflg, errmsg, ncols, pver, effrr_inout, & - scalar_var, has_graupel, effrg_in, ncg_in, has_ice, nci_out, effrl_inout, effri_out, & - effrs_inout, col_start, col_end, scalar_var1, tke_inout, tke2_inout, scalar_var2, & - scalar_var3) - - use effr_calc, only: effr_calc_run - use effr_diag, only: effr_diag_run - use effr_post, only: effr_post_run - use effrs_calc, only: effrs_calc_run - use mod_effr_pre, only: effr_pre_run - - ! Dummy arguments - integer, intent(in) :: num_subcycles - integer, intent(out) :: errflg - character(len=512), intent(out) :: errmsg - integer, intent(in) :: ncols - integer, intent(in) :: pver - real(kind_phys), intent(inout) :: effrr_inout(:,:) - real(kind_phys), intent(in) :: scalar_var - logical, intent(in) :: has_graupel - real(kind_phys), intent(in), target, optional :: effrg_in(:,:) - real(kind_phys), intent(in), target, optional :: ncg_in(:,:) - logical, intent(in) :: has_ice - real(kind_phys), intent(out), target, optional :: nci_out(:,:) - real(kind_phys), intent(inout) :: effrl_inout(:,:) - real(kind_phys), intent(out), target, optional :: effri_out(:,:) - real(8), intent(inout) :: effrs_inout(:,:) - integer, intent(in) :: col_start - integer, intent(in) :: col_end - real(kind_phys), intent(inout) :: scalar_var1 - real(kind_phys), intent(inout) :: tke_inout - real(kind_phys), intent(inout) :: tke2_inout - real(kind_phys), intent(in) :: scalar_var2 - integer, intent(in) :: scalar_var3 - - ! Local Variables - integer :: loop0_num_subcycles_for_effr - type(real) :: internal_var_real - integer :: loop1 - integer :: loop2 - integer :: ncol - integer :: internal_var_integer - real(kind_phys) :: internal_var_real_kind_phys - logical :: internal_var_logical - real(kind_phys) :: scalar_var_local - real(kind_phys), allocatable :: effrr_in_local(:,:) - real(kind_phys), allocatable, target :: effrg_in_local(:,:) - real(kind_phys), allocatable :: effrl_inout_local(:,:) - real(kind_phys), allocatable, target :: effri_out_local(:,:) - real(8), allocatable :: effrs_inout_local(:,:) - real(kind_phys), pointer :: effrg_in_ptr(:,:) => null() - real(kind_phys), pointer :: ncg_in_ptr(:,:) => null() - real(kind_phys), pointer :: nci_out_ptr(:,:) => null() - real(kind_phys), pointer :: effri_out_ptr(:,:) => null() - real(kind_phys), pointer :: ncl_out_ptr(:,:) => null() - - ! Initialize ccpp error handling - errflg = 0 - errmsg = '' - - ! Check state machine - if (trim(ccpp_suite_state) /= 'in_time_step') then - errflg = 1 - write(errmsg, '(3a)') "Invalid initial CCPP state, '", trim(ccpp_suite_state), & - "' in main_suite_radiation1" - return - end if - ! Set horizontal loop extent - ncol = col_end - col_start + 1 - - ! Allocate local arrays - allocate(effrg_in_local(1:ncol, 1:pver)) - allocate(effri_out_local(1:ncol, 1:pver)) - allocate(effrl_inout_local(1:ncol, 1:pver)) - allocate(effrr_in_local(1:ncol, 1:pver)) - allocate(effrs_inout_local(1:ncol, 1:pver)) - do loop0_num_subcycles_for_effr = 1, num_subcycles - - if (errflg == 0) then - ! ################################################################## - ! Begin debug tests - ! ################################################################## - - ! Check size of array effrr_inout - if (size(effrr_inout(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_pre_run: for array effrr_inout, expected size ',& - 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effrr_inout) - errflg = 1 - return - end if - - ! Check length of effrr_inout(:,1) - if (size(effrr_inout(:,1)) /= ncol-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_pre_run: for array effrr_inout(:,1), expected size ', ncol-1+1,& - ' but got ', size(effrr_inout(:,1)) - errflg = 1 - return - end if - ! Check length of effrr_inout(1,:) - if (size(effrr_inout(1,:)) /= pver-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_pre_run: for array effrr_inout(1,:), expected size ', pver-1+1,& - ' but got ', size(effrr_inout(1,:)) - errflg = 1 - return - end if - - ! Assign value of scalar_var to internal_var_real - internal_var_real = scalar_var - - ! ################################################################## - ! End debug tests - ! ################################################################## - - - - ! Call scheme - call effr_pre_run(effrr_inout=effrr_inout, scalar_var=scalar_var, errmsg=errmsg, & - errflg=errflg) - - - - end if - do loop1 = 1, 2 - do loop2 = 1, 2 - - if (errflg == 0) then - ! ################################################################## - ! Begin debug tests - ! ################################################################## - - ! Assign value of pver to internal_var_integer - internal_var_integer = pver - - ! Check size of array effrr_inout - if (size(effrr_inout(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_calc_run: for array effrr_inout, expected size ',& - 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effrr_inout) - errflg = 1 - return - end if - - ! Check length of effrr_inout(:,1) - if (size(effrr_inout(:,1)) /= ncol-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_calc_run: for array effrr_inout(:,1), expected size ',& - ncol-1+1, ' but got ', size(effrr_inout(:,1)) - errflg = 1 - return - end if - ! Check length of effrr_inout(1,:) - if (size(effrr_inout(1,:)) /= pver-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_calc_run: for array effrr_inout(1,:), expected size ',& - pver-1+1, ' but got ', size(effrr_inout(1,:)) - errflg = 1 - return - end if - - if (has_graupel) then - ! Check size of array effrg_in - if (size(effrg_in(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_calc_run: for array effrg_in, expected size ',& - 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effrg_in) - errflg = 1 - return - end if - end if - - if (has_graupel) then - ! Check length of effrg_in(:,1) - if (size(effrg_in(:,1)) /= ncol-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_calc_run: for array effrg_in(:,1), expected size ',& - ncol-1+1, ' but got ', size(effrg_in(:,1)) - errflg = 1 - return - end if - ! Check length of effrg_in(1,:) - if (size(effrg_in(1,:)) /= pver-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_calc_run: for array effrg_in(1,:), expected size ',& - pver-1+1, ' but got ', size(effrg_in(1,:)) - errflg = 1 - return - end if - end if - - if (has_graupel) then - ! Check size of array ncg_in - if (size(ncg_in(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_calc_run: for array ncg_in, expected size ',& - 1*(ncol-1+1)*(pver-1+1), ' but got ', size(ncg_in) - errflg = 1 - return - end if - end if - - if (has_graupel) then - ! Check length of ncg_in(:,1) - if (size(ncg_in(:,1)) /= ncol-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_calc_run: for array ncg_in(:,1), expected size ',& - ncol-1+1, ' but got ', size(ncg_in(:,1)) - errflg = 1 - return - end if - ! Check length of ncg_in(1,:) - if (size(ncg_in(1,:)) /= pver-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_calc_run: for array ncg_in(1,:), expected size ',& - pver-1+1, ' but got ', size(ncg_in(1,:)) - errflg = 1 - return - end if - end if - - if (has_ice) then - ! Check size of array nci_out - if (size(nci_out(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_calc_run: for array nci_out, expected size ',& - 1*(ncol-1+1)*(pver-1+1), ' but got ', size(nci_out) - errflg = 1 - return - end if - end if - - ! Check size of array effrl_inout - if (size(effrl_inout(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_calc_run: for array effrl_inout, expected size ',& - 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effrl_inout) - errflg = 1 - return - end if - - ! Check length of effrl_inout(:,1) - if (size(effrl_inout(:,1)) /= ncol-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_calc_run: for array effrl_inout(:,1), expected size ',& - ncol-1+1, ' but got ', size(effrl_inout(:,1)) - errflg = 1 - return - end if - ! Check length of effrl_inout(1,:) - if (size(effrl_inout(1,:)) /= pver-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_calc_run: for array effrl_inout(1,:), expected size ',& - pver-1+1, ' but got ', size(effrl_inout(1,:)) - errflg = 1 - return - end if - - if (has_ice) then - ! Check size of array effri_out - if (size(effri_out(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_calc_run: for array effri_out, expected size ',& - 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effri_out) - errflg = 1 - return - end if - end if - - ! Check size of array effrs_inout - if (size(effrs_inout(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_calc_run: for array effrs_inout, expected size ',& - 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effrs_inout) - errflg = 1 - return - end if - - ! Check length of effrs_inout(:,1) - if (size(effrs_inout(:,1)) /= ncol-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_calc_run: for array effrs_inout(:,1), expected size ',& - ncol-1+1, ' but got ', size(effrs_inout(:,1)) - errflg = 1 - return - end if - ! Check length of effrs_inout(1,:) - if (size(effrs_inout(1,:)) /= pver-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_calc_run: for array effrs_inout(1,:), expected size ',& - pver-1+1, ' but got ', size(effrs_inout(1,:)) - errflg = 1 - return - end if - - ! Assign value of has_graupel to internal_var_logical - internal_var_logical = has_graupel - - ! Assign value of scalar_var1 to internal_var_real - internal_var_real = scalar_var1 - - ! Assign value of tke_inout to internal_var_real - internal_var_real = tke_inout - - ! Assign value of tke2_inout to internal_var_real - internal_var_real = tke2_inout - - ! ################################################################## - ! End debug tests - ! ################################################################## - - ! Compute reverse (pre-scheme) transforms - effrr_in_local(:,1:pver) = 1.0E+6_kind_phys*effrr_inout(:,pver:1:-1) - effrg_in_local(:,1:pver) = 1.0E+6_kind_phys*effrg_in(:,1:pver) - effrl_inout_local(:,1:pver) = 1.0E+6_kind_phys*effrl_inout(:,1:pver) - effrs_inout_local(:,1:pver) = 1.0E+6_8*real(effrs_inout(:,pver:1:-1), 8) - scalar_var_local = 1.0E-3_kind_phys*scalar_var1 - - ! Associate conditional variables - if (has_graupel) then - effrg_in_ptr => effrg_in_local - end if - if (has_graupel) then - ncg_in_ptr => ncg_in - end if - if (has_ice) then - nci_out_ptr => nci_out - end if - if (has_ice) then - effri_out_ptr => effri_out_local - end if - - ! Call scheme - call effr_calc_run(ncol=ncol, nlev=pver, effrr_in=effrr_in_local, & - effrg_in=effrg_in_local, ncg_in=ncg_in_ptr, nci_out=nci_out_ptr, & - effrl_inout=effrl_inout_local, effri_out=effri_out_ptr, & - effrs_inout=effrs_inout_local, ncl_out=ncl_out_ptr, & - has_graupel=has_graupel, scalar_var=scalar_var_local, & - tke_inout=tke_inout, tke2_inout=tke2_inout, errmsg=errmsg, errflg=errflg) - - ! Copy any local pointers to dummy/local variables - if (has_ice) then - nci_out = nci_out_ptr - end if - if (has_ice) then - effri_out_local = effri_out_ptr - end if - - ! Compute forward (post-scheme) transforms - effrl_inout(:,1:pver) = 1.0E-6_kind_phys*effrl_inout_local(:,1:pver) - effri_out(:,1:pver) = 1.0E-6_kind_phys*effri_out_local(:,1:pver) - effrs_inout(:,pver:1:-1) = 1.0E-6_kind_phys*real(effrs_inout_local(:,1:pver), & - kind_phys) - scalar_var1 = 1.0E+3_kind_phys*scalar_var_local - - end if - end do - end do - - if (errflg == 0) then - ! ################################################################## - ! Begin debug tests - ! ################################################################## - - ! Check size of array effrr_inout - if (size(effrr_inout(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_post_run: for array effrr_inout, expected size ',& - 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effrr_inout) - errflg = 1 - return - end if - - ! Check length of effrr_inout(:,1) - if (size(effrr_inout(:,1)) /= ncol-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_post_run: for array effrr_inout(:,1), expected size ', ncol-1+1,& - ' but got ', size(effrr_inout(:,1)) - errflg = 1 - return - end if - ! Check length of effrr_inout(1,:) - if (size(effrr_inout(1,:)) /= pver-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_post_run: for array effrr_inout(1,:), expected size ', pver-1+1,& - ' but got ', size(effrr_inout(1,:)) - errflg = 1 - return - end if - - ! Assign value of scalar_var2 to internal_var_real - internal_var_real = scalar_var2 - - ! ################################################################## - ! End debug tests - ! ################################################################## - - - - ! Call scheme - call effr_post_run(effrr_inout=effrr_inout, scalar_var=scalar_var2, errmsg=errmsg, & - errflg=errflg) - - - - end if - end do - do loop0_num_subcycles_for_effr = 1, num_subcycles - - if (errflg == 0) then - ! ################################################################## - ! Begin debug tests - ! ################################################################## - - ! Check size of array effrs_inout - if (size(effrs_inout(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effrs_calc_run: for array effrs_inout, expected size ',& - 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effrs_inout) - errflg = 1 - return - end if - - ! Check length of effrs_inout(:,1) - if (size(effrs_inout(:,1)) /= ncol-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effrs_calc_run: for array effrs_inout(:,1), expected size ', ncol-1+1,& - ' but got ', size(effrs_inout(:,1)) - errflg = 1 - return - end if - ! Check length of effrs_inout(1,:) - if (size(effrs_inout(1,:)) /= pver-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effrs_calc_run: for array effrs_inout(1,:), expected size ', pver-1+1,& - ' but got ', size(effrs_inout(1,:)) - errflg = 1 - return - end if - - ! ################################################################## - ! End debug tests - ! ################################################################## - - - - ! Call scheme - call effrs_calc_run(effrs_inout=effrs_inout, errmsg=errmsg, errflg=errflg) - - - - end if - end do - - if (errflg == 0) then - ! ################################################################## - ! Begin debug tests - ! ################################################################## - - ! Check size of array effrr_inout - if (size(effrr_inout(:,:)) /= 1*(ncol-1+1)*(pver-1+1)) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_diag_run: for array effrr_inout, expected size ',& - 1*(ncol-1+1)*(pver-1+1), ' but got ', size(effrr_inout) - errflg = 1 - return - end if - - ! Check length of effrr_inout(:,1) - if (size(effrr_inout(:,1)) /= ncol-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_diag_run: for array effrr_inout(:,1), expected size ', ncol-1+1,& - ' but got ', size(effrr_inout(:,1)) - errflg = 1 - return - end if - ! Check length of effrr_inout(1,:) - if (size(effrr_inout(1,:)) /= pver-1+1) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_radiation1 before effr_diag_run: for array effrr_inout(1,:), expected size ', pver-1+1,& - ' but got ', size(effrr_inout(1,:)) - errflg = 1 - return - end if - - ! Assign value of scalar_var3 to internal_var_integer - internal_var_integer = scalar_var3 - - ! ################################################################## - ! End debug tests - ! ################################################################## - - ! Compute reverse (pre-scheme) transforms - effrr_in_local(:,1:pver) = 1.0E+6_kind_phys*effrr_inout(:,pver:1:-1) - - - ! Call scheme - call effr_diag_run(effrr_in=effrr_in_local, scalar_var=scalar_var3, errmsg=errmsg, & - errflg=errflg) - - - - end if - - ! Deallocate local arrays - if (allocated(effrg_in_local)) deallocate(effrg_in_local) - if (allocated(effri_out_local)) deallocate(effri_out_local) - if (allocated(effrl_inout_local)) deallocate(effrl_inout_local) - if (allocated(effrr_in_local)) deallocate(effrr_in_local) - if (allocated(effrs_inout_local)) deallocate(effrs_inout_local) - - ! Nullify local pointers - if (associated(effrg_in_ptr)) nullify(effrg_in_ptr) - if (associated(ncg_in_ptr)) nullify(ncg_in_ptr) - if (associated(nci_out_ptr)) nullify(nci_out_ptr) - if (associated(effri_out_ptr)) nullify(effri_out_ptr) - if (associated(ncl_out_ptr)) nullify(ncl_out_ptr) - ! Suite state does not change - - end subroutine main_suite_radiation1 - - ! ======================================================================== - - - subroutine main_suite_rad_lw_group(errflg, errmsg, col_start, col_end, fluxlw, ncols) - - use rad_lw, only: rad_lw_run - use mod_rad_ddt, only: ty_rad_lw - - ! Dummy arguments - integer, intent(out) :: errflg - character(len=512), intent(out) :: errmsg - integer, intent(in) :: col_start - integer, intent(in) :: col_end - type(ty_rad_lw), intent(inout) :: fluxlw(:) - integer, intent(in) :: ncols - - ! Local Variables - integer :: ncol - type(ty_rad_lw) :: internal_var_ty_rad_lw - - ! Initialize ccpp error handling - errflg = 0 - errmsg = '' - - ! Check state machine - if (trim(ccpp_suite_state) /= 'in_time_step') then - errflg = 1 - write(errmsg, '(3a)') "Invalid initial CCPP state, '", trim(ccpp_suite_state), & - "' in main_suite_rad_lw_group" - return - end if - ! Set horizontal loop extent - ncol = col_end - col_start + 1 - - ! Allocate local arrays - - if (errflg == 0) then - ! ################################################################## - ! Begin debug tests - ! ################################################################## - - ! ################################################################## - ! End debug tests - ! ################################################################## - - - - ! Call scheme - call rad_lw_run(ncol=ncol, fluxlw=fluxlw, errmsg=errmsg, errflg=errflg) - - - - end if - ! Suite state does not change - - end subroutine main_suite_rad_lw_group - - ! ======================================================================== - - - subroutine main_suite_rad_sw_group(errflg, errmsg, col_start, col_end, sfc_up_sw, ncols, & - sfc_down_sw) - - use rad_sw, only: rad_sw_run - - ! Dummy arguments - integer, intent(out) :: errflg - character(len=512), intent(out) :: errmsg - integer, intent(in) :: col_start - integer, intent(in) :: col_end - real(kind_phys), intent(inout) :: sfc_up_sw(:) - integer, intent(in) :: ncols - real(kind_phys), intent(inout) :: sfc_down_sw(:) - - ! Local Variables - integer :: ncol - type(real) :: internal_var_real - - ! Initialize ccpp error handling - errflg = 0 - errmsg = '' - - ! Check state machine - if (trim(ccpp_suite_state) /= 'in_time_step') then - errflg = 1 - write(errmsg, '(3a)') "Invalid initial CCPP state, '", trim(ccpp_suite_state), & - "' in main_suite_rad_sw_group" - return - end if - ! Set horizontal loop extent - ncol = col_end - col_start + 1 - - ! Allocate local arrays - - if (errflg == 0) then - ! ################################################################## - ! Begin debug tests - ! ################################################################## - - ! Check size of array sfc_up_sw - if (size(sfc_up_sw(:)) /= 1*(ncol-1+1)) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_rad_sw_group before rad_sw_run: for array sfc_up_sw, expected size ', 1*(ncol-1+1),& - ' but got ', size(sfc_up_sw) - errflg = 1 - return - end if - - - ! Check size of array sfc_down_sw - if (size(sfc_down_sw(:)) /= 1*(ncol-1+1)) then - write(errmsg, '(2(a,i8))') & - 'In group main_suite_rad_sw_group before rad_sw_run: for array sfc_down_sw, expected size ', 1*(ncol-1+1),& - ' but got ', size(sfc_down_sw) - errflg = 1 - return - end if - - - ! ################################################################## - ! End debug tests - ! ################################################################## - - - - ! Call scheme - call rad_sw_run(ncol=ncol, sfc_up_sw=sfc_up_sw, sfc_down_sw=sfc_down_sw, errmsg=errmsg, & - errflg=errflg) - - - - end if - ! Suite state does not change - - end subroutine main_suite_rad_sw_group - - ! ======================================================================== - - - subroutine main_suite_timestep_final(errflg, errmsg) - - - ! Dummy arguments - integer, intent(out) :: errflg - character(len=512), intent(out) :: errmsg - - ! Initialize ccpp error handling - errflg = 0 - errmsg = '' - - ! Output threaded region check -#ifdef _OPENMP - if (omp_get_thread_num() > 1) then - errflg = 1 - errmsg = "Cannot call timestep_final routine from a threaded region" - return - end if -#endif - ! Check state machine - if (trim(ccpp_suite_state) /= 'in_time_step') then - errflg = 1 - write(errmsg, '(3a)') "Invalid initial CCPP state, '", trim(ccpp_suite_state), & - "' in main_suite_timestep_final" - return - end if - ! Set horizontal loop extent - - ! Allocate local arrays - ccpp_suite_state = 'initialized' - - end subroutine main_suite_timestep_final - - ! ======================================================================== - - - subroutine main_suite_finalize(errflg, errmsg) - - - ! Dummy arguments - integer, intent(out) :: errflg - character(len=512), intent(out) :: errmsg - - ! Initialize ccpp error handling - errflg = 0 - errmsg = '' - - ! Output threaded region check -#ifdef _OPENMP - if (omp_get_thread_num() > 1) then - errflg = 1 - errmsg = "Cannot call finalize routine from a threaded region" - return - end if -#endif - ! Check state machine - if (trim(ccpp_suite_state) /= 'initialized') then - errflg = 1 - write(errmsg, '(3a)') "Invalid initial CCPP state, '", trim(ccpp_suite_state), & - "' in main_suite_finalize" - return - end if - ! Set horizontal loop extent - - ! Allocate local arrays - ccpp_suite_state = 'uninitialized' - - end subroutine main_suite_finalize - - ! ======================================================================== - - subroutine ccpp_create_constituent_array(errmsg, errflg) - ! Allocate and fill the constituent property array - ! for this suite - ! Dummy arguments - character(len=512), intent(out) :: errmsg - integer, intent(out) :: errflg - errmsg = '' - errflg = 0 - ccpp_constituents_initialized = .true. - end subroutine ccpp_create_constituent_array - - - ! ======================================================================== - - integer function main_suite_constituents_num_consts(errmsg, errflg) - ! Return the number of constituents for this suite - ! Dummy arguments - character(len=512), intent(out) :: errmsg - integer, intent(out) :: errflg - errmsg = '' - errflg = 0 - ! Make sure that our constituent array is initialized - if (.not. ccpp_constituents_initialized) then - call ccpp_create_constituent_array(errflg=errflg, errmsg=errmsg) - end if - main_suite_constituents_num_consts = 0 - end function main_suite_constituents_num_consts - - ! ======================================================================== - - subroutine main_suite_constituents_const_name(index, name_out, errmsg, errflg) - ! Return the name of constituent, - ! Dummy arguments - integer, intent(in) :: index - character(len=*), intent(out) :: name_out - character(len=512), intent(out) :: errmsg - integer, intent(out) :: errflg - - errflg = 0 - errmsg = '' - ! Make sure that our constituent array is initialized - if (.not. ccpp_constituents_initialized) then - errflg = 1 - errmsg = "constituent properties not initialized for suite, main_suite" - end if - errflg = 1 - write(errmsg, '(a,i0,a)') 'ERROR: main_suite_constituents, has no constituents' - end subroutine main_suite_constituents_const_name - - ! ======================================================================== - - subroutine main_suite_constituents_copy_const(index, cnst_out, errmsg, errflg) - ! Copy the data for a constituent - ! Dummy arguments - integer, intent(in) :: index - type(ccpp_constituent_properties_t), intent(out) :: cnst_out - character(len=512), intent(out) :: errmsg - integer, intent(out) :: errflg - - errflg = 0 - errmsg = '' - ! Make sure that our constituent array is initialized - if (.not. ccpp_constituents_initialized) then - errflg = 1 - errmsg = "constituent properties not initialized for suite, main_suite" - end if - errflg = 1 - write(errmsg, '(a,i0,a)') 'ERROR: main_suite_constituents, has no constituents' - end subroutine main_suite_constituents_copy_const - -end module ccpp_main_suite_cap diff --git a/test/nested_suite_test/main_suite.xml b/test/nested_suite_test/main_suite.xml index e9b96c61..0003e4fd 100644 --- a/test/nested_suite_test/main_suite.xml +++ b/test/nested_suite_test/main_suite.xml @@ -27,4 +27,4 @@ - \ No newline at end of file + diff --git a/test/nested_suite_test/radiation2_suite.xml b/test/nested_suite_test/radiation2_suite.xml index 6752f7ef..b5087fe7 100644 --- a/test/nested_suite_test/radiation2_suite.xml +++ b/test/nested_suite_test/radiation2_suite.xml @@ -14,4 +14,4 @@ - \ No newline at end of file + From 648612afabf5330b6e583e03bb53af941ee2efa8 Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Tue, 7 Oct 2025 13:57:16 -0600 Subject: [PATCH 05/12] Remove unused test/nested_suite_test/nested_suite_test_reports.py --- .../nested_suite_test_reports.py | 116 ------------------ 1 file changed, 116 deletions(-) delete mode 100755 test/nested_suite_test/nested_suite_test_reports.py diff --git a/test/nested_suite_test/nested_suite_test_reports.py b/test/nested_suite_test/nested_suite_test_reports.py deleted file mode 100755 index b409e65a..00000000 --- a/test/nested_suite_test/nested_suite_test_reports.py +++ /dev/null @@ -1,116 +0,0 @@ -#! /usr/bin/env python3 -""" ------------------------------------------------------------------------ - Description: Test capgen database report python interface - - Assumptions: - - Command line arguments: build_dir database_filepath - - Usage: python test_reports ------------------------------------------------------------------------ -""" -import os -import unittest - -from test_stub import BaseTests - -_BUILD_DIR = os.path.join(os.path.abspath(os.environ['BUILD_DIR']), "test", "nested_suite_test") -_DATABASE = os.path.abspath(os.path.join(_BUILD_DIR, "ccpp", "datatable.xml")) - -_TEST_DIR = os.path.dirname(os.path.abspath(__file__)) -_FRAMEWORK_DIR = os.path.abspath(os.path.join(_TEST_DIR, os.pardir, os.pardir)) -_SCRIPTS_DIR = os.path.join(_FRAMEWORK_DIR, "scripts") -_SRC_DIR = os.path.join(_FRAMEWORK_DIR, "src") - -# Check data -_HOST_FILES = [os.path.join(_BUILD_DIR, "ccpp", "test_host_ccpp_cap.F90")] -_SUITE_FILES = [os.path.join(_BUILD_DIR, "ccpp", "ccpp_var_compatibility_suite_cap.F90")] -_UTILITY_FILES = [os.path.join(_BUILD_DIR, "ccpp", "ccpp_kinds.F90"), - os.path.join(_SRC_DIR, "ccpp_constituent_prop_mod.F90"), - os.path.join(_SRC_DIR, "ccpp_scheme_utils.F90"), - os.path.join(_SRC_DIR, "ccpp_hashable.F90"), - os.path.join(_SRC_DIR, "ccpp_hash_table.F90")] -_CCPP_FILES = _UTILITY_FILES + \ - [os.path.join(_BUILD_DIR, "ccpp", "test_host_ccpp_cap.F90"), - os.path.join(_BUILD_DIR, "ccpp", "ccpp_var_compatibility_suite_cap.F90")] -_PROCESS_LIST = [""] -_MODULE_LIST = ["effr_calc", "effrs_calc", "effr_diag", "effr_post", "mod_effr_pre", "rad_lw", "rad_sw"] -_SUITE_LIST = ["nested_suite"] -_DEPENDENCIES = [ os.path.join(_TEST_DIR, "module_rad_ddt.F90")] -_INPUT_VARS_VAR_ACTION = ["horizontal_loop_begin", "horizontal_loop_end", "horizontal_dimension", "vertical_layer_dimension", - "effective_radius_of_stratiform_cloud_liquid_water_particle", - "effective_radius_of_stratiform_cloud_rain_particle", - "effective_radius_of_stratiform_cloud_snow_particle", - "effective_radius_of_stratiform_cloud_graupel", - "cloud_graupel_number_concentration", - "scalar_variable_for_testing", - "turbulent_kinetic_energy", - "turbulent_kinetic_energy2", - "scalar_variable_for_testing_a", - "scalar_variable_for_testing_b", - "scalar_variable_for_testing_c", - "scheme_order_in_suite", - "flag_indicating_cloud_microphysics_has_graupel", - "flag_indicating_cloud_microphysics_has_ice", - "surface_downwelling_shortwave_radiation_flux", - "surface_upwelling_shortwave_radiation_flux", - "longwave_radiation_fluxes", - "num_subcycles_for_effr"] -_OUTPUT_VARS_VAR_ACTION = ["ccpp_error_code", "ccpp_error_message", - "effective_radius_of_stratiform_cloud_ice_particle", - "effective_radius_of_stratiform_cloud_liquid_water_particle", - "effective_radius_of_stratiform_cloud_snow_particle", - "cloud_ice_number_concentration", - "effective_radius_of_stratiform_cloud_rain_particle", - "turbulent_kinetic_energy", - "turbulent_kinetic_energy2", - "scalar_variable_for_testing", - "scalar_variable_for_testing", - "surface_downwelling_shortwave_radiation_flux", - "surface_upwelling_shortwave_radiation_flux", - "longwave_radiation_fluxes", - "scheme_order_in_suite"] -_REQUIRED_VARS_VAR_ACTION = _INPUT_VARS_VAR_ACTION + _OUTPUT_VARS_VAR_ACTION - - -class TestVarCompatibilityHostDataTables(unittest.TestCase, BaseTests.TestHostDataTables): - database = _DATABASE - host_files = _HOST_FILES - suite_files = _SUITE_FILES - utility_files = _UTILITY_FILES - ccpp_files = _CCPP_FILES - process_list = _PROCESS_LIST - module_list = _MODULE_LIST - dependencies = _DEPENDENCIES - suite_list = _SUITE_LIST - - -class CommandLineVarCompatibilityHostDatafileRequiredFiles(unittest.TestCase, BaseTests.TestHostCommandLineDataFiles): - database = _DATABASE - host_files = _HOST_FILES - suite_files = _SUITE_FILES - utility_files = _UTILITY_FILES - ccpp_files = _CCPP_FILES - process_list = _PROCESS_LIST - module_list = _MODULE_LIST - dependencies = _DEPENDENCIES - suite_list = _SUITE_LIST - datafile_script = f"{_SCRIPTS_DIR}/ccpp_datafile.py" - - -class TestCapgenDdtSuite(unittest.TestCase, BaseTests.TestSuite): - database = _DATABASE - required_vars = _REQUIRED_VARS_VAR_ACTION - input_vars = _INPUT_VARS_VAR_ACTION - output_vars = _OUTPUT_VARS_VAR_ACTION - suite_name = "nested_suite" - - -class CommandLineCapgenDdtSuite(unittest.TestCase, BaseTests.TestSuiteCommandLine): - database = _DATABASE - required_vars = _REQUIRED_VARS_VAR_ACTION - input_vars = _INPUT_VARS_VAR_ACTION - output_vars = _OUTPUT_VARS_VAR_ACTION - suite_name = "nested_suite" - datafile_script = f"{_SCRIPTS_DIR}/ccpp_datafile.py" From 0d144ae1e6701df21a5884b0733fa517ca2a60ae Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Tue, 4 Nov 2025 12:03:44 -0700 Subject: [PATCH 06/12] Nested suites: prevent infinite recursion, allow file attribute to be absolute or relative, always write expanded SDF --- scripts/ccpp_suite.py | 16 +++++---- scripts/parse_tools/xml_tools.py | 56 +++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/scripts/ccpp_suite.py b/scripts/ccpp_suite.py index 9fa98c0e..b60bdd51 100644 --- a/scripts/ccpp_suite.py +++ b/scripts/ccpp_suite.py @@ -682,21 +682,23 @@ def __init__(self, sdfs, host_model, scheme_headers, run_env): if not res: raise CCPPError(f"Invalid suite definition file, '{sdf}'") + # Write the expanded sdf to the capgen output directory. + # This file isn't used by capgen (everything is in memory + # from here onwards), but it is useful for developers/users + # (although the output can also be found in the datatable). + (sdf_path, sdf_name) = os.path.split(sdf) + sdf_expanded = os.path.join(run_env.output_dir, + sdf_name.replace(".xml", "_expanded.xml")) # Processing of the sdf depends on the schema version if xml_root.tag.lower() == "suite" and schema_version[0] == 1: suite = Suite(sdf, xml_root, self, run_env) suite.analyze(self.host_model, scheme_library, self.__ddt_lib, run_env) self.__suites.append(suite) + write_xml_file(xml_root, sdf_expanded, run_env.logger) elif xml_root.tag.lower() == "suites" and schema_version[0] == 2: # Preprocess the sdf to expand nested suites - expand_nested_suites(xml_root, logger=run_env.logger) - # Write the expanded sdf to the capgen output directory; - # this file isn't used by capgen (everything is in memory - # from here onwards), but it is useful for developers/users - # (although the output can also be found in the datatable). - sdf_expanded = os.path.join(run_env.output_dir, - os.path.split(sdf)[1].replace(".xml", "_expanded.xml")) + expand_nested_suites(xml_root, sdf_path, logger=run_env.logger) write_xml_file(xml_root, sdf_expanded, run_env.logger) for suite_item in xml_root: suite = Suite(sdf, suite_item, self, run_env) diff --git a/scripts/parse_tools/xml_tools.py b/scripts/parse_tools/xml_tools.py index ed77cd84..43f8e777 100644 --- a/scripts/parse_tools/xml_tools.py +++ b/scripts/parse_tools/xml_tools.py @@ -324,7 +324,7 @@ def load_suite_by_name(suite_name, group_name, main_root, file=None, logger=None raise CCPPError(emsg) ############################################################################### -def replace_nested_suite(element, nested_suite, root, logger): +def replace_nested_suite(element, nested_suite, root, default_path, logger): ############################################################################### """ Replace a tag with the actual suite or group it references. @@ -340,6 +340,7 @@ def replace_nested_suite(element, nested_suite, root, logger): element (xml.etree.ElementTree.Element): The parent element containing the nested suite. nested_suite (xml.etree.ElementTree.Element): The element to be replaced. root (xml.etree.ElementTree.Element): The root XML element (used when no file is specified). + default_path (str): The default path to look for nested SDFs if file is not a absolute path. logger (logging.Logger or None): Logger to record debug information. Returns: @@ -367,7 +368,7 @@ def replace_nested_suite(element, nested_suite, root, logger): >>> root = tree.getroot() >>> top_suite = root.find("suite[@name='top']") >>> nested = top_suite.find("nested_suite") - >>> replace_nested_suite(top_suite, nested, root, logger) + >>> replace_nested_suite(top_suite, nested, root, '/no/valid/path', logger) Expanded nested suite 'my_suite' 'my_suite' >>> [child.tag for child in top_suite] @@ -393,7 +394,7 @@ def replace_nested_suite(element, nested_suite, root, logger): >>> top_suite = root.find("suite[@name='top']") >>> top_group = top_suite.find("group") >>> nested = top_group.find("nested_suite") - >>> replace_nested_suite(top_group, nested, root, logger) + >>> replace_nested_suite(top_group, nested, root, '/no/valid/path', logger) Expanded nested suite 'my_suite', group 'my_group' 'my_suite' >>> [child.tag for child in top_suite] @@ -404,6 +405,8 @@ def replace_nested_suite(element, nested_suite, root, logger): suite_name = nested_suite.attrib.get("name") group_name = nested_suite.attrib.get("group") file = nested_suite.attrib.get("file") + if file and not os.path.isabs(file): + file = os.path.join(default_path, file) referenced_suite = load_suite_by_name(suite_name, group_name, root, file=file, logger=logger) imported_content = [ET.fromstring(ET.tostring(child)) @@ -428,7 +431,7 @@ def replace_nested_suite(element, nested_suite, root, logger): return suite_name if not file else None ############################################################################### -def expand_nested_suites(root, logger=None): +def expand_nested_suites(root, default_path, logger=None): ############################################################################### """ Recursively expand all elements within the XML elements. @@ -473,7 +476,7 @@ def expand_nested_suites(root, logger=None): ... ... ''' >>> root = ET.fromstring(xml) - >>> expand_nested_suites(root, logger) + >>> expand_nested_suites(root, '/no/valid/path', logger) Expanded nested suite 'microphysics_suite', group 'micro' Expanded nested suite 'pbl_suite' Removed nested suite 'microphysics_suite' from root element @@ -488,6 +491,29 @@ def expand_nested_suites(root, logger=None): 'main' >>> group.find("scheme").text 'cloud_scheme' + >>> xml2 = ''' + ... + ... + ... + ... + ... + ... + ... + ... + ... + ... cloud_scheme + ... + ... + ... + ... + ... + ... + ... ''' + >>> root2 = ET.fromstring(xml2) + >>> expand_nested_suites(root2, logger) #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + CCPPError: Infinite recursion while expanding nested suites: ['physics_suite', 'physics_suite'] """ # Keep track of any nested suites defined under the same root # that need to be removed at the end of this function. @@ -497,23 +523,31 @@ def expand_nested_suites(root, logger=None): keep_expanding = True while keep_expanding: keep_expanding = False + ## To avoid infinite recursion, keep track of suite names + #suite_names = [] for suite in root.findall("suite"): + # To avoid infinite recursion, keep track of suite names + suite_names = [suite.attrib.get("name")] # First, search all groups for nested_suite elements groups = suite.findall("group") for group in groups: nested_suites = group.findall("nested_suite") for nested in nested_suites: - suite_name = replace_nested_suite(group, nested, root, logger) - if suite_name: - expanded_suites_to_remove.append(suite_name) + suite_name = replace_nested_suite(group, nested, root, default_path, logger) + suite_names.append(suite_name) + if not len(suite_names) == len(set(suite_names)): + raise CCPPError(f"Infinite recursion while expanding nested suites: {suite_names}") + expanded_suites_to_remove.append(suite_name) # Trigger another pass over the root element keep_expanding = True # Second, search all suites for nested_suite elements nested_suites = suite.findall("nested_suite") for nested in nested_suites: - suite_name = replace_nested_suite(suite, nested, root, logger) - if suite_name: - expanded_suites_to_remove.append(suite_name) + suite_name = replace_nested_suite(suite, nested, root, default_path, logger) + suite_names.append(suite_name) + if not len(suite_names) == len(set(suite_names)): + raise CCPPError(f"Infinite recursion while expanding nested suites: {suite_names}") + expanded_suites_to_remove.append(suite_name) # Trigger another pass over the root element keep_expanding = True # Remove expanded suites From f336c174b618817bdcaee84abe3db65dd43d96ea Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Wed, 12 Nov 2025 10:36:19 -0700 Subject: [PATCH 07/12] Add capability to insert only a specific group of a nested suite at the suite level --- schema/suite_v2_0.xsd | 1 + scripts/parse_tools/xml_tools.py | 35 ++++++++++++++++++++++++++- test/nested_suite_test/main_suite.xml | 10 +++++--- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/schema/suite_v2_0.xsd b/schema/suite_v2_0.xsd index 6a20020b..225897ea 100644 --- a/schema/suite_v2_0.xsd +++ b/schema/suite_v2_0.xsd @@ -53,6 +53,7 @@ + diff --git a/scripts/parse_tools/xml_tools.py b/scripts/parse_tools/xml_tools.py index 43f8e777..c23e09f9 100644 --- a/scripts/parse_tools/xml_tools.py +++ b/scripts/parse_tools/xml_tools.py @@ -401,6 +401,29 @@ def replace_nested_suite(element, nested_suite, root, default_path, logger): ['group'] >>> top_suite.find("group").find("scheme").text 'my_scheme' + >>> xml = ''' + ... + ... + ... + ... my_scheme + ... + ... + ... + ... + ... + ... + ... ''' + >>> tree = ET.ElementTree(ET.fromstring(xml)) + >>> root = tree.getroot() + >>> top_suite = root.find("suite[@name='top']") + >>> nested = top_suite.find("nested_suite") + >>> replace_nested_suite(top_suite, nested, root, '/no/valid/path', logger) + Expanded nested suite 'my_suite', group 'my_group' + 'my_suite' + >>> [child.tag for child in top_suite] + ['group'] + >>> top_suite.find("group").find("scheme").text + 'my_scheme' """ suite_name = nested_suite.attrib.get("name") group_name = nested_suite.attrib.get("group") @@ -419,7 +442,17 @@ def replace_nested_suite(element, nested_suite, root, default_path, logger): if item.tag == "nested_suite": if file and not item.attrib.get("file"): item.set("file", file) - element.insert(list(element).index(nested_suite), item) + # If we are inserting a nested suite at the suite level (element.tag is suite), + # but we only want one group (group_name is not none), then we need to wrap + # the item in a group element. If on the other hand we insert an entire suite + # (all groups) at the suite level, or a specific group at the group level, + # then we can insert the item as is. + if element.tag == 'suite' and group_name: + item_to_insert = ET.Element("group", attrib={"name": group_name}) + item_to_insert.append(item) + else: + item_to_insert = item + element.insert(list(element).index(nested_suite), item_to_insert) element.remove(nested_suite) if logger: msg = f"Expanded nested suite '{suite_name}'" \ diff --git a/test/nested_suite_test/main_suite.xml b/test/nested_suite_test/main_suite.xml index 0003e4fd..cd4d84c5 100644 --- a/test/nested_suite_test/main_suite.xml +++ b/test/nested_suite_test/main_suite.xml @@ -3,14 +3,17 @@ - - rad_lw - + + + rad_lw + + + @@ -24,6 +27,7 @@ + From e0d375b8914f0675cb6e30e02c869235321663d7 Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Thu, 13 Nov 2025 12:21:42 -0700 Subject: [PATCH 08/12] Revert back to only one suite per SDF; fix check for recursive expansion of nested suites --- schema/suite_v1_0.xsd | 2 +- schema/suite_v2_0.xsd | 18 +- scripts/ccpp_suite.py | 17 +- scripts/parse_tools/xml_tools.py | 419 +++++++++--------- test/nested_suite_test/main_suite.xml | 42 +- test/nested_suite_test/radiation2_suite.xml | 23 +- .../nested_suite_test/radiation3_subsuite.xml | 12 +- test/nested_suite_test/radiation3_suite.xml | 7 + test/nested_suite_test/radiation4_suite.xml | 7 + 9 files changed, 255 insertions(+), 292 deletions(-) create mode 100644 test/nested_suite_test/radiation3_suite.xml create mode 100644 test/nested_suite_test/radiation4_suite.xml diff --git a/schema/suite_v1_0.xsd b/schema/suite_v1_0.xsd index 121438ed..dfa96cc5 100644 --- a/schema/suite_v1_0.xsd +++ b/schema/suite_v1_0.xsd @@ -26,7 +26,7 @@ - + diff --git a/schema/suite_v2_0.xsd b/schema/suite_v2_0.xsd index 225897ea..c6752c27 100644 --- a/schema/suite_v2_0.xsd +++ b/schema/suite_v2_0.xsd @@ -44,7 +44,7 @@ - + @@ -54,7 +54,7 @@ - + @@ -134,7 +134,7 @@ - + @@ -143,17 +143,9 @@ - - - - - - - - - - + + diff --git a/scripts/ccpp_suite.py b/scripts/ccpp_suite.py index b60bdd51..5f47c22d 100644 --- a/scripts/ccpp_suite.py +++ b/scripts/ccpp_suite.py @@ -689,22 +689,15 @@ def __init__(self, sdfs, host_model, scheme_headers, run_env): (sdf_path, sdf_name) = os.path.split(sdf) sdf_expanded = os.path.join(run_env.output_dir, sdf_name.replace(".xml", "_expanded.xml")) - # Processing of the sdf depends on the schema version - if xml_root.tag.lower() == "suite" and schema_version[0] == 1: + if schema_version[0] in [1, 2]: + # Preprocess the sdf to expand nested suites + if schema_version[0] == 2: + expand_nested_suites(xml_root, sdf_path, logger=run_env.logger) + write_xml_file(xml_root, sdf_expanded, run_env.logger) suite = Suite(sdf, xml_root, self, run_env) suite.analyze(self.host_model, scheme_library, self.__ddt_lib, run_env) self.__suites.append(suite) - write_xml_file(xml_root, sdf_expanded, run_env.logger) - elif xml_root.tag.lower() == "suites" and schema_version[0] == 2: - # Preprocess the sdf to expand nested suites - expand_nested_suites(xml_root, sdf_path, logger=run_env.logger) - write_xml_file(xml_root, sdf_expanded, run_env.logger) - for suite_item in xml_root: - suite = Suite(sdf, suite_item, self, run_env) - suite.analyze(self.host_model, scheme_library, - self.__ddt_lib, run_env) - self.__suites.append(suite) else: errmsg = f"Suite XML schema not supported: " + \ "root={xml_root.tag}, version={schema_version}" diff --git a/scripts/parse_tools/xml_tools.py b/scripts/parse_tools/xml_tools.py index c23e09f9..7b9b1710 100644 --- a/scripts/parse_tools/xml_tools.py +++ b/scripts/parse_tools/xml_tools.py @@ -226,7 +226,19 @@ def validate_xml_file(filename, schema_root, version, logger, ############################################################################### def read_xml_file(filename, logger=None): ############################################################################### - """Read the XML file, , and return its tree and root""" + """Read the XML file, , and return its tree and root + + Parameters: + filename (str): The path to an XML file to read and search. + logger (logging.Logger, optional): Logger for warnings/errors. + + Returns: + tree (xml.etree.ElementTreet): The element tree from the input file. + root (xml.etree.ElementTree.Element): The root element of tree. + + Raises: + CCPPError: If the file cannot be found or read. + """ if os.path.isfile(filename) and os.access(filename, os.R_OK): file_open = (lambda x: open(x, 'r', encoding='utf-8')) with file_open(filename) as file_: @@ -248,18 +260,15 @@ def read_xml_file(filename, logger=None): return tree, root ############################################################################### -def load_suite_by_name(suite_name, group_name, main_root, file=None, logger=None): +def load_suite_by_name(suite_name, group_name, file, logger=None): ############################################################################### """ Load a suite by its name, or a group of a suite by the suite and group names. - If the optional file argument is provided, look for the object in that file, - otherwise search the current main_root. Parameters: suite_name (str): The name of the suite to find. group_name (str or None): The name of the group to find within the suite. - main_root (xml.etree.ElementTree.Element): The XML root to search if no file is given. - file (str, optional): The path to an XML file to read and search instead of main_root. + file (str): The path to an XML file to read and search. logger (logging.Logger, optional): Logger for warnings/errors. Returns: @@ -269,169 +278,143 @@ def load_suite_by_name(suite_name, group_name, main_root, file=None, logger=None CCPPError: If the suite or group is not found, or if the schema is invalid. Examples: + >>> import tempfile >>> import xml.etree.ElementTree as ET - >>> from types import SimpleNamespace - >>> def read_xml_file(file, logger=None): - ... return None, ET.fromstring(file) - >>> def find_schema_version(root): - ... return (2, 0) - >>> def validate_xml_file(file, kind, schema_version, logger=None): - ... return True - >>> xml_content = ''' - ... - ... - ... - ... - ... - ... - ... ''' - >>> root = ET.fromstring(xml_content) - >>> load_suite_by_name("physics_suite", None, root).tag + >>> logger = init_log('xml_tools') + >>> # Create temporary files for the nested suites + >>> tmpdir = tempfile.TemporaryDirectory() + >>> file1_path = os.path.join(tmpdir.name, "file1.xml") + >>> # Write XML contents to temporary file + >>> with open(file1_path, "w") as f: + ... _ = f.write(''' + ... + ... + ... + ... + ... ''') + >>> load_suite_by_name("physics_suite", None, file1_path, logger).tag 'suite' - >>> load_suite_by_name("physics_suite", "dynamics", root).attrib['name'] + >>> load_suite_by_name("physics_suite", "dynamics", file1_path, logger).attrib['name'] 'dynamics' - >>> load_suite_by_name("physics_suite", "missing_group", root) #doctest: +IGNORE_EXCEPTION_DETAIL + >>> load_suite_by_name("physics_suite", "missing_group", file1_path, logger) #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... CCPPError: Nested suite physics_suite, group missing_group, not found - >>> load_suite_by_name("missing_suite", None, root) #doctest: +IGNORE_EXCEPTION_DETAIL + >>> load_suite_by_name("missing_suite", None, file1_path, logger) #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... CCPPError: Nested suite missing_suite not found + >>> tmpdir.cleanup() """ - if file: - _, root = read_xml_file(file, logger) - schema_version = find_schema_version(root) - if schema_version[0] < 2: - raise CCPPError(f"XML schema version {schema_version} " + \ - f"invalid for nested suite {suite_name}") - res = validate_xml_file(file, 'suite', schema_version, logger) - if not res: - raise CCPPError(f"Invalid suite definition file, '{sdf}'") - else: - root = main_root - for suite in root.findall("suite"): - if suite.attrib.get("name") == suite_name: - if group_name: - for group in suite.findall("group"): - if group.attrib.get("name") == group_name: - return group - else: - return suite + _, root = read_xml_file(file, logger) + schema_version = find_schema_version(root) + if schema_version[0] < 2: + raise CCPPError(f"XML schema version {schema_version} " + \ + f"invalid for nested suite {suite_name}") + res = validate_xml_file(file, 'suite', schema_version, logger) + if not res: + raise CCPPError(f"Invalid suite definition file, '{sdf}'") + suite = root + if suite.attrib.get("name") == suite_name: + if group_name: + for group in suite.findall("group"): + if group.attrib.get("name") == group_name: + return group + else: + return suite emsg = f"Nested suite {suite_name}" \ + (f", group {group_name}," if group_name else "") \ + " not found" + (f" in file {file}" if file else "") raise CCPPError(emsg) ############################################################################### -def replace_nested_suite(element, nested_suite, root, default_path, logger): +def replace_nested_suite(element, nested_suite, default_path, logger): ############################################################################### """ Replace a tag with the actual suite or group it references. - This function looks up a referenced suite or suite group from the main XML tree - or an external file (if specified), deep copies its children, and replaces the - element in the parent `element` with the copied contents. - - If the nested suite being inserted contains its own elements and - within the same external file, the `file` attribute is propagated into those. + This function looks up a referenced suite or suite group from an external + file, deep copies its children, and replaces the element + in the parent `element` with the copied contents. Parameters: element (xml.etree.ElementTree.Element): The parent element containing the nested suite. nested_suite (xml.etree.ElementTree.Element): The element to be replaced. - root (xml.etree.ElementTree.Element): The root XML element (used when no file is specified). default_path (str): The default path to look for nested SDFs if file is not a absolute path. logger (logging.Logger or None): Logger to record debug information. Returns: - str or None: The name of the suite if the nested suite came from the root XML element - (to delete it later), or None if it came from a separate file. + str: The name of the suite that was replaced Example: + >>> import tempfile >>> import xml.etree.ElementTree as ET >>> from types import SimpleNamespace - >>> logger = SimpleNamespace() - >>> logger.debug = print - >>> xml = ''' - ... - ... - ... - ... my_scheme - ... - ... - ... - ... - ... - ... + >>> logger = init_log('xml_tools') + >>> tmpdir = tempfile.TemporaryDirectory() + >>> file1_path = os.path.join(tmpdir.name, "file1.xml") + >>> with open(file1_path, "w") as f: + ... _ = f.write(''' + ... + ... + ... my_scheme + ... + ... + ... ''') + >>> # Import nested suite at suite level + >>> xml = f''' + ... + ... + ... ... ''' - >>> tree = ET.ElementTree(ET.fromstring(xml)) - >>> root = tree.getroot() - >>> top_suite = root.find("suite[@name='top']") + >>> top_suite = ET.fromstring(xml) >>> nested = top_suite.find("nested_suite") - >>> replace_nested_suite(top_suite, nested, root, '/no/valid/path', logger) - Expanded nested suite 'my_suite' + >>> replace_nested_suite(top_suite, nested, tmpdir.name, logger) 'my_suite' >>> [child.tag for child in top_suite] ['group'] >>> top_suite.find("group").find("scheme").text 'my_scheme' - >>> xml = ''' - ... - ... - ... - ... my_scheme - ... - ... - ... - ... - ... - ... - ... - ... + >>> # Import group from nested suite at group level + >>> xml = f''' + ... + ... + ... + ... + ... ... ''' - >>> tree = ET.ElementTree(ET.fromstring(xml)) - >>> root = tree.getroot() - >>> top_suite = root.find("suite[@name='top']") + >>> top_suite = ET.fromstring(xml) >>> top_group = top_suite.find("group") >>> nested = top_group.find("nested_suite") - >>> replace_nested_suite(top_group, nested, root, '/no/valid/path', logger) - Expanded nested suite 'my_suite', group 'my_group' + >>> replace_nested_suite(top_group, nested, tmpdir.name, logger) 'my_suite' >>> [child.tag for child in top_suite] ['group'] >>> top_suite.find("group").find("scheme").text 'my_scheme' - >>> xml = ''' - ... - ... - ... - ... my_scheme - ... - ... - ... - ... - ... - ... + >>> # Import group from nested suite at suite level + >>> xml = f''' + ... + ... + ... ... ''' - >>> tree = ET.ElementTree(ET.fromstring(xml)) - >>> root = tree.getroot() - >>> top_suite = root.find("suite[@name='top']") + >>> top_suite = ET.fromstring(xml) >>> nested = top_suite.find("nested_suite") - >>> replace_nested_suite(top_suite, nested, root, '/no/valid/path', logger) - Expanded nested suite 'my_suite', group 'my_group' + >>> replace_nested_suite(top_suite, nested, tmpdir.name, logger) 'my_suite' >>> [child.tag for child in top_suite] ['group'] >>> top_suite.find("group").find("scheme").text 'my_scheme' + >>> tmpdir.cleanup() """ suite_name = nested_suite.attrib.get("name") group_name = nested_suite.attrib.get("group") file = nested_suite.attrib.get("file") - if file and not os.path.isabs(file): + if not os.path.isabs(file): file = os.path.join(default_path, file) - referenced_suite = load_suite_by_name(suite_name, group_name, root, - file=file, logger=logger) + referenced_suite = load_suite_by_name(suite_name, group_name, file, + logger=logger) imported_content = [ET.fromstring(ET.tostring(child)) for child in referenced_suite] # Swap nested suite with imported content @@ -459,140 +442,146 @@ def replace_nested_suite(element, nested_suite, root, default_path, logger): + (f", group '{group_name}'," if group_name else "") \ + (f" in file '{file}'" if file else "") logger.debug(msg.rstrip(',')) - # If the nested suite resides in the same file as the root - # element then we need to remove it - return suite_name if not file else None + # Return the name of the suite that we just replaced + return suite_name ############################################################################### -def expand_nested_suites(root, default_path, logger=None): +def expand_nested_suites(suite, default_path, logger=None): ############################################################################### """ - Recursively expand all elements within the XML elements. + Recursively expand all elements within the XML element. This function finds elements within or elements, - and replaces them with the corresponding content from another suite. Nested - suites from the same XML root are removed after expansion. + and replaces them with the corresponding content from another suite. This operation is recursive and will continue expanding until no elements remain. Parameters: - root (xml.etree.ElementTree.Element): The root element containing elements. + suite (xml.etree.ElementTree.Element): The root element. logger (logging.Logger, optional): Logger for debug messages. Returns: None. The XML tree is modified in place. Example: + >>> import tempfile >>> import xml.etree.ElementTree as ET - >>> from types import SimpleNamespace - >>> logger = SimpleNamespace() - >>> logger.debug = print - >>> xml = ''' - ... - ... - ... - ... - ... - ... - ... - ... - ... - ... cloud_scheme - ... - ... - ... - ... - ... pbl_scheme - ... - ... - ... + >>> logger = init_log('xml_tools') + >>> tmpdir = tempfile.TemporaryDirectory() + >>> file1_path = os.path.join(tmpdir.name, "file1.xml") + >>> file2_path = os.path.join(tmpdir.name, "file2.xml") + >>> file3_path = os.path.join(tmpdir.name, "file3.xml") + >>> file4_path = os.path.join(tmpdir.name, "file4.xml") + >>> file5_path = os.path.join(tmpdir.name, "file5.xml") + >>> # Write mock XML contents for the nested suites + >>> with open(file1_path, "w") as f: + ... _ = f.write(''' + ... + ... + ... cloud_scheme + ... + ... + ... ''') + >>> with open(file2_path, "w") as f: + ... _ = f.write(''' + ... + ... + ... pbl_scheme + ... + ... + ... ''') + >>> with open(file3_path, "w") as f: + ... _ = f.write(''' + ... + ... + ... rrtmg_lw_scheme + ... + ... + ... rrtmg_sw_scheme + ... + ... + ... ''') + >>> with open(file4_path, "w") as f: + ... _ = f.write(f''' + ... + ... + ... + ... ''') + >>> with open(file5_path, "w") as f: + ... _ = f.write(f''' + ... + ... + ... + ... ''') + >>> # Parent suite + >>> xml_content = f''' + ... + ... + ... + ... + ... + ... + ... ... ''' - >>> root = ET.fromstring(xml) - >>> expand_nested_suites(root, '/no/valid/path', logger) - Expanded nested suite 'microphysics_suite', group 'micro' - Expanded nested suite 'pbl_suite' - Removed nested suite 'microphysics_suite' from root element - Removed nested suite 'pbl_suite' from root element - >>> len(root.findall("suite")) # Only one suite left - 1 - >>> suite = root.find("suite") - >>> suite.attrib.get("name") - 'physics_suite' - >>> group = suite.find("group") - >>> group.attrib.get("name") - 'main' - >>> group.find("scheme").text - 'cloud_scheme' - >>> xml2 = ''' - ... - ... - ... - ... - ... - ... - ... - ... - ... - ... cloud_scheme - ... - ... - ... - ... - ... - ... + >>> suite = ET.fromstring(xml_content) + >>> expand_nested_suites(suite, tmpdir.name, logger) + >>> ET.dump(suite) + + + cloud_scheme + + pbl_scheme + + rrtmg_lw_scheme + + rrtmg_sw_scheme + + >>> # Test infite recursion + >>> xml_content = f''' + ... + ... + ... + ... + ... + ... ... ''' - >>> root2 = ET.fromstring(xml2) - >>> expand_nested_suites(root2, logger) #doctest: +IGNORE_EXCEPTION_DETAIL + >>> suite = ET.fromstring(xml_content) + >>> expand_nested_suites(suite, tmpdir.name, logger) #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - CCPPError: Infinite recursion while expanding nested suites: ['physics_suite', 'physics_suite'] + CCPPError: Exceeded number of iterations while expanding nested suites + >>> tmpdir.cleanup() """ - # Keep track of any nested suites defined under the same root - # that need to be removed at the end of this function. - # This happens all in memory, it does not alter files on disk. - expanded_suites_to_remove = list() + # To avoid infinite recursion, we simply count the number + # of iterations and stop at a certain limit. If someone is + # smart enough to come up with nested suite constructs that + # require more iterations, than he/she should be able to + # track down this variable and adjust it! + max_iterations = 10 # Iteratively expand nested suites until they are all gone keep_expanding = True - while keep_expanding: + for num_iterations in range(max_iterations): keep_expanding = False - ## To avoid infinite recursion, keep track of suite names - #suite_names = [] - for suite in root.findall("suite"): - # To avoid infinite recursion, keep track of suite names - suite_names = [suite.attrib.get("name")] - # First, search all groups for nested_suite elements - groups = suite.findall("group") - for group in groups: - nested_suites = group.findall("nested_suite") - for nested in nested_suites: - suite_name = replace_nested_suite(group, nested, root, default_path, logger) - suite_names.append(suite_name) - if not len(suite_names) == len(set(suite_names)): - raise CCPPError(f"Infinite recursion while expanding nested suites: {suite_names}") - expanded_suites_to_remove.append(suite_name) - # Trigger another pass over the root element - keep_expanding = True - # Second, search all suites for nested_suite elements - nested_suites = suite.findall("nested_suite") + # First, search all groups for nested_suite elements + groups = suite.findall("group") + for group in groups: + nested_suites = group.findall("nested_suite") for nested in nested_suites: - suite_name = replace_nested_suite(suite, nested, root, default_path, logger) - suite_names.append(suite_name) - if not len(suite_names) == len(set(suite_names)): - raise CCPPError(f"Infinite recursion while expanding nested suites: {suite_names}") - expanded_suites_to_remove.append(suite_name) + _ = replace_nested_suite(group, nested, default_path, logger) # Trigger another pass over the root element keep_expanding = True - # Remove expanded suites - expanded_suites_to_remove = list(set(expanded_suites_to_remove)) - for suite in root.findall("suite"): - suite_name = suite.attrib.get("name") - if suite_name in expanded_suites_to_remove: - root.remove(suite) - if logger: - msg = f"Removed nested suite '{suite_name}' from root element" - logger.debug(msg) - + # Second, search all suites for nested_suite elements + nested_suites = suite.findall("nested_suite") + for nested in nested_suites: + _ = replace_nested_suite(suite, nested, default_path, logger) + # Trigger another pass over the root element + keep_expanding = True + if not keep_expanding: + return + raise CCPPError("Exceeded number of iterations while expanding nested suites:" + \ + "check for inifite recursion or adjust limit max_iterations") + ############################################################################### def write_xml_file(root, file_path, logger=None): ############################################################################### diff --git a/test/nested_suite_test/main_suite.xml b/test/nested_suite_test/main_suite.xml index cd4d84c5..a319ec47 100644 --- a/test/nested_suite_test/main_suite.xml +++ b/test/nested_suite_test/main_suite.xml @@ -1,34 +1,18 @@ - - - - - - - - - - - rad_lw - - - - - - - effr_pre + + + + effr_pre + - - effr_calc - + effr_calc - effr_post - - - - - - - + effr_post + + + + + + diff --git a/test/nested_suite_test/radiation2_suite.xml b/test/nested_suite_test/radiation2_suite.xml index b5087fe7..e20b81e8 100644 --- a/test/nested_suite_test/radiation2_suite.xml +++ b/test/nested_suite_test/radiation2_suite.xml @@ -1,17 +1,10 @@ - - - - effr_diag - - - - - - effrs_calc - - - - - + + + + effrs_calc + + effr_diag + + diff --git a/test/nested_suite_test/radiation3_subsuite.xml b/test/nested_suite_test/radiation3_subsuite.xml index dc5bb983..346db62d 100644 --- a/test/nested_suite_test/radiation3_subsuite.xml +++ b/test/nested_suite_test/radiation3_subsuite.xml @@ -1,9 +1,7 @@ - - - - rad_sw - - - + + + rad_sw + + diff --git a/test/nested_suite_test/radiation3_suite.xml b/test/nested_suite_test/radiation3_suite.xml new file mode 100644 index 00000000..89e5bc13 --- /dev/null +++ b/test/nested_suite_test/radiation3_suite.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/nested_suite_test/radiation4_suite.xml b/test/nested_suite_test/radiation4_suite.xml new file mode 100644 index 00000000..d3df4fb9 --- /dev/null +++ b/test/nested_suite_test/radiation4_suite.xml @@ -0,0 +1,7 @@ + + + + + rad_lw + + From 28779432fae643cb600630559cbb8fbb388d68c9 Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Thu, 20 Nov 2025 16:45:26 -0600 Subject: [PATCH 09/12] In xml_tools.py: collect names of expanded suite and provide to user when maximum number of iterations is exceeded --- scripts/parse_tools/xml_tools.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/parse_tools/xml_tools.py b/scripts/parse_tools/xml_tools.py index 7b9b1710..ba3a5d57 100644 --- a/scripts/parse_tools/xml_tools.py +++ b/scripts/parse_tools/xml_tools.py @@ -559,6 +559,8 @@ def expand_nested_suites(suite, default_path, logger=None): # require more iterations, than he/she should be able to # track down this variable and adjust it! max_iterations = 10 + # Collect the names of the expanded suites + suite_names = [] # Iteratively expand nested suites until they are all gone keep_expanding = True for num_iterations in range(max_iterations): @@ -568,19 +570,20 @@ def expand_nested_suites(suite, default_path, logger=None): for group in groups: nested_suites = group.findall("nested_suite") for nested in nested_suites: - _ = replace_nested_suite(group, nested, default_path, logger) + suite_names.append(replace_nested_suite(group, nested, default_path, logger)) # Trigger another pass over the root element keep_expanding = True # Second, search all suites for nested_suite elements nested_suites = suite.findall("nested_suite") for nested in nested_suites: - _ = replace_nested_suite(suite, nested, default_path, logger) + suite_names.append(replace_nested_suite(suite, nested, default_path, logger)) # Trigger another pass over the root element keep_expanding = True if not keep_expanding: return raise CCPPError("Exceeded number of iterations while expanding nested suites:" + \ - "check for inifite recursion or adjust limit max_iterations") + "check for inifite recursion or adjust limit max_iterations." + \ + f"Suites expanded so far: {suite_names}") ############################################################################### def write_xml_file(root, file_path, logger=None): From 4ac80e41a02d924257e71b5b19c3ca7959de501f Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Thu, 20 Nov 2025 16:53:09 -0600 Subject: [PATCH 10/12] Remove unused optional attribute lib from schema/suite_v2_0.xsd --- schema/suite_v2_0.xsd | 2 -- 1 file changed, 2 deletions(-) diff --git a/schema/suite_v2_0.xsd b/schema/suite_v2_0.xsd index c6752c27..51afeece 100644 --- a/schema/suite_v2_0.xsd +++ b/schema/suite_v2_0.xsd @@ -33,7 +33,6 @@ - @@ -145,7 +144,6 @@ - From 1b80a4beae66200e5c52f4d20517a02ce638488c Mon Sep 17 00:00:00 2001 From: Steve Goldhaber Date: Sun, 30 Nov 2025 23:31:51 +0100 Subject: [PATCH 11/12] Added tracking and trapping of circular dependencies, partially reverted and with additional bug fixes and updates. Added test/unit_tests to GitHub actions. --- .github/workflows/capgen_unit_tests.yaml | 16 +- .github/workflows/python.yaml | 7 + scripts/ccpp_suite.py | 9 +- scripts/parse_tools/xml_tools.py | 13 +- .../sample_suite_files/another_suite.xml | 10 + .../sample_suite_files/another_suite2.xml | 16 + .../sample_suite_files/nested_full_suite.xml | 10 + .../sample_suite_files/subsuite1.xml | 7 + .../sample_suite_files/subsuite_inline.xml | 9 + .../suite_bad_v2_duplicate_group.xml | 16 + .../suite_bad_v2_suite_tag.xml | 7 + .../suite_bad_version01.xml | 8 + .../suite_bad_version02.xml | 8 + .../suite_bad_version03.xml | 8 + .../suite_bad_version04.xml | 8 + .../suite_good_v1_test01.xml | 8 + .../suite_good_v1_test02.xml | 11 + .../suite_good_v2_test01.xml | 9 + .../suite_good_v2_test01_exp.xml | 11 + .../suite_good_v2_test02.xml | 10 + .../suite_good_v2_test02_exp.xml | 13 + .../suite_good_v2_test03.xml | 19 + .../suite_good_v2_test03_exp.xml | 30 + .../suite_good_v2_test04.xml | 18 + .../suite_good_v2_test04_exp.xml | 26 + .../sample_suite_files/suite_missing_file.xml | 9 + .../suite_missing_group.xml | 7 + .../suite_missing_loaded_suite.xml | 16 + .../suite_missing_version.xml | 8 + .../suite_recurse_level2.xml | 10 + .../suite_recurse_level2a.xml | 10 + .../suite_recurse_level3.xml | 10 + .../suite_recurse_level3a.xml | 10 + .../sample_suite_files/suite_recurse_top1.xml | 18 + .../sample_suite_files/suite_recurse_top2.xml | 18 + test/unit_tests/test_common.py | 0 test/unit_tests/test_metadata_table.py | 0 test/unit_tests/test_sdf.py | 548 ++++++++++++++++++ test/unit_tests/test_var_transforms.py | 0 39 files changed, 954 insertions(+), 17 deletions(-) create mode 100644 test/unit_tests/sample_suite_files/another_suite.xml create mode 100644 test/unit_tests/sample_suite_files/another_suite2.xml create mode 100644 test/unit_tests/sample_suite_files/nested_full_suite.xml create mode 100644 test/unit_tests/sample_suite_files/subsuite1.xml create mode 100644 test/unit_tests/sample_suite_files/subsuite_inline.xml create mode 100644 test/unit_tests/sample_suite_files/suite_bad_v2_duplicate_group.xml create mode 100644 test/unit_tests/sample_suite_files/suite_bad_v2_suite_tag.xml create mode 100644 test/unit_tests/sample_suite_files/suite_bad_version01.xml create mode 100644 test/unit_tests/sample_suite_files/suite_bad_version02.xml create mode 100644 test/unit_tests/sample_suite_files/suite_bad_version03.xml create mode 100644 test/unit_tests/sample_suite_files/suite_bad_version04.xml create mode 100644 test/unit_tests/sample_suite_files/suite_good_v1_test01.xml create mode 100644 test/unit_tests/sample_suite_files/suite_good_v1_test02.xml create mode 100644 test/unit_tests/sample_suite_files/suite_good_v2_test01.xml create mode 100644 test/unit_tests/sample_suite_files/suite_good_v2_test01_exp.xml create mode 100644 test/unit_tests/sample_suite_files/suite_good_v2_test02.xml create mode 100644 test/unit_tests/sample_suite_files/suite_good_v2_test02_exp.xml create mode 100644 test/unit_tests/sample_suite_files/suite_good_v2_test03.xml create mode 100644 test/unit_tests/sample_suite_files/suite_good_v2_test03_exp.xml create mode 100644 test/unit_tests/sample_suite_files/suite_good_v2_test04.xml create mode 100644 test/unit_tests/sample_suite_files/suite_good_v2_test04_exp.xml create mode 100644 test/unit_tests/sample_suite_files/suite_missing_file.xml create mode 100644 test/unit_tests/sample_suite_files/suite_missing_group.xml create mode 100644 test/unit_tests/sample_suite_files/suite_missing_loaded_suite.xml create mode 100644 test/unit_tests/sample_suite_files/suite_missing_version.xml create mode 100644 test/unit_tests/sample_suite_files/suite_recurse_level2.xml create mode 100644 test/unit_tests/sample_suite_files/suite_recurse_level2a.xml create mode 100644 test/unit_tests/sample_suite_files/suite_recurse_level3.xml create mode 100644 test/unit_tests/sample_suite_files/suite_recurse_level3a.xml create mode 100644 test/unit_tests/sample_suite_files/suite_recurse_top1.xml create mode 100644 test/unit_tests/sample_suite_files/suite_recurse_top2.xml mode change 100755 => 100644 test/unit_tests/test_common.py mode change 100755 => 100644 test/unit_tests/test_metadata_table.py create mode 100644 test/unit_tests/test_sdf.py mode change 100755 => 100644 test/unit_tests/test_var_transforms.py diff --git a/.github/workflows/capgen_unit_tests.yaml b/.github/workflows/capgen_unit_tests.yaml index 7247100f..dbb74fc5 100644 --- a/.github/workflows/capgen_unit_tests.yaml +++ b/.github/workflows/capgen_unit_tests.yaml @@ -25,7 +25,11 @@ jobs: python3 \ git \ libxml2-utils - pip install --user pytest + python -m pip install --upgrade pip + pip install pytest + which xmllint + xmllint --version + which pytest - name: Build the framework run: | @@ -42,11 +46,11 @@ jobs: run: | BUILD_DIR=./build \ PYTHONPATH=test/:scripts/ \ - pytest \ - test/capgen_test/capgen_test_reports.py \ - test/advection_test/advection_test_reports.py \ - test/ddthost_test/ddthost_test_reports.py \ - test/var_compatibility_test/var_compatibility_test_reports.py + pytest \ + test/capgen_test/capgen_test_reports.py \ + test/advection_test/advection_test_reports.py \ + test/ddthost_test/ddthost_test_reports.py \ + test/var_compatibility_test/var_compatibility_test_reports.py - name: Run Fortran to metadata test run: cd test && ./test_fortran_to_metadata.sh diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml index 791c5084..08aa7962 100644 --- a/.github/workflows/python.yaml +++ b/.github/workflows/python.yaml @@ -20,8 +20,15 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | + sudo apt-get update + sudo apt-get install -y \ + libxml2-utils python -m pip install --upgrade pip pip install pytest + which xmllint + xmllint --version + which pytest + - name: Test with pytest run: | export PYTHONPATH=$(pwd)/scripts:$(pwd)/scripts/parse_tools diff --git a/scripts/ccpp_suite.py b/scripts/ccpp_suite.py index 5f47c22d..d1a6e968 100644 --- a/scripts/ccpp_suite.py +++ b/scripts/ccpp_suite.py @@ -678,9 +678,7 @@ def __init__(self, sdfs, host_model, scheme_headers, run_env): self.__context = ParseContext(filename=sdf) # Validate the XML file schema_version = find_schema_version(xml_root) - res = validate_xml_file(sdf, 'suite', schema_version, run_env.logger) - if not res: - raise CCPPError(f"Invalid suite definition file, '{sdf}'") + _ = validate_xml_file(sdf, 'suite', schema_version, run_env.logger) # Write the expanded sdf to the capgen output directory. # This file isn't used by capgen (everything is in memory @@ -693,7 +691,12 @@ def __init__(self, sdfs, host_model, scheme_headers, run_env): # Preprocess the sdf to expand nested suites if schema_version[0] == 2: expand_nested_suites(xml_root, sdf_path, logger=run_env.logger) + # For both versions 1 and 2, write the SDF (expanded for + # version 2, original for version 1) to the current directory write_xml_file(xml_root, sdf_expanded, run_env.logger) + # Validate the expanded SDF for version 2 + if schema_version[0] == 2: + _ = validate_xml_file(sdf, 'suite', schema_version, run_env.logger) suite = Suite(sdf, xml_root, self, run_env) suite.analyze(self.host_model, scheme_library, self.__ddt_lib, run_env) diff --git a/scripts/parse_tools/xml_tools.py b/scripts/parse_tools/xml_tools.py index ba3a5d57..980426fd 100644 --- a/scripts/parse_tools/xml_tools.py +++ b/scripts/parse_tools/xml_tools.py @@ -233,7 +233,7 @@ def read_xml_file(filename, logger=None): logger (logging.Logger, optional): Logger for warnings/errors. Returns: - tree (xml.etree.ElementTreet): The element tree from the input file. + tree (xml.etree.ElementTree): The element tree from the input file. root (xml.etree.ElementTree.Element): The root element of tree. Raises: @@ -308,9 +308,6 @@ def load_suite_by_name(suite_name, group_name, file, logger=None): """ _, root = read_xml_file(file, logger) schema_version = find_schema_version(root) - if schema_version[0] < 2: - raise CCPPError(f"XML schema version {schema_version} " + \ - f"invalid for nested suite {suite_name}") res = validate_xml_file(file, 'suite', schema_version, logger) if not res: raise CCPPError(f"Invalid suite definition file, '{sdf}'") @@ -415,7 +412,7 @@ def replace_nested_suite(element, nested_suite, default_path, logger): file = os.path.join(default_path, file) referenced_suite = load_suite_by_name(suite_name, group_name, file, logger=logger) - imported_content = [ET.fromstring(ET.tostring(child)) + imported_content = [ET.fromstring(ET.tostring(child)) for child in referenced_suite] # Swap nested suite with imported content for item in imported_content: @@ -581,8 +578,8 @@ def expand_nested_suites(suite, default_path, logger=None): keep_expanding = True if not keep_expanding: return - raise CCPPError("Exceeded number of iterations while expanding nested suites:" + \ - "check for inifite recursion or adjust limit max_iterations." + \ + raise CCPPError("Exceeded number of iterations while expanding nested suites. " + \ + "Check for infinite recursion or adjust limit max_iterations. " + \ f"Suites expanded so far: {suite_names}") ############################################################################### @@ -601,7 +598,7 @@ def remove_whitespace_nodes(node): # Convert ElementTree to a byte string byte_string = ET.tostring(root, 'us-ascii') - + # Parse string using minidom for pretty printing reparsed = xml.dom.minidom.parseString(byte_string) diff --git a/test/unit_tests/sample_suite_files/another_suite.xml b/test/unit_tests/sample_suite_files/another_suite.xml new file mode 100644 index 00000000..72346933 --- /dev/null +++ b/test/unit_tests/sample_suite_files/another_suite.xml @@ -0,0 +1,10 @@ + + + + + + another_scheme + + more_scheme + + diff --git a/test/unit_tests/sample_suite_files/another_suite2.xml b/test/unit_tests/sample_suite_files/another_suite2.xml new file mode 100644 index 00000000..def97177 --- /dev/null +++ b/test/unit_tests/sample_suite_files/another_suite2.xml @@ -0,0 +1,16 @@ + + + + + + another_scheme + + more_scheme + + + + another_scheme + + more_scheme + + diff --git a/test/unit_tests/sample_suite_files/nested_full_suite.xml b/test/unit_tests/sample_suite_files/nested_full_suite.xml new file mode 100644 index 00000000..2979f3ff --- /dev/null +++ b/test/unit_tests/sample_suite_files/nested_full_suite.xml @@ -0,0 +1,10 @@ + + + + + g1_scheme1 + + + + + diff --git a/test/unit_tests/sample_suite_files/subsuite1.xml b/test/unit_tests/sample_suite_files/subsuite1.xml new file mode 100644 index 00000000..c58ed752 --- /dev/null +++ b/test/unit_tests/sample_suite_files/subsuite1.xml @@ -0,0 +1,7 @@ + + + + + scheme_subsuite1 + + diff --git a/test/unit_tests/sample_suite_files/subsuite_inline.xml b/test/unit_tests/sample_suite_files/subsuite_inline.xml new file mode 100644 index 00000000..706ea801 --- /dev/null +++ b/test/unit_tests/sample_suite_files/subsuite_inline.xml @@ -0,0 +1,9 @@ + + + + + scheme1i + scheme2i + scheme1i + + diff --git a/test/unit_tests/sample_suite_files/suite_bad_v2_duplicate_group.xml b/test/unit_tests/sample_suite_files/suite_bad_v2_duplicate_group.xml new file mode 100644 index 00000000..8ea72077 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_bad_v2_duplicate_group.xml @@ -0,0 +1,16 @@ + + + + + + effr_pre + + + scheme9 + + + scheme3 + + + + diff --git a/test/unit_tests/sample_suite_files/suite_bad_v2_suite_tag.xml b/test/unit_tests/sample_suite_files/suite_bad_v2_suite_tag.xml new file mode 100644 index 00000000..6bc4f424 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_bad_v2_suite_tag.xml @@ -0,0 +1,7 @@ + + + + + subsuite_inline + + diff --git a/test/unit_tests/sample_suite_files/suite_bad_version01.xml b/test/unit_tests/sample_suite_files/suite_bad_version01.xml new file mode 100644 index 00000000..ecee0c63 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_bad_version01.xml @@ -0,0 +1,8 @@ + + + + + scheme1 + scheme2 + + diff --git a/test/unit_tests/sample_suite_files/suite_bad_version02.xml b/test/unit_tests/sample_suite_files/suite_bad_version02.xml new file mode 100644 index 00000000..55aff67a --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_bad_version02.xml @@ -0,0 +1,8 @@ + + + + + scheme1 + scheme2 + + diff --git a/test/unit_tests/sample_suite_files/suite_bad_version03.xml b/test/unit_tests/sample_suite_files/suite_bad_version03.xml new file mode 100644 index 00000000..794bfe7b --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_bad_version03.xml @@ -0,0 +1,8 @@ + + + + + scheme1 + scheme2 + + diff --git a/test/unit_tests/sample_suite_files/suite_bad_version04.xml b/test/unit_tests/sample_suite_files/suite_bad_version04.xml new file mode 100644 index 00000000..aaaab154 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_bad_version04.xml @@ -0,0 +1,8 @@ + + + + + scheme1 + scheme2 + + diff --git a/test/unit_tests/sample_suite_files/suite_good_v1_test01.xml b/test/unit_tests/sample_suite_files/suite_good_v1_test01.xml new file mode 100644 index 00000000..0eee366b --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_good_v1_test01.xml @@ -0,0 +1,8 @@ + + + + + scheme1 + scheme2 + + diff --git a/test/unit_tests/sample_suite_files/suite_good_v1_test02.xml b/test/unit_tests/sample_suite_files/suite_good_v1_test02.xml new file mode 100644 index 00000000..3d355cc5 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_good_v1_test02.xml @@ -0,0 +1,11 @@ + + + + + scheme1 + scheme2 + scheme3 + scheme2 + scheme1 + + diff --git a/test/unit_tests/sample_suite_files/suite_good_v2_test01.xml b/test/unit_tests/sample_suite_files/suite_good_v2_test01.xml new file mode 100644 index 00000000..73c30732 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_good_v2_test01.xml @@ -0,0 +1,9 @@ + + + + + scheme5 + + scheme9 + + diff --git a/test/unit_tests/sample_suite_files/suite_good_v2_test01_exp.xml b/test/unit_tests/sample_suite_files/suite_good_v2_test01_exp.xml new file mode 100644 index 00000000..dcecf218 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_good_v2_test01_exp.xml @@ -0,0 +1,11 @@ + + + + + scheme5 + scheme1i + scheme2i + scheme1i + scheme9 + + diff --git a/test/unit_tests/sample_suite_files/suite_good_v2_test02.xml b/test/unit_tests/sample_suite_files/suite_good_v2_test02.xml new file mode 100644 index 00000000..c7b5b8c9 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_good_v2_test02.xml @@ -0,0 +1,10 @@ + + + + + + scheme6 + + + + diff --git a/test/unit_tests/sample_suite_files/suite_good_v2_test02_exp.xml b/test/unit_tests/sample_suite_files/suite_good_v2_test02_exp.xml new file mode 100644 index 00000000..6b100283 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_good_v2_test02_exp.xml @@ -0,0 +1,13 @@ + + + + + + scheme6 + + + another_scheme + + more_scheme + + diff --git a/test/unit_tests/sample_suite_files/suite_good_v2_test03.xml b/test/unit_tests/sample_suite_files/suite_good_v2_test03.xml new file mode 100644 index 00000000..eff15298 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_good_v2_test03.xml @@ -0,0 +1,19 @@ + + + + + scheme13 + + effr_pre + + + main_calc + + + main_post + + + + + + diff --git a/test/unit_tests/sample_suite_files/suite_good_v2_test03_exp.xml b/test/unit_tests/sample_suite_files/suite_good_v2_test03_exp.xml new file mode 100644 index 00000000..5f9a9987 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_good_v2_test03_exp.xml @@ -0,0 +1,30 @@ + + + + + scheme13 + + effr_pre + + + main_calc + + + main_post + + + another_scheme + + more_scheme + + another_scheme + + more_scheme + + + g1_scheme1 + + + scheme_subsuite1 + + diff --git a/test/unit_tests/sample_suite_files/suite_good_v2_test04.xml b/test/unit_tests/sample_suite_files/suite_good_v2_test04.xml new file mode 100644 index 00000000..abb87008 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_good_v2_test04.xml @@ -0,0 +1,18 @@ + + + + + + effr_pre + + + main_calc + + + main_post + + + + + + diff --git a/test/unit_tests/sample_suite_files/suite_good_v2_test04_exp.xml b/test/unit_tests/sample_suite_files/suite_good_v2_test04_exp.xml new file mode 100644 index 00000000..af103e7d --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_good_v2_test04_exp.xml @@ -0,0 +1,26 @@ + + + + + + effr_pre + + + main_calc + + + main_post + + + another_scheme + + more_scheme + + another_scheme + + more_scheme + + + scheme_subsuite1 + + diff --git a/test/unit_tests/sample_suite_files/suite_missing_file.xml b/test/unit_tests/sample_suite_files/suite_missing_file.xml new file mode 100644 index 00000000..8c7b85e6 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_missing_file.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/test/unit_tests/sample_suite_files/suite_missing_group.xml b/test/unit_tests/sample_suite_files/suite_missing_group.xml new file mode 100644 index 00000000..a33078e6 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_missing_group.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/unit_tests/sample_suite_files/suite_missing_loaded_suite.xml b/test/unit_tests/sample_suite_files/suite_missing_loaded_suite.xml new file mode 100644 index 00000000..cf21a590 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_missing_loaded_suite.xml @@ -0,0 +1,16 @@ + + + + + + scheme23 + + + scheme9 + + + scheme3 + + + + diff --git a/test/unit_tests/sample_suite_files/suite_missing_version.xml b/test/unit_tests/sample_suite_files/suite_missing_version.xml new file mode 100644 index 00000000..463c56d9 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_missing_version.xml @@ -0,0 +1,8 @@ + + + + + scheme1 + scheme2 + + diff --git a/test/unit_tests/sample_suite_files/suite_recurse_level2.xml b/test/unit_tests/sample_suite_files/suite_recurse_level2.xml new file mode 100644 index 00000000..35fed8b2 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_recurse_level2.xml @@ -0,0 +1,10 @@ + + + + + scheme13 + scheme3 + + scheme43 + + diff --git a/test/unit_tests/sample_suite_files/suite_recurse_level2a.xml b/test/unit_tests/sample_suite_files/suite_recurse_level2a.xml new file mode 100644 index 00000000..2e018e39 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_recurse_level2a.xml @@ -0,0 +1,10 @@ + + + + + scheme13 + scheme3 + scheme43 + + + diff --git a/test/unit_tests/sample_suite_files/suite_recurse_level3.xml b/test/unit_tests/sample_suite_files/suite_recurse_level3.xml new file mode 100644 index 00000000..f38a9d8f --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_recurse_level3.xml @@ -0,0 +1,10 @@ + + + + + scheme13 + scheme3 + + scheme43 + + diff --git a/test/unit_tests/sample_suite_files/suite_recurse_level3a.xml b/test/unit_tests/sample_suite_files/suite_recurse_level3a.xml new file mode 100644 index 00000000..a11182dc --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_recurse_level3a.xml @@ -0,0 +1,10 @@ + + + + + scheme13 + scheme3 + scheme43 + + + diff --git a/test/unit_tests/sample_suite_files/suite_recurse_top1.xml b/test/unit_tests/sample_suite_files/suite_recurse_top1.xml new file mode 100644 index 00000000..8e8c4f1a --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_recurse_top1.xml @@ -0,0 +1,18 @@ + + + + + scheme13 + + scheme23 + + + scheme9 + + + scheme3 + + + scheme43 + + diff --git a/test/unit_tests/sample_suite_files/suite_recurse_top2.xml b/test/unit_tests/sample_suite_files/suite_recurse_top2.xml new file mode 100644 index 00000000..87ce7057 --- /dev/null +++ b/test/unit_tests/sample_suite_files/suite_recurse_top2.xml @@ -0,0 +1,18 @@ + + + + + scheme13 + + scheme23 + + + scheme9 + + + scheme3 + + scheme43 + + + diff --git a/test/unit_tests/test_common.py b/test/unit_tests/test_common.py old mode 100755 new mode 100644 diff --git a/test/unit_tests/test_metadata_table.py b/test/unit_tests/test_metadata_table.py old mode 100755 new mode 100644 diff --git a/test/unit_tests/test_sdf.py b/test/unit_tests/test_sdf.py new file mode 100644 index 00000000..f06d189f --- /dev/null +++ b/test/unit_tests/test_sdf.py @@ -0,0 +1,548 @@ +#! /usr/bin/env python3 +""" +----------------------------------------------------------------------- + Description: Contains unit tests for parsing Suite Definition Files (SDFs) + in scripts/parse_tools/xml_tools.py + + Assumptions: + + Command line arguments: none + + Usage: python3 test_sdf.py # run the unit tests +----------------------------------------------------------------------- +""" + +import filecmp +import glob +import logging +import os +import sys +import unittest +import xml.etree.ElementTree as ET + +_TEST_DIR = os.path.dirname(os.path.abspath(__file__)) +_SCRIPTS_DIR = os.path.abspath(os.path.join(_TEST_DIR, os.pardir, + os.pardir, "scripts")) +_SAMPLE_FILES_DIR = os.path.join(_TEST_DIR, "sample_suite_files") +_PRE_TMP_DIR = os.path.join(_TEST_DIR, "tmp") +_TMP_DIR = os.path.join(_PRE_TMP_DIR, "suite_files") + +if not os.path.exists(_SCRIPTS_DIR): + raise ImportError(f"Cannot find scripts directory, {_SCRIPTS_DIR}") + +sys.path.append(_SCRIPTS_DIR) + +# pylint: disable=wrong-import-position +from parse_tools import init_log +from parse_tools import read_xml_file, validate_xml_file, write_xml_file +from parse_tools import find_schema_version, expand_nested_suites +# pylint: enable=wrong-import-position + +class SDFParseTestCase(unittest.TestCase): + + """Tests for `expand_nested_suites` and related functions.""" + + logger = None + + @classmethod + def setUpClass(cls): + """Clean output directory (tmp) before running tests""" + # Does "tmp" directory exist? If not then create it: + if not os.path.exists(_PRE_TMP_DIR): + os.makedirs(_PRE_TMP_DIR) + # end if + + # We need a logger + cls.logger = init_log(cls.__name__, level=logging.WARNING) + + #Does "tmp" directory exist? If not then create it: + # Ensure the "tmp/suite_files" directory exists and is empty + if os.path.exists(_TMP_DIR): + # Clear out all files: + for fpath in glob.iglob(os.path.join(_TMP_DIR, '*.*')): + if os.path.exists(fpath): + os.remove(fpath) + # End if + # End for + else: + os.makedirs(_TMP_DIR) + # end if + + # Run inherited setup method: + super().setUpClass() + + @classmethod + def get_logger(cls): + return cls.logger + + @classmethod + def compare_text(cls, name, txt1, txt2, typ): + """Compare two XML text or tail items (which may be None). + Return None if items match, otherwise, return an error string""" + res = None + if txt1 and txt2: + if txt1.strip() != txt2.strip(): + res = f"{name} {typ}, '{txt1}', does not match {typ}, '{txt2}'" + # end if + elif txt1: + res = f"{name} {typ} is missing from string2" + elif txt2: + res = f"{name} {typ} is missing from string1" + else: + res = None + # end if + return res + + @classmethod + def xml_diff(cls, xt1, xt2): + """ + Compares two xml etrees, xt1 and xt2 + Return None if the trees match, otherwise, return a difference string + """ + + diffs = [] + # First, compare the XML tags + if xt1.tag != xt2.tag: + diffs.append(f"Tags do not match: {xt1.tag} != {xt2.tag}") + else: + # Compare the attributes + for name, value in xt1.attrib.items(): + if name not in xt2.attrib: + diffs.append(f"xt1 attribute, {name}, is missing in xt2") + else: + xt2v = xt2.attrib.get(name) + if xt2v != value: + diffs.append(f"Attributes for {name} do not match: {str(value)} != {str(xt2v)}") + # end if + # end if + # end for + for name in xt2.attrib.keys(): + if name not in xt1.attrib: + diffs.append(f"xt2 attribute, {name}, is missing in xt1") + # end if + # end for + # Compare the text bodies (if any) + tdiff = cls.compare_text(xt1.tag, xt1.text, xt2.text, "text") + if tdiff: + diffs.append(tdiff) + # end if + tdiff = cls.compare_text(xt1.tag, xt1.tail, xt2.tail, "tail") + if tdiff: + diffs.append(tdiff) + # end if + # Compare children + if len(xt1) != len(xt2): + diffs.append(f"Number of children length differs, {len(xt1)} != {len(xt2)}") + else: + for child1, child2 in zip(xt1, xt2): + kid_diffs = cls.xml_diff(child1, child2) + if kid_diffs: + diffs.extend(kid_diffs) + # end if + # end for + # end if + # end if + return diffs + + def test_xml_diff(self): + """Test that xml_diff catches xml differences""" + root1 = ET.fromstring("item") + root2 = ET.fromstring("item") + diffs = self.xml_diff(root1, root2) + self.assertTrue(diffs) + self.assertEqual(len(diffs), 1) + self.assertTrue("Tags do not match" in diffs[0], + msg="tag1 should not match taga") + root1 = ET.fromstring("item1") + root2 = ET.fromstring("item2") + diffs = self.xml_diff(root1, root2) + self.assertTrue(diffs) + self.assertEqual(len(diffs), 1) + self.assertTrue("does not match" in diffs[0], + msg="item1 should not match item2") + root1 = ET.fromstring('item1') + root2 = ET.fromstring('item1') + diffs = self.xml_diff(root1, root2) + self.assertTrue(diffs) + self.assertEqual(len(diffs), 3) + self.assertTrue("Attributes for" in diffs[0] and "do not match" in diffs[0], + msg="attrib1 values should not match") + self.assertTrue("xt1 attribute, attrib2, is missing in xt2" in diffs[1], + msg=f"attrib2 is missing in root2") + self.assertTrue("xt2 attribute, attrib3, is missing in xt1" in diffs[2], + msg=f"attrib3 is missing in root1") + root1 = ET.fromstring('') + root2 = ET.fromstring('') + diffs = self.xml_diff(root1, root2) + self.assertEqual(len(diffs), 1) + self.assertTrue("Attributes for" in diffs[0] and "do not match" in diffs[0], + msg=f"attrib2 values should not match") + + def test_good_v1_sdf(self): + """Test that the parser recognizes a V1 SDF and parses it correctly + """ + num_tests = 2 + header = "Test of parsing of good V1 SDF" + for test_num in range(num_tests): + # Setup + testname = f"suite_good_v1_test{test_num+1:{0}{2}}" + source = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.xml") + compare = os.path.join(_TMP_DIR, f"{testname}_out.xml") + logger = self.get_logger() + # Exercise + _, xml_root = read_xml_file(source, logger) + schema_version = find_schema_version(xml_root) + self.assertEqual(schema_version[0], 1) + self.assertEqual(schema_version[1], 0) + res = validate_xml_file(source, 'suite', schema_version, logger, + error_on_noxmllint=True) + self.assertTrue(res) + write_xml_file(xml_root, compare, logger) + amsg = f"{compare} does not exist" + self.assertTrue(os.path.exists(compare), msg=amsg) + _, compare_root = read_xml_file(compare, logger) + diffs = self.xml_diff(xml_root, compare_root) + lsep = '\n' + amsg = f"{source} does not match {compare}\n{lsep.join(diffs)}" + self.assertFalse(diffs, msg=amsg) + # end for + + def test_good_v2_sdf_01(self): + """Test that the parser recognizes a V2 SDF and parses and + expands it correctly. + Test the expansion of one group of a simple nested suite at group level. + """ + header = "Test of parsing of good V2 SDF" + # Setup + testname = "suite_good_v2_test01" + source = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.xml") + source_exp = os.path.join(_SAMPLE_FILES_DIR, f"{testname}_exp.xml") + compare = os.path.join(_TMP_DIR, f"{testname}_out.xml") + logger = self.get_logger() + # Exercise + _, xml_root = read_xml_file(source, logger) + schema_version = find_schema_version(xml_root) + self.assertEqual(schema_version[0], 2) + self.assertEqual(schema_version[1], 0) + expand_nested_suites(xml_root, _SAMPLE_FILES_DIR, logger=logger) + write_xml_file(xml_root, compare, logger) + res = validate_xml_file(source, 'suite', schema_version, logger, + error_on_noxmllint=True) + self.assertTrue(res) + amsg = f"{compare} does not exist" + self.assertTrue(os.path.exists(compare), msg=amsg) + _, xml_root = read_xml_file(source_exp, logger) + _, compare_root = read_xml_file(compare, logger) + diffs = self.xml_diff(xml_root, compare_root) + lsep = '\n' + amsg = f"{source_exp} does not match {compare}\n{lsep.join(diffs)}" + self.assertFalse(diffs, msg=amsg) + + def test_good_v2_sdf_02(self): + """Test that the parser recognizes a V2 SDF and parses and + expands it correctly + Test the expansion of one group of a multiple group nested suite at group level. + """ + header = "Test of parsing of good V2 SDF" + # Setup + testname = "suite_good_v2_test02" + source = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.xml") + source_exp = os.path.join(_SAMPLE_FILES_DIR, f"{testname}_exp.xml") + compare = os.path.join(_TMP_DIR, f"{testname}_out.xml") + logger = self.get_logger() + # Exercise + _, xml_root = read_xml_file(source, logger) + schema_version = find_schema_version(xml_root) + self.assertEqual(schema_version[0], 2) + self.assertEqual(schema_version[1], 0) + expand_nested_suites(xml_root, _SAMPLE_FILES_DIR, logger=logger) + write_xml_file(xml_root, compare, logger) + res = validate_xml_file(source, 'suite', schema_version, logger, + error_on_noxmllint=True) + self.assertTrue(res) + amsg = f"{compare} does not exist" + self.assertTrue(os.path.exists(compare), msg=amsg) + _, xml_root = read_xml_file(source_exp, logger) + _, compare_root = read_xml_file(compare, logger) + diffs = self.xml_diff(xml_root, compare_root) + lsep = '\n' + amsg = f"{source_exp} does not match {compare}\n{lsep.join(diffs)}" + self.assertFalse(diffs, msg=amsg) + + def test_good_v2_sdf_03(self): + """Test that the parser recognizes a V2 SDF and parses and + expands it correctly + Test expansion of two nested suites at group level plus a full nested suite at + suite level. + """ + header = "Test of parsing of good V2 SDF" + # Setup + testname = "suite_good_v2_test03" + source = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.xml") + source_exp = os.path.join(_SAMPLE_FILES_DIR, f"{testname}_exp.xml") + compare = os.path.join(_TMP_DIR, f"{testname}_out.xml") + logger = self.get_logger() + # Exercise + _, xml_root = read_xml_file(source, logger) + schema_version = find_schema_version(xml_root) + self.assertEqual(schema_version[0], 2) + self.assertEqual(schema_version[1], 0) + expand_nested_suites(xml_root, _SAMPLE_FILES_DIR, logger=logger) + write_xml_file(xml_root, compare, logger) + res = validate_xml_file(source, 'suite', schema_version, logger, + error_on_noxmllint=True) + self.assertTrue(res) + amsg = f"{compare} does not exist" + self.assertTrue(os.path.exists(compare), msg=amsg) + _, xml_root = read_xml_file(source_exp, logger) + _, compare_root = read_xml_file(compare, logger) + diffs = self.xml_diff(xml_root, compare_root) + lsep = '\n' + amsg = f"{source_exp} does not match {compare}\n{lsep.join(diffs)}" + self.assertFalse(diffs, msg=amsg) + + def test_good_v2_sdf_04(self): + """Test that the parser recognizes a V2 SDF and parses and + expands it correctly + Test expansion of two nested suites at group level plus one group from a + nested suite at suite level. + """ + header = "Test of parsing of good V2 SDF" + # Setup + testname = "suite_good_v2_test04" + source = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.xml") + source_exp = os.path.join(_SAMPLE_FILES_DIR, f"{testname}_exp.xml") + compare = os.path.join(_TMP_DIR, f"{testname}_out.xml") + logger = self.get_logger() + # Exercise + _, xml_root = read_xml_file(source, logger) + schema_version = find_schema_version(xml_root) + self.assertEqual(schema_version[0], 2) + self.assertEqual(schema_version[1], 0) + expand_nested_suites(xml_root, _SAMPLE_FILES_DIR, logger=logger) + write_xml_file(xml_root, compare, logger) + res = validate_xml_file(source, 'suite', schema_version, logger, + error_on_noxmllint=True) + self.assertTrue(res) + amsg = f"{compare} does not exist" + self.assertTrue(os.path.exists(compare), msg=amsg) + _, xml_root = read_xml_file(source_exp, logger) + _, compare_root = read_xml_file(compare, logger) + diffs = self.xml_diff(xml_root, compare_root) + lsep = '\n' + amsg = f"{source_exp} does not match {compare}\n{lsep.join(diffs)}" + self.assertFalse(diffs, msg=amsg) + + def test_bad_v2_suite_tag_sdf(self): + """Test that verification system recognizes a misplaced suite tag""" + header = "Test trapping of version attribute on a v2 suite tag" + # Setup + testname = f"suite_bad_v2_suite_tag" + source = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.xml") + logger = self.get_logger() + # Exercise + _, xml_root = read_xml_file(source, logger) + schema_version = find_schema_version(xml_root) + # Some versions of xmllint return an exit code 0 even if the + # validation fails. "Good" versions return an exit code /= 0, + # which then raises a CCPPError internally. The following + # logic handles the correct behavior (validation fails ==> + # exit code /= 0 ==> CCPPError). + try: + res = validate_xml_file(source, 'suite', schema_version, logger, + error_on_noxmllint=True) + except Exception as e: + emsg = "Schemas validity error : Element 'suite': This element is not expected." + msg = str(e) + self.assertTrue(emsg in msg) + + def test_bad_v2_suite_duplicate_group1(self): + """Test that verification system recognizes a duplicate group name""" + header = "Test trapping of expanded suite duplicate group name" + # Setup + testname = f"suite_bad_v2_duplicate_group" + source = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.xml") + compare = os.path.join(_TMP_DIR, f"{testname}_out.xml") + logger = self.get_logger() + # Exercise + _, xml_root = read_xml_file(source, logger) + schema_version = find_schema_version(xml_root) + self.assertEqual(schema_version[0], 2) + self.assertEqual(schema_version[1], 0) + res = validate_xml_file(source, 'suite', schema_version, logger, + error_on_noxmllint=True) + self.assertTrue(res, msg="Initial suite file should be valid") + with self.assertRaises(Exception) as context: + expand_nested_suites(xml_root, _SAMPLE_FILES_DIR, logger=logger) + write_xml_file(xml_root, compare, logger) + _ = validate_xml_file(compare, 'suite', schema_version, logger) + # end with + emsg = "Schemas validity error : Element 'group', attribute 'name': " + \ + "'group1' is not a valid value of the atomic type 'xs:ID'" + fmsg = str(context.exception) + self.assertTrue(emsg in fmsg, msg=fmsg) + if not emsg in fmsg: + raise context + + def test_bad_v2_suite_missing_group(self): + """Test that verification system recognizes a missing group name""" + header = "Test trapping of expanded suite missing group name" + # Setup + testname = f"suite_missing_group" + source = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.xml") + compare = os.path.join(_TMP_DIR, f"{testname}_out.xml") + logger = self.get_logger() + # Exercise + _, xml_root = read_xml_file(source, logger) + schema_version = find_schema_version(xml_root) + self.assertEqual(schema_version[0], 2) + self.assertEqual(schema_version[1], 0) + res = validate_xml_file(source, 'suite', schema_version, logger, + error_on_noxmllint=True) + self.assertTrue(res, msg="Initial suite file should be valid") + with self.assertRaises(Exception) as context: + expand_nested_suites(xml_root, _SAMPLE_FILES_DIR, logger=logger) + write_xml_file(xml_root, compare, logger) + # end with + emsg = "Nested suite subsuite_1, group group2, not found" + fmsg = str(context.exception) + self.assertTrue(emsg in fmsg, msg=fmsg) + + def test_bad_v2_suite_missing_file(self): + """Test that verification system recognizes a missing file argument""" + header = "Test trapping of missing file for nested suite" + # Setup + testname = f"suite_missing_file" + source = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.xml") + compare = os.path.join(_TMP_DIR, f"{testname}_out.xml") + logger = self.get_logger() + # Exercise + _, xml_root = read_xml_file(source, logger) + schema_version = find_schema_version(xml_root) + self.assertEqual(schema_version[0], 2) + self.assertEqual(schema_version[1], 0) + # See note about different behavior of xmllint versions + # in test test_bad_v2_suite_tag_sdf above. + try: + res = validate_xml_file(source, 'suite', schema_version, logger, + error_on_noxmllint=True) + except Exception as e: + emsg = "Schemas validity error : Element 'nested_suite': " + \ + "The attribute 'file' is required but missing." + msg = str(e) + self.assertTrue(emsg in msg) + + def test_bad_v2_suite_missing_loaded_suite(self): + """Test that verification system recognizes a missing suite loaded + from another file""" + header = "Test trapping of expanded suite missing a subsuite in a different file" + # Setup + testname = f"suite_missing_loaded_suite" + source = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.xml") + compare = os.path.join(_TMP_DIR, f"{testname}_out.xml") + logger = self.get_logger() + # Exercise + _, xml_root = read_xml_file(source, logger) + schema_version = find_schema_version(xml_root) + self.assertEqual(schema_version[0], 2) + self.assertEqual(schema_version[1], 0) + res = validate_xml_file(source, 'suite', schema_version, logger, + error_on_noxmllint=True) + self.assertTrue(res, msg="Initial suite file should be valid") + with self.assertRaises(Exception) as context: + expand_nested_suites(xml_root, _SAMPLE_FILES_DIR, logger=logger) + write_xml_file(xml_root, compare, logger) + # end with + emsg = "Nested suite v12_suite, group main_group, not found in file" + fmsg = str(context.exception) + self.assertTrue(emsg in fmsg, msg=fmsg) + + def test_bad_v2_suite_infinite_group_recursion(self): + """Test that verification system recognizes infinite recursion when + including at the group level""" + header = "Test trapping of expanded suite with infinite group recursion" + # Setup + testname = f"suite_recurse_top1" + source = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.xml") + compare = os.path.join(_TMP_DIR, f"{testname}_out.xml") + logger = self.get_logger() + # Exercise + _, xml_root = read_xml_file(source, logger) + schema_version = find_schema_version(xml_root) + self.assertEqual(schema_version[0], 2) + self.assertEqual(schema_version[1], 0) + res = validate_xml_file(source, 'suite', schema_version, logger, + error_on_noxmllint=True) + self.assertTrue(res, msg="Initial suite file should be valid") + with self.assertRaises(Exception) as context: + expand_nested_suites(xml_root, _SAMPLE_FILES_DIR, logger=logger) + write_xml_file(xml_root, compare, logger) + # end with + emsg = ("Exceeded number of iterations while expanding nested suites") + fmsg = str(context.exception) + self.assertTrue(emsg in fmsg, msg=fmsg) + + def test_bad_v2_suite_infinite_suite_recursion(self): + """Test that verification system recognizes infinite recursion when + including at the imported suite level""" + header = "Test trapping of expanded suite with infinite suite recursion" + # Setup + testname = f"suite_recurse_top2" + source = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.xml") + compare = os.path.join(_TMP_DIR, f"{testname}_out.xml") + logger = self.get_logger() + # Exercise + _, xml_root = read_xml_file(source, logger) + schema_version = find_schema_version(xml_root) + self.assertEqual(schema_version[0], 2) + self.assertEqual(schema_version[1], 0) + res = validate_xml_file(source, 'suite', schema_version, logger, + error_on_noxmllint=True) + self.assertTrue(res, msg="Initial suite file should be valid") + with self.assertRaises(Exception) as context: + expand_nested_suites(xml_root, _SAMPLE_FILES_DIR, logger=logger) + write_xml_file(xml_root, compare, logger) + # end with + emsg = ("Exceeded number of iterations while expanding nested suites") + fmsg = str(context.exception) + self.assertTrue(emsg in fmsg, msg=fmsg) + + def test_bad_schema_version(self): + """Test that verification system recognizes a bad version entry""" + num_tests = 4 + header = "Test trapping of invalid SDF version" + exc_strings = ["Format must be .", + "Format must be .", + "Major version must be at least 1", + "Minor version must be non-negative"] + for test_num in range(num_tests): + # Setup + testname = f"suite_bad_version{test_num+1:{0}{2}}" + source = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.xml") + logger = self.get_logger() + # Exercise + with self.assertRaises(Exception) as context: + _, xml_root = read_xml_file(source, logger) + schema_version = find_schema_version(xml_root) + # end with + # Check exception for expected error messages + exp_str = str(context.exception) + self.assertTrue(exc_strings[test_num] in exp_str, + msg=f"Bad exception in test {test_num + 1}, '{exp_str}'") + # end for + + def test_missing_schema_version(self): + """Test that verification system recognizes a missing version num""" + header = "Test trapping of missing SDF version" + # Setup + testname = f"suite_missing_version" + source = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.xml") + logger = self.get_logger() + # Exercise + with self.assertRaises(Exception) as context: + _, xml_root = read_xml_file(source, logger) + schema_version = find_schema_version(xml_root) + # end with + # Check exception for expected error messages + self.assertTrue("version attribute required" in str(context.exception), + msg=f"Bad exception for missing suite version") diff --git a/test/unit_tests/test_var_transforms.py b/test/unit_tests/test_var_transforms.py old mode 100755 new mode 100644 From afbf356a77f7de0e0de7faf33eae4d63112e8001 Mon Sep 17 00:00:00 2001 From: Dom Heinzeller Date: Tue, 2 Dec 2025 15:01:29 -0700 Subject: [PATCH 12/12] Remove no-longer-needed logic to inject file attribute for nested suite in scripts/parse_tools/xml_tools.py --- scripts/parse_tools/xml_tools.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/scripts/parse_tools/xml_tools.py b/scripts/parse_tools/xml_tools.py index 980426fd..ed50dd43 100644 --- a/scripts/parse_tools/xml_tools.py +++ b/scripts/parse_tools/xml_tools.py @@ -416,12 +416,6 @@ def replace_nested_suite(element, nested_suite, default_path, logger): for child in referenced_suite] # Swap nested suite with imported content for item in imported_content: - # If the imported content comes from a separate file and has - # nested suites that are within that separate file, then we - # need to inject the file attribute here. - if item.tag == "nested_suite": - if file and not item.attrib.get("file"): - item.set("file", file) # If we are inserting a nested suite at the suite level (element.tag is suite), # but we only want one group (group_name is not none), then we need to wrap # the item in a group element. If on the other hand we insert an entire suite