1515from pathlib import Path
1616from 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+
1822from monitor_jobs import monitor_jobs , write_monitor_file
1923
2024from uwtools .api .config import get_yaml_config
2125
22- sys .path .append ("../../ush" )
2326# pylint: disable=wrong-import-order, wrong-import-position
2427from 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 \n User 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- "\n WARNING: 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 \n User 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
367196def 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