Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
6588c25
add staged data directory path for ursa
georgemccabe May 28, 2026
0838daf
initial version of regression test script -- just runs tests for curr…
georgemccabe May 28, 2026
df0569b
fix failures due to incorrect type for maxtries values
georgemccabe May 28, 2026
d77a41c
fix to allow we2e tests to run
georgemccabe May 28, 2026
3ab80ad
create python script to handle running regression test for branch or PR
georgemccabe Jun 1, 2026
91fc2de
handle Path vs string in git commands
georgemccabe Jun 1, 2026
e1f13cf
fix repo location
georgemccabe Jun 1, 2026
600a67d
run script from tests/WE2E dir from branch because run_we2e_tests.py …
georgemccabe Jun 1, 2026
cbf9a14
update version of uwtools
georgemccabe Jun 2, 2026
582894f
updates to fix running tests via subprocess to set up conda env in br…
georgemccabe Jun 2, 2026
4f4bbee
rearrange functions in order of use and remove whitespace
georgemccabe Jun 2, 2026
3c4980d
force install conda env into local conda directory to ensure it is fo…
georgemccabe Jun 3, 2026
daa8991
support creating vx_diff environment if vx_diff argument is provided
georgemccabe Jun 3, 2026
b6d1c3c
added environment file for conda env to run METplus diff_util
georgemccabe Jun 3, 2026
1cc4300
fix setting of env yaml file after dependent env var is set
georgemccabe Jun 4, 2026
9197949
start of script to run METplus diff util on each experiment's output
georgemccabe Jun 4, 2026
1ace70a
Add option to diff all files or only diff dated subdirectories to spe…
georgemccabe Jun 4, 2026
3730b4c
fix handling of success status
georgemccabe Jun 4, 2026
7616bec
print details of test results
georgemccabe Jun 4, 2026
8eb20ea
clean up import
georgemccabe Jun 4, 2026
dfc5cf7
Add staged location data for Ursa
mkavulich May 27, 2026
f079f0f
Fix issue with launching jobs from cron
mkavulich May 27, 2026
894de82
Fix setup_conda for environments where LD_LIBRARY_PATH is unset
mkavulich May 28, 2026
4514341
Fix issues with crontab submission:
mkavulich May 28, 2026
8f6b232
Attempting to get automated pulling of best track data; not quite wor…
mkavulich Jun 16, 2026
76567bc
borrow latest conda setup logic from mpas_plot repository
mkavulich Jun 16, 2026
abae915
Wording fixes, suppress "conda activate" message since setup_conda.sh…
mkavulich Jun 16, 2026
318fc64
Use full ROCOTO_PATH for all rocotorun/rocotostat invocations
mkavulich Jun 16, 2026
ab0fedb
Fix missing rocoto_path in update_expt_status skip list
mkavulich Jun 16, 2026
b90c56b
Fix setup_conda.sh to work when sourced from any directory
mkavulich Jun 16, 2026
09c39de
Use builtin cd in setup_conda.sh to bypass custom cd functions
mkavulich Jun 16, 2026
0e168c8
added --regression_dir arg, rename baseline/metplus args, and update …
georgemccabe Jun 16, 2026
a2d92e4
Added --commit argument to specify a commit to run. Added the --basel…
georgemccabe Jun 16, 2026
dae72c6
added instructions to use the regression test scripts
georgemccabe Jun 16, 2026
f15db16
fix f-string
georgemccabe Jun 16, 2026
a423309
Fetch changes if the repo has already been cloned. Checkout branch or…
georgemccabe Jun 17, 2026
f4aefa4
rename function to be more generic
georgemccabe Jun 17, 2026
6d6ea54
Merge branch 'develop' into feature/regression_tests
georgemccabe Jun 17, 2026
90b91e6
Move baseline branch/commit to its own file so the history of updatin…
georgemccabe Jun 17, 2026
d16a3ee
exit non-zero when differences are flagged
georgemccabe Jun 17, 2026
030d0b6
Pulling of A-DECK files now working!
mkavulich Jun 22, 2026
43e9d90
I'm dumb, we need to retrieve BDECK, not ADECK files. Working now!
mkavulich Jun 22, 2026
b003127
Need to unzip retrieved file
mkavulich Jun 22, 2026
a0dbfaa
Fix some commands and templates, best track pulling now fully working…
mkavulich Jun 22, 2026
e548648
Update PR and Issue templates
mkavulich Jun 22, 2026
1c48fdf
Dont overwrite conda_loc every time setup_conda.sh is invoked
mkavulich Jun 22, 2026
ac52932
Add check in setup.py for old BEST_TRACK variable name
mkavulich Jun 23, 2026
a53fb40
Fix unformatted f-string in get_obs.py
mkavulich Jun 23, 2026
6981d8f
Fix incorrect check for OBS_DIR in pcpcombine.sh for FCST tasks
mkavulich Jun 23, 2026
96241ed
Fix test for get_crontab_contents.py
mkavulich Jun 23, 2026
66f6083
Lint tcpairs.py
mkavulich Jun 23, 2026
a1aba00
Not running test_retrieve_data.py for now; need to adapt to retrieve …
mkavulich Jun 23, 2026
56d3d1c
Fix pylint check
mkavulich Jun 23, 2026
982c943
Restore fix for setup_conda.sh when LD_LIBRARY_PATH is unset
mkavulich Jun 23, 2026
2d6cd5a
ACTUALLY fix pylint check
mkavulich Jun 23, 2026
0666859
Completely disable retrieve_data.py test
mkavulich Jun 23, 2026
09e095b
Merge branch 'develop' into feature/fix_some_issues
mkavulich Jun 24, 2026
6cede53
Merge branch 'develop' into feature/regression_tests
georgemccabe Jun 25, 2026
abd32fc
merged feature/fix_some_issues branch and resolve conflicts with setu…
georgemccabe Jun 25, 2026
6be4b6b
add logging options
georgemccabe Jun 26, 2026
adef994
fix list of all tests (union not intersection)
georgemccabe Jun 26, 2026
9583f1e
print path to log file to terminal
georgemccabe Jun 30, 2026
1f85c91
add --debug command line argument to produce more log information abo…
georgemccabe Jun 30, 2026
5f85da0
log paths that are compared by diff script
georgemccabe Jun 30, 2026
f96adec
update the info about how to update the baseline used for the diff
georgemccabe Jun 30, 2026
7c32a83
remove module load of rocoto since the changes in PR #25 handle findi…
georgemccabe Jun 30, 2026
68ca84e
add instruction to ensure shared regression test directory has group …
georgemccabe Jul 1, 2026
612f3ac
Only check for OBS_DIR if obs are needed
mkavulich Jul 1, 2026
de8cc10
Enable script to be called from any directory, not just tests/WE2E. R…
georgemccabe Jul 1, 2026
87db0b4
Better check: dont check for OBS_DIR for GenEnsProd task
mkavulich Jul 1, 2026
827a3e8
Fix error submitting TCPAIRS job on Hera: we need to specify a differ…
mkavulich Jul 2, 2026
5f021c6
Merge remote-tracking branch 'origin/develop' into feature/regression…
mkavulich Jul 9, 2026
23c38f0
Add stage directory to diff ignore list
georgemccabe Jul 15, 2026
1917126
per PR feedback from @mkavulich, rename command line argument to diff…
georgemccabe Jul 16, 2026
606a882
per PR feedback from @mkavulich, add argument to test script to run a…
georgemccabe Jul 16, 2026
35e8910
Merge branch 'feature/regression_tests' into feature/we2e_test_from_a…
georgemccabe Jul 16, 2026
91cea5c
move imports to functions that use them so the python_utils module ca…
georgemccabe Jul 17, 2026
bcb922b
move logic to parse and check --tests argument input into python_util…
georgemccabe Jul 17, 2026
911882d
bugfix: all multiple arguments to be supplied to the --tests argument…
georgemccabe Jul 17, 2026
cfc2f44
Enhance diff script to take --tests argument to subset tests to proce…
georgemccabe Jul 17, 2026
c248cfc
added unit tests to test various formats of --tests argument
georgemccabe Jul 17, 2026
24591f7
call run_we2e_tests.py script from output directory so log files are …
georgemccabe Jul 20, 2026
bab4bbb
fix bug where path to test_configs was not correct -- updated tests t…
georgemccabe Jul 20, 2026
7212699
use pathlib Path instead of os.path to get ushdir to avoid having to …
georgemccabe Jul 20, 2026
9dcd861
Merge branch 'develop' into feature/we2e_test_from_any_dir
georgemccabe Jul 20, 2026
9e6aac1
fixed missing close of comment so documentation header is displayed
georgemccabe Jul 20, 2026
587a009
add info about running a subset of diff tests
georgemccabe Jul 20, 2026
e3955c7
fix pylint complaints about long lines
georgemccabe Jul 20, 2026
0c484f6
fix broken logic to switch to the merge commit of an open PR
georgemccabe Jul 20, 2026
1d36c46
add ush to the python path in the pylint config file so PYTHONPATH do…
georgemccabe Jul 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/PULL_REQUEST_TEMPLATE
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<!-- Add any links to PRs (internal or external) that must be merged before this one. For example:
- #23
- https://github.com/dtcenter/METplus/pull/3301
-->

