Skip to content

Commit f32f0e1

Browse files
authored
[develop]: Regression test improvements (#39)
## DESCRIPTION OF CHANGES: * Moved logic to parse `--tests` argument of `run_we2e_tests.py` into python_utils so that it can be used by the `run_diff.py` regression test script to determine which subdirectories to diff * Added unit tests for `--tests` argument parsing (identified unsupported inputs that could be added later) * Updated regression test scripts to support `--tests` argument to run a subset of tests and diff a subset of tests (reports failure if requested test was not generated by either run) * Updated `run_we2e_tests.py` script so that it can be run from any directory instead of just the tests/WE2E directory (allows log outputs to be written to more appropriate location when running via regression test scripts * Moved jinja2 imports in python_utils (metplus_conf_utils.py and config_parser.py) into functions that use them so that the entire python_utils module can be imported and functions that don't require jinja2 can be called without the dependency * Renamed `run_diff.py` argument `--baseline` to `--baseline_dir` to differentiate from the `run_regression.py argument (one says to run tests on the baseline commit while the other overrides the location of the baseline data) * Fixed bug in `run_regression.py` script where `--tests` argument cannot take more than 1 input value (list of tests) * Fixed this PR template that hides a header * Fixed bug in logic to get merge commit of PR to run regression tests * add ush to the python path in the pylint config file so PYTHONPATH doesn't need to be set when calling pylint
1 parent da686ac commit f32f0e1

13 files changed

Lines changed: 494 additions & 298 deletions

File tree

.github/PULL_REQUEST_TEMPLATE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
<!-- Add any links to PRs (internal or external) that must be merged before this one. For example:
3737
- #23
3838
- https://github.com/dtcenter/METplus/pull/3301
39+
-->
3940

4041
## DOCUMENTATION:
4142
<!-- 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. -->

.github/workflows/python_tests.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ jobs:
3333
- name: Lint the python code
3434
run: |
3535
micromamba activate vx_workflow
36-
export PYTHONPATH=$(pwd)/ush
3736
pylint --version
3837
pylint --ignore-imports=yes tests/test_python/test_*.py
3938
pylint ush/generate_wflow.py

.pylintrc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ ignored-modules=
6767

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

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

430431
# Enable the message, report, category or checker with the given id(s). You can
431432
# either give multiple identifier separated by comma (,) or put this option

tests/WE2E/run_we2e_tests.py

Lines changed: 49 additions & 222 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,23 @@
1515
from pathlib import Path
1616
from textwrap import dedent
1717

18+
# add tests/WE2E and ush directories to sys path to find other python files
19+
USH_DIR = Path(__file__).absolute().parents[2] / "ush"
20+
sys.path.insert(0, str(USH_DIR))
21+
1822
from monitor_jobs import monitor_jobs, write_monitor_file
1923

2024
from uwtools.api.config import get_yaml_config
2125

22-
sys.path.append("../../ush")
2326
# pylint: disable=wrong-import-order, wrong-import-position
2427
from generate_wflow import generate_wflow
25-
from python_utils import check_python_version
26-
28+
from python_utils import check_python_version, get_tests_to_run, get_pretty_list
2729

2830

29-
def run_we2e_tests(homedir, args) -> None:
31+
def run_we2e_tests(args) -> None:
3032
"""Runs the Workflow End-to-End (WE2E) tests selected by the user
3133
3234
Args:
33-
homedir (str): The full path to the top-level application directory
3435
args (argparse.Namespace): Command-line arguments
3536
3637
Returns:
@@ -40,116 +41,31 @@ def run_we2e_tests(homedir, args) -> None:
4041
# Set up logging to write to screen and logfile
4142
setup_logging(debug=args.debug)
4243
logging.debug(f"Arguments to run_we2e_tests():\n{args}")
43-
# Set some important directories
44-
ushdir = Path(homedir, "ush")
4544

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

4948
# Derecho requires long delay between calls to rocotorun due to system-level cacheing of
5049
# job statuses
51-
if machine=="derecho":
52-
if args.delay < 60:
53-
logging.info("Derecho requires 60 second delay between calls to rocotorun")
54-
args.delay=60
55-
56-
alltests = glob.glob("test_configs/**/config*.yaml", recursive=True)
57-
testdirs = next(os.walk("test_configs"))[1]
58-
# If args.tests is a list of length more than one, we assume it is a list of test names
59-
if len(args.tests) > 1:
60-
tests_to_check = args.tests
61-
logging.debug(f"User specified a list of tests:\n{tests_to_check}")
62-
else:
63-
# First see if args.tests is a valid test name
64-
user_spec_tests = args.tests
65-
logging.debug(f"Checking if {user_spec_tests} is a valid test name")
66-
match = check_test(user_spec_tests[0])
67-
if match:
68-
tests_to_check = user_spec_tests
69-
else:
70-
# If not a valid test name, check if it is a test suite
71-
logging.debug(f"Checking if {user_spec_tests} is a valid test suite")
72-
if user_spec_tests[0] == "all":
73-
tests_to_check = []
74-
for f in alltests:
75-
filename = Path(f).name
76-
# We just want the test name in this list, so cut out the
77-
# "config." prefix and ".yaml" extension
78-
if len(filename) > 12:
79-
if filename[:7] == "config." and filename[-5:] == ".yaml":
80-
tests_to_check.append(filename[7:-5])
81-
else:
82-
logging.debug(f"Skipping non-test file {filename}")
83-
else:
84-
logging.debug(f"Skipping non-test file {filename}")
85-
logging.debug(f"Will check all tests:\n{tests_to_check}")
86-
elif user_spec_tests[0] in testdirs:
87-
# If a subdirectory under test_configs/ is specified, run all
88-
# tests in that directory
89-
logging.debug(
90-
f"{user_spec_tests[0]} is one of the testing directories:\n{testdirs}"
91-
)
92-
logging.debug(
93-
f"Will run all tests in test_configs/{user_spec_tests[0]}"
94-
)
95-
tests_in_dir = glob.glob(
96-
f"test_configs/{user_spec_tests[0]}/config*.yaml", recursive=True
97-
)
98-
tests_to_check = []
99-
for f in tests_in_dir:
100-
filename = Path(f).name
101-
# We just want the test name in this list, so cut out the
102-
# "config." prefix and ".yaml" extension
103-
if len(filename) > 12:
104-
if filename[:7] == "config." and filename[-5:] == ".yaml":
105-
tests_to_check.append(filename[7:-5])
106-
else:
107-
logging.debug(f"Skipping non-test file {filename}")
108-
else:
109-
logging.debug(f"Skipping non-test file {filename}")
110-
else:
111-
# If we have gotten this far then the only option left for user_spec_tests is a
112-
# file containing test names
113-
logging.debug(
114-
f"Checking if {user_spec_tests} is a file containing test names"
115-
)
116-
if Path(user_spec_tests[0]).is_file():
117-
with open(user_spec_tests[0], encoding="utf-8") as f:
118-
tests_to_check = [x.rstrip() for x in f]
119-
else:
120-
raise FileNotFoundError(
121-
dedent(
122-
f"""
123-
The specified 'tests' argument '{user_spec_tests}'
124-
does not appear to be a valid test name, a valid test suite, a subdirectory
125-
under test_configs/, or a file containing valid test names.
126-
127-
Check your inputs and try again.
128-
"""
129-
)
130-
)
131-
132-
logging.info("Checking that all tests are valid")
133-
134-
tests_to_run = check_tests(tests_to_check)
135-
136-
pretty_list = "\n".join(str(x) for x in tests_to_run)
137-
logging.info(f"Will run {len(tests_to_run)} tests:\n{pretty_list}")
50+
if machine=="derecho" and args.delay < 60:
51+
logging.info("Derecho requires 60 second delay between calls to rocotorun")
52+
args.delay=60
53+
54+
tests_to_run = get_tests_to_run(args.tests)
13855

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

143-
machine_file = Path(ushdir, "machine", f"{machine}.yaml")
59+
machine_file = Path(USH_DIR, "machine", f"{machine}.yaml")
14460
logging.debug(f"Loading machine defaults file {machine_file}")
14561
machine_defaults = get_yaml_config(machine_file)
14662

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

206122
logging.info(f"Calling workflow generation function for test {test_name}\n")
207123
if args.quiet:
208124
console_handler = logging.getLogger().handlers[1]
209125
console_handler.setLevel(logging.WARNING)
210126
expt_dir = generate_wflow(
211-
ushdir=str(ushdir),
127+
ushdir=str(USH_DIR),
212128
config="config.yaml",
213-
logfile=f"{str(ushdir)}/log.generate_wflow",
129+
logfile=f"{str(USH_DIR)}/log.generate_wflow",
214130
debug=args.debug,
215131
)
216132
if args.quiet:
@@ -238,131 +154,44 @@ def run_we2e_tests(homedir, args) -> None:
238154
# Make WORKFLOW_ID actually mean something
239155
test_cfg["workflow"].update({"WORKFLOW_ID": workflow_id})
240156

241-
if args.launch != "cron":
242-
monitor_file = f"WE2E_tests_{starttime_string}.yaml"
243-
write_monitor_file(monitor_file, monitor_yaml)
244-
logging.info("All experiments have been generated;")
245-
logging.info(f"Experiment file {monitor_file} created")
246-
if args.launch == "python":
247-
write_monitor_file(monitor_file, monitor_yaml)
248-
logging.debug("calling function that monitors jobs, prints summary")
249-
try:
250-
monitor_file = monitor_jobs(
251-
monitor_yaml,
252-
monitor_file=monitor_file,
253-
procs=args.procs,
254-
debug=args.debug,
255-
delay=args.delay,
256-
)
257-
except KeyboardInterrupt:
258-
logging.info(
259-
"\n\nUser interrupted monitor script; to resume monitoring jobs run:\n"
260-
)
261-
rerun_string=f"./monitor_jobs.py -y={monitor_file}"
262-
if args.procs>1:
263-
rerun_string+=f" -p={args.procs}"
264-
if args.delay!=5:
265-
rerun_string+=f" --delay={args.delay}"
266-
267-
logging.info(f"{rerun_string}\n")
268-
else:
269-
logging.info("To automatically run and monitor experiments, use:\n")
270-
logging.info(f"./monitor_jobs.py -y={monitor_file}\n")
271-
else:
157+
if args.launch == "cron":
272158
logging.info(
273159
"All experiments have been generated; using cron to submit workflows"
274160
)
275161
logging.info("To view running experiments in cron try `crontab -l`")
162+
return
276163

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

278-
def check_tests(tests: list) -> list:
279-
"""
280-
Checks that all tests in a provided list of tests are valid
281-
282-
Args:
283-
tests (list): List of potentially valid test names
284-
285-
Returns:
286-
tests_to_run: List of configuration files corresponding to test names
287-
"""
169+
if args.launch == "none":
170+
logging.info("To automatically run and monitor experiments, use:\n")
171+
logging.info(f"./monitor_jobs.py -y={monitor_file}\n")
172+
return
288173

289-
testfiles = glob.glob("test_configs/**/config*.yaml", recursive=True)
290-
# Check that there are no duplicate test filenames
291-
testfilenames = []
292-
for testfile in testfiles:
293-
testfile = Path(testfile)
294-
if testfile.name in testfilenames:
295-
duplicates = glob.glob(f"test_configs/**/{testfile.name}", recursive=True)
296-
raise ValueError(
297-
dedent(
298-
f"""
299-
Found duplicate test file names:
300-
{duplicates}
301-
Ensure that each test file name under the test_configs/ directory
302-
is unique.
303-
"""
304-
)
305-
)
306-
testfilenames.append(testfile.name)
307-
tests_to_run = []
308-
for test in tests:
309-
# Skip blank/empty testnames; this avoids failure if newlines or spaces are included
310-
if not test or test.isspace():
311-
continue
312-
# Skip if string has an octothorpe
313-
if "#" in test:
314-
logging.debug(
315-
f"Assuming line is a comment due to presence of '#' character:\n{test}"
316-
)
317-
continue
318-
match = check_test(test)
319-
if not match:
320-
raise FileNotFoundError(f"Could not find test {test}")
321-
tests_to_run.append(match)
322-
# Because some test files are symlinked to other tests, check that we don't
323-
# include the same test twice
324-
for testfile in tests_to_run.copy():
325-
testfile = Path(testfile)
326-
if testfile.is_symlink():
327-
if testfile.resolve() in tests_to_run:
328-
logging.warning(
329-
dedent(
330-
f"""WARNING: test file {testfile} is a symbolic link to a
331-
test file ({testfile.resolve()}) that is also included in
332-
the test list. Only the latter test will be run."""
333-
)
334-
)
335-
tests_to_run.remove(str(testfile))
336-
if len(tests_to_run) != len(set(tests_to_run)):
337-
logging.warning(
338-
"\nWARNING: Duplicate test names were found in list. "
339-
"Removing duplicates and continuing.\n"
174+
write_monitor_file(monitor_file, monitor_yaml)
175+
logging.debug("calling function that monitors jobs, prints summary")
176+
try:
177+
monitor_file = monitor_jobs(
178+
monitor_yaml,
179+
monitor_file=monitor_file,
180+
procs=args.procs,
181+
debug=args.debug,
182+
delay=args.delay,
340183
)
341-
tests_to_run = list(set(tests_to_run))
342-
return tests_to_run
343-
344-
345-
def check_test(test: str) -> str:
346-
"""
347-
Checks that a string corresponds to a valid test name
348-
349-
Args:
350-
test (str): Potential test name
351-
352-
Returns:
353-
config: Name of the test configuration file (empty string if no test file is found)
354-
"""
355-
# potential test files
356-
testfiles = glob.glob("test_configs/**/config*.yaml", recursive=True)
357-
# potential test file for input test name
358-
test_config = f"config.{test.strip()}.yaml"
359-
config = ""
360-
for testfile in testfiles:
361-
if test_config in testfile:
362-
logging.debug(f"found test {test}, testfile {testfile}")
363-
config = Path(testfile).absolute()
364-
return config
184+
except KeyboardInterrupt:
185+
logging.info(
186+
"\n\nUser interrupted monitor script; to resume monitoring jobs run:\n"
187+
)
188+
rerun_string=f"./monitor_jobs.py -y={monitor_file}"
189+
if args.procs>1:
190+
rerun_string+=f" -p={args.procs}"
191+
if args.delay!=5:
192+
rerun_string+=f" --delay={args.delay}"
365193

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

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

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

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

535362
try:
536-
run_we2e_tests(top_dir, user_args)
363+
run_we2e_tests(user_args)
537364
except: #pylint: disable=bare-except
538365
logging.exception(
539366
dedent(

0 commit comments

Comments
 (0)