|  | #!/usr/bin/env python3 | 
|  | ############################################################################ | 
|  | # Copyright (C) 2006 The Regents of the University of California. | 
|  | # Produced at Lawrence Livermore National Laboratory (cf, DISCLAIMER). | 
|  | # Written by Christopher J. Morrone <morrone2@llnl.gov> | 
|  | # CODE-OCEC-09-009. All rights reserved. | 
|  | # | 
|  | # This file is part of Slurm, a resource management program. | 
|  | # For details, see <https://slurm.schedmd.com/>. | 
|  | # Please also read the supplied file: DISCLAIMER. | 
|  | # | 
|  | # Slurm is free software; you can redistribute it and/or modify it under | 
|  | # the terms of the GNU General Public License as published by the Free | 
|  | # Software Foundation; either version 2 of the License, or (at your option) | 
|  | # any later version. | 
|  | # | 
|  | # Slurm is distributed in the hope that it will be useful, but WITHOUT ANY | 
|  | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | 
|  | # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more | 
|  | # details. | 
|  | # | 
|  | # You should have received a copy of the GNU General Public License along | 
|  | # with Slurm; if not, write to the Free Software Foundation, Inc., | 
|  | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA. | 
|  | ############################################################################ | 
|  |  | 
|  | """This script makes it easier to run the Slurm expect test scripts.""" | 
|  |  | 
|  | from __future__ import print_function | 
|  | import json | 
|  | import os | 
|  | import re | 
|  | import sys | 
|  | import time | 
|  | import signal | 
|  | from optparse import OptionParser | 
|  | from optparse import OptionValueError | 
|  | from subprocess import Popen | 
|  |  | 
|  |  | 
|  | def main(argv=None): | 
|  | # "tests" is a list containing tuples of length 3 of the form | 
|  | # (test major number, test minor number, test filename) | 
|  | tests = [] | 
|  | failed_tests = [] | 
|  | passed_tests = [] | 
|  | skipped_tests = [] | 
|  | begin = (1, 1) | 
|  | abort = False | 
|  |  | 
|  | # Handle command line parameters | 
|  | if argv is None: | 
|  | argv = sys.argv | 
|  |  | 
|  | parser = OptionParser() | 
|  | parser.add_option( | 
|  | "-t", | 
|  | "--time-individual", | 
|  | action="store_true", | 
|  | dest="time_individual", | 
|  | default=False, | 
|  | ) | 
|  | parser.add_option( | 
|  | "-e", | 
|  | "--exclude", | 
|  | type="string", | 
|  | dest="exclude_tests", | 
|  | action="callback", | 
|  | callback=test_parser, | 
|  | help="comma or space separated string of tests to skip", | 
|  | ) | 
|  | parser.add_option( | 
|  | "-i", | 
|  | "--include", | 
|  | type="string", | 
|  | dest="include_tests", | 
|  | action="callback", | 
|  | callback=test_parser, | 
|  | help="comma or space separated string of tests to include", | 
|  | ) | 
|  | parser.add_option("-k", "--keep-logs", action="store_true", default=False) | 
|  | parser.add_option("-s", "--stop-on-first-fail", action="store_true", default=False) | 
|  | parser.add_option( | 
|  | "-b", | 
|  | "--begin-from-test", | 
|  | type="string", | 
|  | dest="begin_from_test", | 
|  | action="callback", | 
|  | callback=test_parser, | 
|  | ) | 
|  | parser.add_option( | 
|  | "-f", | 
|  | "--results-file", | 
|  | type="string", | 
|  | help="write json result to specified file name", | 
|  | ) | 
|  |  | 
|  | (options, args) = parser.parse_args(args=argv) | 
|  |  | 
|  | # Sanity check | 
|  | if not os.path.isfile("globals"): | 
|  | print('ERROR: "globals" not here as needed', file=sys.stderr) | 
|  | return -1 | 
|  |  | 
|  | # Clear any environment variables that could break the tests. | 
|  | # Cray sets some squeue format options that break tests | 
|  | del os.environ["SQUEUE_ALL"] | 
|  | del os.environ["SQUEUE_SORT"] | 
|  | del os.environ["SQUEUE_FORMAT"] | 
|  | del os.environ["SQUEUE_FORMAT2"] | 
|  |  | 
|  | # Read the current working directory and build a sorted list | 
|  | # of the available tests. | 
|  | test_re = re.compile(r"test(\d+)\.(\d+)$") | 
|  | for filename in os.listdir("."): | 
|  | match = test_re.match(filename) | 
|  | if match: | 
|  | major = int(match.group(1)) | 
|  | minor = int(match.group(2)) | 
|  | if not test_in_list(major, minor, options.exclude_tests) and ( | 
|  | not options.include_tests | 
|  | or test_in_list(major, minor, options.include_tests) | 
|  | ): | 
|  | tests.append((major, minor, filename)) | 
|  | if not tests: | 
|  | print( | 
|  | "ERROR: no test files found in current working directory", file=sys.stderr | 
|  | ) | 
|  | return -1 | 
|  | # sort by major, minor | 
|  | tests.sort(key=lambda t: (t[0], t[1])) | 
|  |  | 
|  | # Set begin value | 
|  | if options.begin_from_test is not None: | 
|  | begin = options.begin_from_test[0] | 
|  |  | 
|  | # Now run the tests | 
|  | start_time = time.time() | 
|  | test_env = os.environ.copy() | 
|  | if options.stop_on_first_fail: | 
|  | test_env["SLURM_TESTSUITE_CLEANUP_ON_FAILURE"] = "false" | 
|  | else: | 
|  | test_env["SLURM_TESTSUITE_CLEANUP_ON_FAILURE"] = "true" | 
|  | print("Started:", time.asctime(time.localtime(start_time)), file=sys.stdout) | 
|  | sys.stdout.flush() | 
|  | results_list = [] | 
|  | for test in tests: | 
|  | if begin[0] > test[0] or (begin[0] == test[0] and begin[1] > test[1]): | 
|  | continue | 
|  | test_id = f"{test[0]}.{test[1]}" | 
|  | sys.stdout.write(f"Running test {test_id} ") | 
|  | sys.stdout.flush() | 
|  | test_dict = {} | 
|  | test_dict["id"] = test_id | 
|  | testlog_name = f"test{test_id}.log" | 
|  | try: | 
|  | os.remove(testlog_name + ".failed") | 
|  | except Exception: | 
|  | pass | 
|  |  | 
|  | if os.path.exists(testlog_name): | 
|  | os.remove(testlog_name) | 
|  | testlog = open(testlog_name, "w+") | 
|  |  | 
|  | if options.time_individual: | 
|  | t1 = time.time() | 
|  | test_dict["start_time"] = float("%.03f" % t1) | 
|  |  | 
|  | try: | 
|  | child = Popen( | 
|  | ("expect", test[2]), | 
|  | shell=False, | 
|  | env=test_env, | 
|  | stdout=testlog, | 
|  | stderr=testlog, | 
|  | ) | 
|  | retcode = child.wait() | 
|  | except KeyboardInterrupt: | 
|  | child.send_signal(signal.SIGINT) | 
|  | retcode = child.wait() | 
|  | abort = True | 
|  |  | 
|  | if options.time_individual: | 
|  | t2 = time.time() | 
|  | minutes = int(int(t2 - t1) / 60) | 
|  | seconds = (int(t2 - t1)) % 60 | 
|  | if minutes > 0: | 
|  | sys.stdout.write("%d min " % (minutes)) | 
|  | sys.stdout.write("%.2f sec " % (seconds)) | 
|  | test_dict["duration"] = float("%.03f" % (t2 - t1)) | 
|  |  | 
|  | if retcode == 0: | 
|  | status = "pass" | 
|  | elif retcode > 127: | 
|  | status = "skip" | 
|  | else: | 
|  | status = "fail" | 
|  |  | 
|  | test_dict["status"] = status | 
|  |  | 
|  | # Determine the reason if requesting a json results file | 
|  | if status != "pass" and options.results_file: | 
|  | testlog.flush() | 
|  | testlog.seek(0) | 
|  | test_output = testlog.read() | 
|  |  | 
|  | sections = [s for s in test_output.split("=" * 78 + "\n")] | 
|  | # header = sections[1] | 
|  | body = sections[2] | 
|  | # footer = "".join(sections[3:]) | 
|  |  | 
|  | fatals = re.findall( | 
|  | r"(?ms)\[[^\]]+\][ \[]+Fatal[ \]:]+(.*?) \(fail[^\)]+\)$", body | 
|  | ) | 
|  | errors = re.findall( | 
|  | r"(?ms)\[[^\]]+\][ \[]+Error[ \]:]+(.*?) \(subfail[^\)]+\)$", body | 
|  | ) | 
|  | warnings = re.findall( | 
|  | r"(?ms)\[[^\]]+\][ \[]+Warning[ \]:]+((?:(?!Warning).)*) \((?:sub)?skip[^\)]+\)$", | 
|  | body, | 
|  | ) | 
|  | if fatals: | 
|  | test_dict["reason"] = fatals[0] | 
|  | elif errors: | 
|  | test_dict["reason"] = errors[0] | 
|  | elif warnings: | 
|  | test_dict["reason"] = warnings[0] | 
|  |  | 
|  | results_list.append(test_dict) | 
|  |  | 
|  | testlog.close() | 
|  |  | 
|  | if status == "pass": | 
|  | passed_tests.append(test) | 
|  | sys.stdout.write("\n") | 
|  | if not options.keep_logs: | 
|  | try: | 
|  | os.remove(testlog_name) | 
|  | except IOError as e: | 
|  | print( | 
|  | "ERROR failed to close %s %s" % (testlog_name, e), | 
|  | file=sys.stederr, | 
|  | ) | 
|  | elif status == "skip": | 
|  | skipped_tests.append(test) | 
|  | sys.stdout.write("SKIPPED\n") | 
|  | if not options.keep_logs: | 
|  | try: | 
|  | os.remove(testlog_name) | 
|  | except IOError as e: | 
|  | print( | 
|  | "ERROR failed to close %s %s" % (testlog_name, e), | 
|  | file=sys.stederr, | 
|  | ) | 
|  | else: | 
|  | failed_tests.append(test) | 
|  | os.rename(testlog_name, testlog_name + ".failed") | 
|  | sys.stdout.write("FAILED!\n") | 
|  | if options.stop_on_first_fail: | 
|  | break | 
|  | sys.stdout.flush() | 
|  |  | 
|  | if abort: | 
|  | sys.stdout.write("\nRegression interrupted!\n") | 
|  | break | 
|  |  | 
|  | end_time = time.time() | 
|  | print("Ended:", time.asctime(time.localtime(end_time)), file=sys.stdout) | 
|  | print( | 
|  | "\nTestsuite ran for %d minutes %d seconds" | 
|  | % ((end_time - start_time) / 60, (end_time - start_time) % 60), | 
|  | file=sys.stdout, | 
|  | ) | 
|  |  | 
|  | if options.results_file: | 
|  | with open(options.results_file, "w") as results_file: | 
|  | json.dump(results_list, results_file) | 
|  |  | 
|  | print("Completions  :", len(passed_tests), file=sys.stdout) | 
|  | print("Failures     :", len(failed_tests), file=sys.stdout) | 
|  | print("Skipped      :", len(skipped_tests), file=sys.stdout) | 
|  | if len(failed_tests) > 0: | 
|  | print("Failed tests : ", file=sys.stdout) | 
|  | first = True | 
|  | for test in failed_tests: | 
|  | if first: | 
|  | first = False | 
|  | else: | 
|  | sys.stdout.write(",") | 
|  | sys.stdout.write("%d.%d" % (test[0], test[1])) | 
|  | sys.stdout.write("\n") | 
|  | sys.stdout.flush() | 
|  |  | 
|  | if abort: | 
|  | print("INCOMPLETE", file=sys.stdout) | 
|  |  | 
|  | if len(failed_tests) > 0: | 
|  | return 1 | 
|  |  | 
|  |  | 
|  | def test_in_list(major, minor, test_list): | 
|  | """Test for whether a test numbered major.minor is in test_list. | 
|  |  | 
|  | "major" and "minor" must be integers.  "test_list" is a list of | 
|  | tuples, each tuple representing one test.  The tuples are of the | 
|  | form: | 
|  |  | 
|  | (major, minor, filename) | 
|  |  | 
|  | Returns True if the test is in the list, and False otherwise. | 
|  | """ | 
|  |  | 
|  | if not test_list: | 
|  | return False | 
|  | for test in test_list: | 
|  | if (test[0] == "*" or test[0] == major) and ( | 
|  | test[1] == "*" or test[1] == minor | 
|  | ): | 
|  | return True | 
|  | return False | 
|  |  | 
|  |  | 
|  | def test_parser(option, opt_str, value, parser): | 
|  | """Option callback function for the optparse.OptionParser class. | 
|  |  | 
|  | Will take a string representing one or more test names and append | 
|  | a tuple representing the test into a list in the options's destination | 
|  | variable. | 
|  |  | 
|  | A string representing test names must patch the regular expression | 
|  | named "test_re" below.  Some examples of exceptable options are: | 
|  |  | 
|  | '1.5' | 
|  | 'test9.8' | 
|  | '2.6 test3.1 14.2' | 
|  | '3.4,6.7,8.3' | 
|  | '1.*' | 
|  | '*.2' | 
|  | '1.*,3.8,9.2' | 
|  |  | 
|  | Raises OptionValueError on error. | 
|  | """ | 
|  |  | 
|  | # Initialize the option's destination array, if is does not already exist. | 
|  | if not hasattr(parser.values, option.dest): | 
|  | setattr(parser.values, option.dest, []) | 
|  | if getattr(parser.values, option.dest) is None: | 
|  | setattr(parser.values, option.dest, []) | 
|  |  | 
|  | # Get a pointer to the option's destination array. | 
|  | dest = getattr(parser.values, option.dest) | 
|  |  | 
|  | # Split the user's option string into a series of tuples that represent | 
|  | # each test, and add each tuple to the destination array. | 
|  | splitter = re.compile(r"[,\s]+") | 
|  | val = splitter.split(value) | 
|  | test_re = re.compile(r"(test)?((\d+)|\*)[\._]((\d+)|\*)$") | 
|  | for v in val: | 
|  | m = test_re.match(v) | 
|  | if not m: | 
|  | raise OptionValueError | 
|  | major = m.group(2) | 
|  | if major != "*": | 
|  | major = int(major) | 
|  | minor = m.group(4) | 
|  | if minor != "*": | 
|  | minor = int(minor) | 
|  | dest.append((major, minor)) | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | sys.exit(main()) |