## DOCUMENTATION:
<!-- If this PR is contributing new capabilities that need to be documented, please also include updates to the RST files (docs/UsersGuide/source) as supporting material. -->
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/python_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ jobs:
- name: Lint the python code
run: |
micromamba activate vx_workflow
export PYTHONPATH=$(pwd)/ush
pylint --version
pylint --ignore-imports=yes tests/test_python/test_*.py
pylint ush/generate_wflow.py
Expand Down
5 changes: 3 additions & 2 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ ignored-modules=

# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
init-hook="import sys; sys.path.append('./ush')"

# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use, and will cap the count on Windows to
Expand Down Expand Up @@ -425,7 +425,8 @@ disable=raw-checker-failed,
use-symbolic-message-instead,
logging-fstring-interpolation,
too-many-locals,
similarities
similarities,
import-outside-toplevel

# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
Expand Down
271 changes: 49 additions & 222 deletions tests/WE2E/run_we2e_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,23 @@
from pathlib import Path
from textwrap import dedent

# add tests/WE2E and ush directories to sys path to find other python files
USH_DIR = Path(__file__).absolute().parents[2] / "ush"
sys.path.insert(0, str(USH_DIR))

from monitor_jobs import monitor_jobs, write_monitor_file

from uwtools.api.config import get_yaml_config

