diff --git a/testing/functional_class.py b/testing/functional_class.py index 6ca085ef2c..50da8e6c5b 100644 --- a/testing/functional_class.py +++ b/testing/functional_class.py @@ -1,3 +1,5 @@ +import os +import urllib.request from abc import ABC, abstractmethod from utils import str_to_bool, str_to_list @@ -5,15 +7,28 @@ class FunctionalTest(ABC): """Class for running FATES functional tests""" def __init__(self, name:str, test_dir:str, test_exe:str, out_file:str, - use_param_file:str, other_args:str): + use_param_file:str, datm_file:str, datm_file_url:str, other_args:str): self.name = name self.test_dir = test_dir self.test_exe = test_exe self.out_file = out_file self.use_param_file = str_to_bool(use_param_file) + self.datm_file = None self.other_args = str_to_list(other_args) self.plot = False - + + # Check that datm exists and save its absolute path + if datm_file: + self.datm_file = os.path.abspath(datm_file) + if not os.path.exists(self.datm_file): + if not datm_file_url: + raise FileNotFoundError(f"datm_file not found: '{self.datm_file}'") + datm_file_dir = os.path.dirname(self.datm_file) + if not os.path.isdir(datm_file_dir): + os.makedirs(datm_file_dir) + print(f"Downloading datm_file from {datm_file_url}") + urllib.request.urlretrieve(datm_file_url, self.datm_file) + @abstractmethod def plot_output(self, run_dir:str, save_figs:bool, plot_dir:str): pass diff --git a/testing/functional_testing/allometry/allometry_test.py b/testing/functional_testing/allometry/allometry_test.py index bb24ab3729..d75fd8f279 100644 --- a/testing/functional_testing/allometry/allometry_test.py +++ b/testing/functional_testing/allometry/allometry_test.py @@ -23,6 +23,8 @@ def __init__(self, test_dict): test_dict["test_exe"], test_dict["out_file"], test_dict["use_param_file"], + test_dict["datm_file"], + test_dict["datm_file_url"], test_dict["other_args"], ) self.plot = True diff --git a/testing/functional_testing/fire/fuel/fuel_test.py b/testing/functional_testing/fire/fuel/fuel_test.py index b9cd151621..9a6ec034bc 100644 --- a/testing/functional_testing/fire/fuel/fuel_test.py +++ b/testing/functional_testing/fire/fuel/fuel_test.py @@ -20,6 +20,8 @@ def __init__(self, test_dict): test_dict["test_exe"], test_dict["out_file"], test_dict["use_param_file"], + test_dict["datm_file"], + test_dict["datm_file_url"], test_dict["other_args"], ) self.plot = True diff --git a/testing/functional_testing/fire/ros/ros_test.py b/testing/functional_testing/fire/ros/ros_test.py index e845040fdb..8bdec046d8 100644 --- a/testing/functional_testing/fire/ros/ros_test.py +++ b/testing/functional_testing/fire/ros/ros_test.py @@ -26,6 +26,8 @@ def __init__(self, test_dict): test_dict["test_exe"], test_dict["out_file"], test_dict["use_param_file"], + test_dict["datm_file"], + test_dict["datm_file_url"], test_dict["other_args"], ) self.plot = True diff --git a/testing/functional_testing/fire/shr/SyntheticFuelModels.F90 b/testing/functional_testing/fire/shr/SyntheticFuelModels.F90 index ce1c8e85e2..aab21bab41 100644 --- a/testing/functional_testing/fire/shr/SyntheticFuelModels.F90 +++ b/testing/functional_testing/fire/shr/SyntheticFuelModels.F90 @@ -178,7 +178,7 @@ integer function FuelModelPosition(this, fuel_model_index) end if end do write(*, '(a, i2, a)') "Cannot find the fuel model index ", fuel_model_index, "." - stop + call abort() end function FuelModelPosition diff --git a/testing/functional_testing/math_utils/math_utils_test.py b/testing/functional_testing/math_utils/math_utils_test.py index 579838df18..b9c82fae83 100644 --- a/testing/functional_testing/math_utils/math_utils_test.py +++ b/testing/functional_testing/math_utils/math_utils_test.py @@ -22,6 +22,8 @@ def __init__(self, test_dict): test_dict["test_exe"], test_dict["out_file"], test_dict["use_param_file"], + test_dict["datm_file"], + test_dict["datm_file_url"], test_dict["other_args"], ) self.plot = True diff --git a/testing/functional_testing/patch/patch_test.py b/testing/functional_testing/patch/patch_test.py index 7d4fec3e8d..c0201b6a2b 100644 --- a/testing/functional_testing/patch/patch_test.py +++ b/testing/functional_testing/patch/patch_test.py @@ -23,6 +23,8 @@ def __init__(self, test_dict): test_dict["test_exe"], test_dict["out_file"], test_dict["use_param_file"], + test_dict["datm_file"], + test_dict["datm_file_url"], test_dict["other_args"], ) self.plot = True diff --git a/testing/functional_tests.cfg b/testing/functional_tests.cfg index 98d8448e42..d5bc1a3c23 100644 --- a/testing/functional_tests.cfg +++ b/testing/functional_tests.cfg @@ -3,6 +3,8 @@ test_dir = fates_allom_ftest test_exe = FATES_allom_exe out_file = allometry_out.nc use_param_file = True +datm_file = +datm_file_url = other_args = [] [quadratic] @@ -10,6 +12,8 @@ test_dir = fates_math_ftest test_exe = FATES_math_exe out_file = quad_out.nc use_param_file = False +datm_file = +datm_file_url = other_args = [] [fuel] @@ -17,13 +21,17 @@ test_dir = fates_fuel_ftest test_exe = FATES_fuel_exe out_file = fuel_out.nc use_param_file = True -other_args = ['../testing/test_data/BONA_datm.nc'] +datm_file = ../testing/test_data/BONA_datm.nc +datm_file_url = https://www.dropbox.com/scl/fi/l7ik0xhnww3snlk2lqngr/BONA_datm.nc?rlkey=15kwixoofokyyj936xkxmfq8t&e=1&dl=1 +other_args = [] [ros] test_dir = fates_ros_ftest test_exe = FATES_ros_exe out_file = ros_out.nc use_param_file = True +datm_file = +datm_file_url = other_args = [] [patch] @@ -31,4 +39,6 @@ test_dir = fates_patch_ftest test_exe = FATES_patch_exe out_file = None use_param_file = True +datm_file = +datm_file_url = other_args = [] diff --git a/testing/run_functional_tests.py b/testing/run_functional_tests.py index 105c22b06a..7186f76aa2 100755 --- a/testing/run_functional_tests.py +++ b/testing/run_functional_tests.py @@ -28,6 +28,7 @@ """ import os import argparse +import subprocess import matplotlib.pyplot as plt from build_fortran_tests import build_tests, build_exists @@ -39,12 +40,18 @@ add_cime_lib_to_path() -from CIME.utils import run_cmd_no_fail +from CIME.utils import run_cmd # constants for this script -_DEFAULT_CONFIG_FILE = "functional_tests.cfg" -_DEFAULT_CDL_PATH = os.path.abspath("../parameter_files/fates_params_default.cdl") -_CMAKE_BASE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../") +_FILE_DIR = os.path.dirname(__file__) +_DEFAULT_CONFIG_FILE = os.path.join(_FILE_DIR, "functional_tests.cfg") +_DEFAULT_CDL_PATH = os.path.abspath(os.path.join( + _FILE_DIR, + os.pardir, + "parameter_files", + "fates_params_default.cdl", +)) +_CMAKE_BASE_DIR = os.path.join(_FILE_DIR, os.pardir) _TEST_SUB_DIR = "testing" @@ -74,6 +81,13 @@ def commandline_args(): "parameter_files directory.\n", ) + parser.add_argument( + "--config-file", + type=str, + default=_DEFAULT_CONFIG_FILE, + help=f"Configuration file where test list is defined. Default: '{_DEFAULT_CONFIG_FILE}'", + ) + parser.add_argument( "-b", "--build-dir", @@ -196,7 +210,7 @@ def check_param_file(param_file): None, "Must supply parameter file with .cdl or .nc ending." ) if not os.path.isfile(param_file): - raise argparse.ArgumentError(None, f"Cannot find file {param_file}.") + raise FileNotFoundError(param_file) def check_build_dir(build_dir, test_dict): @@ -295,8 +309,11 @@ def run_functional_tests( if run_executables: print("Running executables") for _, test in test_dict.items(): - # prepend parameter file (if required) to argument list args = test.other_args + # prepend datm file (if required) to argument list + if test.datm_file: + args.insert(0, test.datm_file) + # prepend parameter file (if required) to argument list if test.use_param_file: args.insert(0, param_file) # run @@ -386,7 +403,11 @@ def run_fortran_exectuables(build_dir, test_dir, test_exe, run_dir, args): run_command.extend(args) os.chdir(run_dir) - out = run_cmd_no_fail(" ".join(run_command), combine_output=True) + cmd = " ".join(run_command) + stat, out, _ = run_cmd(cmd, combine_output=True) + if stat: + print(out) + raise subprocess.CalledProcessError(stat, cmd, out) print(out) @@ -395,13 +416,13 @@ def main(): Reads in command-line arguments and then runs the tests. """ - full_test_dict = config_to_dict(_DEFAULT_CONFIG_FILE) - subclasses = FunctionalTest.__subclasses__() - args = commandline_args() + + full_test_dict = config_to_dict(args.config_file) config_dict = parse_test_list(full_test_dict, args.test_list) test_dict = {} + subclasses = FunctionalTest.__subclasses__() for name in config_dict.keys(): test_class = list(filter(lambda subclass: subclass.name == name, subclasses))[ 0 diff --git a/testing/run_unit_tests.py b/testing/run_unit_tests.py index f9bd344b39..a0f84532d0 100755 --- a/testing/run_unit_tests.py +++ b/testing/run_unit_tests.py @@ -29,8 +29,9 @@ from CIME.utils import run_cmd_no_fail # pylint: disable=wrong-import-position,import-error,wrong-import-order # constants for this script -_CMAKE_BASE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../") -_DEFAULT_CONFIG_FILE = "unit_tests.cfg" +_FILE_DIR = os.path.dirname(os.path.abspath(__file__)) +_CMAKE_BASE_DIR = os.path.join(_FILE_DIR, os.pardir) +_DEFAULT_CONFIG_FILE = os.path.join(_FILE_DIR, "unit_tests.cfg") _TEST_SUB_DIR = "testing" @@ -58,6 +59,13 @@ def commandline_args(): "Will be created if it does not exist.\n", ) + parser.add_argument( + "--config-file", + type=str, + default=_DEFAULT_CONFIG_FILE, + help=f"Configuration file where test list is defined. Default: '{_DEFAULT_CONFIG_FILE}'", + ) + parser.add_argument( "--make-j", type=int, @@ -129,9 +137,8 @@ def main(): Reads in command-line arguments and then runs the tests. """ - full_test_dict = config_to_dict(_DEFAULT_CONFIG_FILE) - args = commandline_args() + full_test_dict = config_to_dict(args.config_file) test_dict = parse_test_list(full_test_dict, args.test_list) run_unit_tests( diff --git a/testing/testing_shr/FatesArgumentUtils.F90 b/testing/testing_shr/FatesArgumentUtils.F90 index ed247fa157..2bdbf825d3 100644 --- a/testing/testing_shr/FatesArgumentUtils.F90 +++ b/testing/testing_shr/FatesArgumentUtils.F90 @@ -24,7 +24,7 @@ function command_line_arg(arg_position) if (n_args < arg_position) then write(*, '(a, i2, a, i2)') "Incorrect number of arguments: ", n_args, ". Should be at least", arg_position, "." - stop + call abort() end if call get_command_argument(arg_position, length=arglen) diff --git a/testing/testing_shr/FatesFactoryMod.F90 b/testing/testing_shr/FatesFactoryMod.F90 index a1d031f717..41a73103d8 100644 --- a/testing/testing_shr/FatesFactoryMod.F90 +++ b/testing/testing_shr/FatesFactoryMod.F90 @@ -520,7 +520,7 @@ subroutine CreateTestPatchList(patch, heights, dbhs) if (present(dbhs)) then if (size(heights) /= size(dbhs)) then write(*, '(a)') "Size of heights array must match size of dbh array." - stop + call abort() end if end if diff --git a/testing/testing_shr/FatesUnitTestIOMod.F90 b/testing/testing_shr/FatesUnitTestIOMod.F90 index 20dd4f198e..51ced8b45a 100644 --- a/testing/testing_shr/FatesUnitTestIOMod.F90 +++ b/testing/testing_shr/FatesUnitTestIOMod.F90 @@ -106,7 +106,7 @@ subroutine Check(status) if (status /= nf90_noerr) then write(*,*) trim(nf90_strerror(status)) - stop + call abort() end if end subroutine Check @@ -134,11 +134,11 @@ subroutine OpenNCFile(nc_file, ncid, fmode) call Check(nf90_create(trim(nc_file), NF90_CLOBBER, ncid)) case DEFAULT write(*,*) 'Need to specify read, write, or readwrite' - stop + call abort() end select else write(*,*) 'Problem reading file' - stop + call abort() end if end subroutine OpenNCFile @@ -480,7 +480,7 @@ subroutine RegisterVar(ncid, var_name, dimID, type, att_names, atts, num_atts, v nc_type = NF90_CHAR else write(*, *) "Must pick correct type" - stop + call abort() end if call Check(nf90_def_var(ncid, var_name, nc_type, dimID, varID)) diff --git a/testing/testing_shr/FatesUnitTestParamReaderMod.F90 b/testing/testing_shr/FatesUnitTestParamReaderMod.F90 index 2a4fb13cd8..d8b655136b 100644 --- a/testing/testing_shr/FatesUnitTestParamReaderMod.F90 +++ b/testing/testing_shr/FatesUnitTestParamReaderMod.F90 @@ -89,7 +89,7 @@ subroutine ReadParameters(this, fates_params) case default write(*, '(a,a)') 'dimension shape:', dimension_shape write(*, '(a)') 'unsupported number of dimensions reading parameters.' - stop + call abort() end select end do diff --git a/testing/testing_shr/SyntheticPatchTypes.F90 b/testing/testing_shr/SyntheticPatchTypes.F90 index 6094f0c6da..64747e5b6d 100644 --- a/testing/testing_shr/SyntheticPatchTypes.F90 +++ b/testing/testing_shr/SyntheticPatchTypes.F90 @@ -163,7 +163,7 @@ integer function PatchDataPosition(this, patch_id, patch_name) ! can't supply both if (present(patch_id) .and. present(patch_name)) then write(*, '(a)') "Can only supply either a patch_id or a patch_name - not both" - stop + call abort() end if do i = 1, this%num_patches @@ -179,11 +179,11 @@ integer function PatchDataPosition(this, patch_id, patch_name) end if else write(*, '(a)') "Must supply either a patch_id or a patch_name." - stop + call abort() end if end do write(*, '(a)') "Cannot find the synthetic patch type supplied" - stop + call abort() end function PatchDataPosition diff --git a/testing/utils.py b/testing/utils.py index 06fd74db0b..57b49d79fe 100644 --- a/testing/utils.py +++ b/testing/utils.py @@ -123,6 +123,27 @@ def get_color_palette(number: int) -> list: return colors[:number] +def get_abspath_from_config_file(relative_path, config_file): + """ + Gets the absolute path of a file relative to the config file where it was defined. + + Args: + relative_path: The path to the target file, relative to the base file. + config_file: The path to the config file. + + Returns: + The absolute path of the target file. + """ + + # Do nothing if it's already a absolute path + if os.path.isabs(relative_path): + return relative_path + + base_dir = os.path.dirname(os.path.abspath(config_file)) + absolute_path = os.path.abspath(os.path.join(base_dir, relative_path)) + return absolute_path + + def config_to_dict(config_file: str) -> dict: """Convert a config file to a python dictionary @@ -132,6 +153,16 @@ def config_to_dict(config_file: str) -> dict: Returns: dictionary: dictionary of config file """ + + # Check that config file exists and is a file + if not os.path.exists(config_file): + raise FileNotFoundError(config_file) + if not os.path.isfile(config_file): + raise RuntimeError(f"config_file is a directory: '{config_file}'") + + # Define list of config file options that we expect to be paths + options_that_are_paths = ["datm_file"] + config = configparser.ConfigParser() config.read(config_file) @@ -139,7 +170,14 @@ def config_to_dict(config_file: str) -> dict: for section in config.sections(): dictionary[section] = {} for option in config.options(section): - dictionary[section][option] = config.get(section, option) + value = config.get(section, option) + + # If the option is one that we expect to be a path, ensure it's an absolute path. + if option in options_that_are_paths: + value = get_abspath_from_config_file(value, config_file) + + # Save value to dictionary + dictionary[section][option] = value return dictionary