sys.path.append("../../ush")
# pylint: disable=wrong-import-order, wrong-import-position
from generate_wflow import generate_wflow
from python_utils import check_python_version

from python_utils import check_python_version, get_tests_to_run, get_pretty_list


def run_we2e_tests(homedir, args) -> None:
def run_we2e_tests(args) -> None:
"""Runs the Workflow End-to-End (WE2E) tests selected by the user

Args:
homedir (str): The full path to the top-level application directory
args (argparse.Namespace): Command-line arguments

Returns:
Expand All @@ -40,116 +41,31 @@ def run_we2e_tests(homedir, args) -> None:
# Set up logging to write to screen and logfile
setup_logging(debug=args.debug)
logging.debug(f"Arguments to run_we2e_tests():\n{args}")
# Set some important directories
ushdir = Path(homedir, "ush")

# Set some variables based on input arguments
machine = args.machine.lower()

# Derecho requires long delay between calls to rocotorun due to system-level cacheing of
# job statuses
if machine=="derecho":
if args.delay < 60:
logging.info("Derecho requires 60 second delay between calls to rocotorun")
args.delay=60

alltests = glob.glob("test_configs/**/config*.yaml", recursive=True)
testdirs = next(os.walk("test_configs"))[1]
# If args.tests is a list of length more than one, we assume it is a list of test names
if len(args.tests) > 1:
tests_to_check = args.tests
logging.debug(f"User specified a list of tests:\n{tests_to_check}")
else:
# First see if args.tests is a valid test name
user_spec_tests = args.tests
logging.debug(f"Checking if {user_spec_tests} is a valid test name")
match = check_test(user_spec_tests[0])
if match:
tests_to_check = user_spec_tests
else:
# If not a valid test name, check if it is a test suite
logging.debug(f"Checking if {user_spec_tests} is a valid test suite")
if user_spec_tests[0] == "all":
tests_to_check = []
for f in alltests:
filename = Path(f).name
# We just want the test name in this list, so cut out the
# "config." prefix and ".yaml" extension
if len(filename) > 12:
if filename[:7] == "config." and filename[-5:] == ".yaml":
tests_to_check.append(filename[7:-5])
else:
logging.debug(f"Skipping non-test file {filename}")
else:
logging.debug(f"Skipping non-test file {filename}")
logging.debug(f"Will check all tests:\n{tests_to_check}")
elif user_spec_tests[0] in testdirs:
# If a subdirectory under test_configs/ is specified, run all
# tests in that directory
logging.debug(
f"{user_spec_tests[0]} is one of the testing directories:\n{testdirs}"
)
logging.debug(
f"Will run all tests in test_configs/{user_spec_tests[0]}"
)
tests_in_dir = glob.glob(
f"test_configs/{user_spec_tests[0]}/config*.yaml", recursive=True
)
tests_to_check = []
for f in tests_in_dir:
filename = Path(f).name
# We just want the test name in this list, so cut out the
# "config." prefix and ".yaml" extension
if len(filename) > 12:
if filename[:7] == "config." and filename[-5:] == ".yaml":
tests_to_check.append(filename[7:-5])
else:
logging.debug(f"Skipping non-test file {filename}")
else:
logging.debug(f"Skipping non-test file {filename}")
else:
# If we have gotten this far then the only option left for user_spec_tests is a
# file containing test names
logging.debug(
f"Checking if {user_spec_tests} is a file containing test names"
)
if Path(user_spec_tests[0]).is_file():
with open(user_spec_tests[0], encoding="utf-8") as f:
tests_to_check = [x.rstrip() for x in f]
else:
raise FileNotFoundError(
dedent(
f"""
The specified 'tests' argument '{user_spec_tests}'
does not appear to be a valid test name, a valid test suite, a subdirectory
under test_configs/, or a file containing valid test names.

Check your inputs and try again.
"""
)
)

logging.info("Checking that all tests are valid")

tests_to_run = check_tests(tests_to_check)

pretty_list = "\n".join(str(x) for x in tests_to_run)
logging.info(f"Will run {len(tests_to_run)} tests:\n{pretty_list}")
if machine=="derecho" and args.delay < 60:
logging.info("Derecho requires 60 second delay between calls to rocotorun")
args.delay=60

tests_to_run = get_tests_to_run(args.tests)

config_default_file = Path(ushdir, "config_defaults.yaml")
logging.debug(f"Loading config defaults file {config_default_file}")
config_defaults = get_yaml_config(config_default_file)
pretty_list = get_pretty_list(tests_to_run)
logging.info(f"Will run {len(tests_to_run)} tests:\n{pretty_list}")

machine_file = Path(ushdir, "machine", f"{machine}.yaml")
machine_file = Path(USH_DIR, "machine", f"{machine}.yaml")
logging.debug(f"Loading machine defaults file {machine_file}")
machine_defaults = get_yaml_config(machine_file)

starttime_string = datetime.now().strftime("%Y%m%d%H%M%S")

monitor_yaml = {}
for test in tests_to_run:
# Starting with test yaml template, fill in user-specified and machine- and
# test-specific options, then write resulting complete config.yaml
starttime = datetime.now()
starttime_string = starttime.strftime("%Y%m%d%H%M%S")
test_name = Path(test).name.split(".")[1]
logging.debug(f"For test {test_name}, constructing config.yaml")
test_cfg = get_yaml_config(test)
Expand Down Expand Up @@ -201,16 +117,16 @@ def run_we2e_tests(homedir, args) -> None:
"based on specified command-line arguments:\n"
)
logging.debug(str(test_cfg))
test_cfg.dump(Path(ushdir, "config.yaml"))
test_cfg.dump(Path(USH_DIR, "config.yaml"))

logging.info(f"Calling workflow generation function for test {test_name}\n")
if args.quiet:
console_handler = logging.getLogger().handlers[1]
console_handler.setLevel(logging.WARNING)
expt_dir = generate_wflow(
ushdir=str(ushdir),
ushdir=str(USH_DIR),
config="config.yaml",
logfile=f"{str(ushdir)}/log.generate_wflow",
logfile=f"{str(USH_DIR)}/log.generate_wflow",
debug=args.debug,
)
if args.quiet:
Expand Down Expand Up @@ -238,131 +154,44 @@ def run_we2e_tests(homedir, args) -> None:
# Make WORKFLOW_ID actually mean something
test_cfg["workflow"].update({"WORKFLOW_ID": workflow_id})

if args.launch != "cron":
monitor_file = f"WE2E_tests_{starttime_string}.yaml"
write_monitor_file(monitor_file, monitor_yaml)
logging.info("All experiments have been generated;")
logging.info(f"Experiment file {monitor_file} created")
if args.launch == "python":
write_monitor_file(monitor_file, monitor_yaml)
logging.debug("calling function that monitors jobs, prints summary")
try:
monitor_file = monitor_jobs(
monitor_yaml,
monitor_file=monitor_file,
procs=args.procs,
debug=args.debug,
delay=args.delay,
)
except KeyboardInterrupt:
logging.info(
"\n\nUser interrupted monitor script; to resume monitoring jobs run:\n"
)
rerun_string=f"./monitor_jobs.py -y={monitor_file}"
if args.procs>1:
rerun_string+=f" -p={args.procs}"
if args.delay!=5:
rerun_string+=f" --delay={args.delay}"

logging.info(f"{rerun_string}\n")
else:
logging.info("To automatically run and monitor experiments, use:\n")
logging.info(f"./monitor_jobs.py -y={monitor_file}\n")
else:
if args.launch == "cron":
logging.info(
"All experiments have been generated; using cron to submit workflows"
)
logging.info("To view running experiments in cron try `crontab -l`")
return

monitor_file = f"WE2E_tests_{starttime_string}.yaml"
write_monitor_file(monitor_file, monitor_yaml)
logging.info("All experiments have been generated;")
logging.info(f"Experiment file {monitor_file} created")

def check_tests(tests: list) -> list:
"""
Checks that all tests in a provided list of tests are valid

Args:
tests (list): List of potentially valid test names

Returns:
tests_to_run: List of configuration files corresponding to test names
"""
if args.launch != "python":
logging.info("To automatically run and monitor experiments, use:\n")
logging.info(f"./monitor_jobs.py -y={monitor_file}\n")
return

testfiles = glob.glob("test_configs/**/config*.yaml", recursive=True)
# Check that there are no duplicate test filenames
testfilenames = []
for testfile in testfiles:
testfile = Path(testfile)
if testfile.name in testfilenames:
duplicates = glob.glob(f"test_configs/**/{testfile.name}", recursive=True)
raise ValueError(
dedent(
f"""
Found duplicate test file names:
{duplicates}
Ensure that each test file name under the test_configs/ directory
is unique.
"""
)
)
testfilenames.append(testfile.name)
tests_to_run = []
for test in tests:
# Skip blank/empty testnames; this avoids failure if newlines or spaces are included
if not test or test.isspace():
continue
# Skip if string has an octothorpe
if "#" in test:
logging.debug(
f"Assuming line is a comment due to presence of '#' character:\n{test}"
)
continue
match = check_test(test)
if not match:
raise FileNotFoundError(f"Could not find test {test}")
tests_to_run.append(match)
# Because some test files are symlinked to other tests, check that we don't
# include the same test twice
for testfile in tests_to_run.copy():
testfile = Path(testfile)
if testfile.is_symlink():
if testfile.resolve() in tests_to_run:
logging.warning(
dedent(
f"""WARNING: test file {testfile} is a symbolic link to a
test file ({testfile.resolve()}) that is also included in
the test list. Only the latter test will be run."""
)
)
tests_to_run.remove(str(testfile))
if len(tests_to_run) != len(set(tests_to_run)):
logging.warning(
"\nWARNING: Duplicate test names were found in list. "
"Removing duplicates and continuing.\n"
write_monitor_file(monitor_file, monitor_yaml)
logging.debug("calling function that monitors jobs, prints summary")
try:
monitor_file = monitor_jobs(
monitor_yaml,
monitor_file=monitor_file,
procs=args.procs,
debug=args.debug,
delay=args.delay,
)
tests_to_run = list(set(tests_to_run))
return tests_to_run


def check_test(test: str) -> str:
"""
Checks that a string corresponds to a valid test name

Args:
test (str): Potential test name

Returns:
config: Name of the test configuration file (empty string if no test file is found)
"""
# potential test files
testfiles = glob.glob("test_configs/**/config*.yaml", recursive=True)
# potential test file for input test name
test_config = f"config.{test.strip()}.yaml"
config = ""
for testfile in testfiles:
if test_config in testfile:
logging.debug(f"found test {test}, testfile {testfile}")
config = Path(testfile).absolute()
return config
except KeyboardInterrupt:
logging.info(
"\n\nUser interrupted monitor script; to resume monitoring jobs run:\n"
)
rerun_string=f"./monitor_jobs.py -y={monitor_file}"
if args.procs>1:
rerun_string+=f" -p={args.procs}"
if args.delay!=5:
rerun_string+=f" --delay={args.delay}"

logging.info(f"{rerun_string}\n")

def setup_logging(logfile: str = "log.run_WE2E_tests", debug: bool = False) -> None:
"""
Expand Down Expand Up @@ -399,8 +228,6 @@ def setup_logging(logfile: str = "log.run_WE2E_tests", debug: bool = False) -> N
# Check python version and presence of some non-standard packages
check_python_version()

# Get the "Home" directory, two levels above this one
top_dir = Path(__file__).absolute().parent.parent.parent
LOGFILE = "log.run_WE2E_tests"

# Parse arguments
Expand Down Expand Up @@ -533,7 +360,7 @@ def setup_logging(logfile: str = "log.run_WE2E_tests", debug: bool = False) -> N
# Call main function

try:
run_we2e_tests(top_dir, user_args)
run_we2e_tests(user_args)
except: #pylint: disable=bare-except
logging.exception(
dedent(
Expand Down
Loading
Loading