11b16049aSZach Atkinsfrom abc import ABC, abstractmethod 21b16049aSZach Atkinsimport argparse 31b16049aSZach Atkinsfrom dataclasses import dataclass, field 41b16049aSZach Atkinsimport difflib 51b16049aSZach Atkinsfrom enum import Enum 61b16049aSZach Atkinsfrom math import isclose 71b16049aSZach Atkinsimport os 81b16049aSZach Atkinsfrom pathlib import Path 91b16049aSZach Atkinsimport re 101b16049aSZach Atkinsimport subprocess 1119868e18SZach Atkinsimport multiprocessing as mp 1219868e18SZach Atkinsfrom itertools import product 131b16049aSZach Atkinsimport sys 141b16049aSZach Atkinsimport time 1578cb100bSJames Wrightfrom typing import Optional, Tuple, List 161b16049aSZach Atkins 171b16049aSZach Atkinssys.path.insert(0, str(Path(__file__).parent / "junit-xml")) 181b16049aSZach Atkinsfrom junit_xml import TestCase, TestSuite, to_xml_report_string # nopep8 191b16049aSZach Atkins 201b16049aSZach Atkins 211b16049aSZach Atkinsclass CaseInsensitiveEnumAction(argparse.Action): 221b16049aSZach Atkins """Action to convert input values to lower case prior to converting to an Enum type""" 231b16049aSZach Atkins 241b16049aSZach Atkins def __init__(self, option_strings, dest, type, default, **kwargs): 251b16049aSZach Atkins if not (issubclass(type, Enum) and issubclass(type, str)): 261b16049aSZach Atkins raise ValueError(f"{type} must be a StrEnum or str and Enum") 271b16049aSZach Atkins # store provided enum type 281b16049aSZach Atkins self.enum_type = type 291b16049aSZach Atkins if isinstance(default, str): 301b16049aSZach Atkins default = self.enum_type(default.lower()) 311b16049aSZach Atkins else: 321b16049aSZach Atkins default = [self.enum_type(v.lower()) for v in default] 331b16049aSZach Atkins # prevent automatic type conversion 341b16049aSZach Atkins super().__init__(option_strings, dest, default=default, **kwargs) 351b16049aSZach Atkins 361b16049aSZach Atkins def __call__(self, parser, namespace, values, option_string=None): 371b16049aSZach Atkins if isinstance(values, str): 381b16049aSZach Atkins values = self.enum_type(values.lower()) 391b16049aSZach Atkins else: 401b16049aSZach Atkins values = [self.enum_type(v.lower()) for v in values] 411b16049aSZach Atkins setattr(namespace, self.dest, values) 421b16049aSZach Atkins 431b16049aSZach Atkins 441b16049aSZach Atkins@dataclass 451b16049aSZach Atkinsclass TestSpec: 461b16049aSZach Atkins """Dataclass storing information about a single test case""" 471b16049aSZach Atkins name: str 488938a869SZach Atkins only: List = field(default_factory=list) 498938a869SZach Atkins args: List = field(default_factory=list) 501b16049aSZach Atkins 511b16049aSZach Atkins 521b16049aSZach Atkinsclass RunMode(str, Enum): 531b16049aSZach Atkins """Enumeration of run modes, either `RunMode.TAP` or `RunMode.JUNIT`""" 541b16049aSZach Atkins __str__ = str.__str__ 551b16049aSZach Atkins __format__ = str.__format__ 561b16049aSZach Atkins TAP: str = 'tap' 571b16049aSZach Atkins JUNIT: str = 'junit' 581b16049aSZach Atkins 591b16049aSZach Atkins 601b16049aSZach Atkinsclass SuiteSpec(ABC): 611b16049aSZach Atkins """Abstract Base Class defining the required interface for running a test suite""" 621b16049aSZach Atkins @abstractmethod 631b16049aSZach Atkins def get_source_path(self, test: str) -> Path: 641b16049aSZach Atkins """Compute path to test source file 651b16049aSZach Atkins 661b16049aSZach Atkins Args: 671b16049aSZach Atkins test (str): Name of test 681b16049aSZach Atkins 691b16049aSZach Atkins Returns: 701b16049aSZach Atkins Path: Path to source file 711b16049aSZach Atkins """ 721b16049aSZach Atkins raise NotImplementedError 731b16049aSZach Atkins 741b16049aSZach Atkins @abstractmethod 751b16049aSZach Atkins def get_run_path(self, test: str) -> Path: 761b16049aSZach Atkins """Compute path to built test executable file 771b16049aSZach Atkins 781b16049aSZach Atkins Args: 791b16049aSZach Atkins test (str): Name of test 801b16049aSZach Atkins 811b16049aSZach Atkins Returns: 821b16049aSZach Atkins Path: Path to test executable 831b16049aSZach Atkins """ 841b16049aSZach Atkins raise NotImplementedError 851b16049aSZach Atkins 861b16049aSZach Atkins @abstractmethod 871b16049aSZach Atkins def get_output_path(self, test: str, output_file: str) -> Path: 881b16049aSZach Atkins """Compute path to expected output file 891b16049aSZach Atkins 901b16049aSZach Atkins Args: 911b16049aSZach Atkins test (str): Name of test 921b16049aSZach Atkins output_file (str): File name of output file 931b16049aSZach Atkins 941b16049aSZach Atkins Returns: 951b16049aSZach Atkins Path: Path to expected output file 961b16049aSZach Atkins """ 971b16049aSZach Atkins raise NotImplementedError 981b16049aSZach Atkins 991b16049aSZach Atkins def post_test_hook(self, test: str, spec: TestSpec) -> None: 1001b16049aSZach Atkins """Function callback ran after each test case 1011b16049aSZach Atkins 1021b16049aSZach Atkins Args: 1031b16049aSZach Atkins test (str): Name of test 1041b16049aSZach Atkins spec (TestSpec): Test case specification 1051b16049aSZach Atkins """ 1061b16049aSZach Atkins pass 1071b16049aSZach Atkins 1081b16049aSZach Atkins def check_pre_skip(self, test: str, spec: TestSpec, resource: str, nproc: int) -> Optional[str]: 1091b16049aSZach Atkins """Check if a test case should be skipped prior to running, returning the reason for skipping 1101b16049aSZach Atkins 1111b16049aSZach Atkins Args: 1121b16049aSZach Atkins test (str): Name of test 1131b16049aSZach Atkins spec (TestSpec): Test case specification 1141b16049aSZach Atkins resource (str): libCEED backend 1151b16049aSZach Atkins nproc (int): Number of MPI processes to use when running test case 1161b16049aSZach Atkins 1171b16049aSZach Atkins Returns: 1181b16049aSZach Atkins Optional[str]: Skip reason, or `None` if test case should not be skipped 1191b16049aSZach Atkins """ 1201b16049aSZach Atkins return None 1211b16049aSZach Atkins 1221b16049aSZach Atkins def check_post_skip(self, test: str, spec: TestSpec, resource: str, stderr: str) -> Optional[str]: 1231b16049aSZach Atkins """Check if a test case should be allowed to fail, based on its stderr output 1241b16049aSZach Atkins 1251b16049aSZach Atkins Args: 1261b16049aSZach Atkins test (str): Name of test 1271b16049aSZach Atkins spec (TestSpec): Test case specification 1281b16049aSZach Atkins resource (str): libCEED backend 1291b16049aSZach Atkins stderr (str): Standard error output from test case execution 1301b16049aSZach Atkins 1311b16049aSZach Atkins Returns: 13219868e18SZach Atkins Optional[str]: Skip reason, or `None` if unexpected error 1331b16049aSZach Atkins """ 1341b16049aSZach Atkins return None 1351b16049aSZach Atkins 13678cb100bSJames Wright def check_required_failure(self, test: str, spec: TestSpec, resource: str, stderr: str) -> Tuple[str, bool]: 1371b16049aSZach Atkins """Check whether a test case is expected to fail and if it failed expectedly 1381b16049aSZach Atkins 1391b16049aSZach Atkins Args: 1401b16049aSZach Atkins test (str): Name of test 1411b16049aSZach Atkins spec (TestSpec): Test case specification 1421b16049aSZach Atkins resource (str): libCEED backend 1431b16049aSZach Atkins stderr (str): Standard error output from test case execution 1441b16049aSZach Atkins 1451b16049aSZach Atkins Returns: 1461b16049aSZach Atkins tuple[str, bool]: Tuple of the expected failure string and whether it was present in `stderr` 1471b16049aSZach Atkins """ 1481b16049aSZach Atkins return '', True 1491b16049aSZach Atkins 1501b16049aSZach Atkins def check_allowed_stdout(self, test: str) -> bool: 1511b16049aSZach Atkins """Check whether a test is allowed to print console output 1521b16049aSZach Atkins 1531b16049aSZach Atkins Args: 1541b16049aSZach Atkins test (str): Name of test 1551b16049aSZach Atkins 1561b16049aSZach Atkins Returns: 1571b16049aSZach Atkins bool: True if the test is allowed to print console output 1581b16049aSZach Atkins """ 1591b16049aSZach Atkins return False 1601b16049aSZach Atkins 1611b16049aSZach Atkins 1621b16049aSZach Atkinsdef has_cgnsdiff() -> bool: 1631b16049aSZach Atkins """Check whether `cgnsdiff` is an executable program in the current environment 1641b16049aSZach Atkins 1651b16049aSZach Atkins Returns: 1661b16049aSZach Atkins bool: True if `cgnsdiff` is found 1671b16049aSZach Atkins """ 1681b16049aSZach Atkins my_env: dict = os.environ.copy() 1691b16049aSZach Atkins proc = subprocess.run('cgnsdiff', 1701b16049aSZach Atkins shell=True, 1711b16049aSZach Atkins stdout=subprocess.PIPE, 1721b16049aSZach Atkins stderr=subprocess.PIPE, 1731b16049aSZach Atkins env=my_env) 1741b16049aSZach Atkins return 'not found' not in proc.stderr.decode('utf-8') 1751b16049aSZach Atkins 1761b16049aSZach Atkins 17778cb100bSJames Wrightdef contains_any(base: str, substrings: List[str]) -> bool: 1781b16049aSZach Atkins """Helper function, checks if any of the substrings are included in the base string 1791b16049aSZach Atkins 1801b16049aSZach Atkins Args: 1811b16049aSZach Atkins base (str): Base string to search in 1828938a869SZach Atkins substrings (List[str]): List of potential substrings 1831b16049aSZach Atkins 1841b16049aSZach Atkins Returns: 1851b16049aSZach Atkins bool: True if any substrings are included in base string 1861b16049aSZach Atkins """ 1871b16049aSZach Atkins return any((sub in base for sub in substrings)) 1881b16049aSZach Atkins 1891b16049aSZach Atkins 19078cb100bSJames Wrightdef startswith_any(base: str, prefixes: List[str]) -> bool: 1911b16049aSZach Atkins """Helper function, checks if the base string is prefixed by any of `prefixes` 1921b16049aSZach Atkins 1931b16049aSZach Atkins Args: 1941b16049aSZach Atkins base (str): Base string to search 1958938a869SZach Atkins prefixes (List[str]): List of potential prefixes 1961b16049aSZach Atkins 1971b16049aSZach Atkins Returns: 1981b16049aSZach Atkins bool: True if base string is prefixed by any of the prefixes 1991b16049aSZach Atkins """ 2001b16049aSZach Atkins return any((base.startswith(prefix) for prefix in prefixes)) 2011b16049aSZach Atkins 2021b16049aSZach Atkins 2031b16049aSZach Atkinsdef parse_test_line(line: str) -> TestSpec: 2041b16049aSZach Atkins """Parse a single line of TESTARGS and CLI arguments into a `TestSpec` object 2051b16049aSZach Atkins 2061b16049aSZach Atkins Args: 2071b16049aSZach Atkins line (str): String containing TESTARGS specification and CLI arguments 2081b16049aSZach Atkins 2091b16049aSZach Atkins Returns: 2101b16049aSZach Atkins TestSpec: Parsed specification of test case 2111b16049aSZach Atkins """ 21278cb100bSJames Wright args: List[str] = re.findall("(?:\".*?\"|\\S)+", line.strip()) 2131b16049aSZach Atkins if args[0] == 'TESTARGS': 2141b16049aSZach Atkins return TestSpec(name='', args=args[1:]) 2151b16049aSZach Atkins raw_test_args: str = args[0][args[0].index('TESTARGS(') + 9:args[0].rindex(')')] 2161b16049aSZach Atkins # transform 'name="myname",only="serial,int32"' into {'name': 'myname', 'only': 'serial,int32'} 2171b16049aSZach Atkins test_args: dict = dict([''.join(t).split('=') for t in re.findall(r"""([^,=]+)(=)"([^"]*)\"""", raw_test_args)]) 218f85e4a7bSJeremy L Thompson name: str = test_args.get('name', '') 21978cb100bSJames Wright constraints: List[str] = test_args['only'].split(',') if 'only' in test_args else [] 2201b16049aSZach Atkins if len(args) > 1: 221f85e4a7bSJeremy L Thompson return TestSpec(name=name, only=constraints, args=args[1:]) 2221b16049aSZach Atkins else: 223f85e4a7bSJeremy L Thompson return TestSpec(name=name, only=constraints) 2241b16049aSZach Atkins 2251b16049aSZach Atkins 22678cb100bSJames Wrightdef get_test_args(source_file: Path) -> List[TestSpec]: 2271b16049aSZach Atkins """Parse all test cases from a given source file 2281b16049aSZach Atkins 2291b16049aSZach Atkins Args: 2301b16049aSZach Atkins source_file (Path): Path to source file 2311b16049aSZach Atkins 2321b16049aSZach Atkins Raises: 2331b16049aSZach Atkins RuntimeError: Errors if source file extension is unsupported 2341b16049aSZach Atkins 2351b16049aSZach Atkins Returns: 2368938a869SZach Atkins List[TestSpec]: List of parsed `TestSpec` objects, or a list containing a single, default `TestSpec` if none were found 2371b16049aSZach Atkins """ 2381b16049aSZach Atkins comment_str: str = '' 2391b16049aSZach Atkins if source_file.suffix in ['.c', '.cpp']: 2401b16049aSZach Atkins comment_str = '//' 2411b16049aSZach Atkins elif source_file.suffix in ['.py']: 2421b16049aSZach Atkins comment_str = '#' 2431b16049aSZach Atkins elif source_file.suffix in ['.usr']: 2441b16049aSZach Atkins comment_str = 'C_' 2451b16049aSZach Atkins elif source_file.suffix in ['.f90']: 2461b16049aSZach Atkins comment_str = '! ' 2471b16049aSZach Atkins else: 2481b16049aSZach Atkins raise RuntimeError(f'Unrecognized extension for file: {source_file}') 2491b16049aSZach Atkins 2501b16049aSZach Atkins return [parse_test_line(line.strip(comment_str)) 2511b16049aSZach Atkins for line in source_file.read_text().splitlines() 2521b16049aSZach Atkins if line.startswith(f'{comment_str}TESTARGS')] or [TestSpec('', args=['{ceed_resource}'])] 2531b16049aSZach Atkins 2541b16049aSZach Atkins 2551b16049aSZach Atkinsdef diff_csv(test_csv: Path, true_csv: Path, zero_tol: float = 3e-10, rel_tol: float = 1e-2) -> str: 2561b16049aSZach Atkins """Compare CSV results against an expected CSV file with tolerances 2571b16049aSZach Atkins 2581b16049aSZach Atkins Args: 2591b16049aSZach Atkins test_csv (Path): Path to output CSV results 2601b16049aSZach Atkins true_csv (Path): Path to expected CSV results 2611b16049aSZach Atkins zero_tol (float, optional): Tolerance below which values are considered to be zero. Defaults to 3e-10. 2621b16049aSZach Atkins rel_tol (float, optional): Relative tolerance for comparing non-zero values. Defaults to 1e-2. 2631b16049aSZach Atkins 2641b16049aSZach Atkins Returns: 2651b16049aSZach Atkins str: Diff output between result and expected CSVs 2661b16049aSZach Atkins """ 26778cb100bSJames Wright test_lines: List[str] = test_csv.read_text().splitlines() 26878cb100bSJames Wright true_lines: List[str] = true_csv.read_text().splitlines() 2691b16049aSZach Atkins 2701b16049aSZach Atkins if test_lines[0] != true_lines[0]: 2711b16049aSZach Atkins return ''.join(difflib.unified_diff([f'{test_lines[0]}\n'], [f'{true_lines[0]}\n'], 2721b16049aSZach Atkins tofile='found CSV columns', fromfile='expected CSV columns')) 2731b16049aSZach Atkins 27478cb100bSJames Wright diff_lines: List[str] = list() 27578cb100bSJames Wright column_names: List[str] = true_lines[0].strip().split(',') 2761b16049aSZach Atkins for test_line, true_line in zip(test_lines[1:], true_lines[1:]): 27778cb100bSJames Wright test_vals: List[float] = [float(val.strip()) for val in test_line.strip().split(',')] 27878cb100bSJames Wright true_vals: List[float] = [float(val.strip()) for val in true_line.strip().split(',')] 2791b16049aSZach Atkins for test_val, true_val, column_name in zip(test_vals, true_vals, column_names): 2801b16049aSZach Atkins true_zero: bool = abs(true_val) < zero_tol 2811b16049aSZach Atkins test_zero: bool = abs(test_val) < zero_tol 2821b16049aSZach Atkins fail: bool = False 2831b16049aSZach Atkins if true_zero: 2841b16049aSZach Atkins fail = not test_zero 2851b16049aSZach Atkins else: 2861b16049aSZach Atkins fail = not isclose(test_val, true_val, rel_tol=rel_tol) 2871b16049aSZach Atkins if fail: 2881b16049aSZach Atkins diff_lines.append(f'step: {true_line[0]}, column: {column_name}, expected: {true_val}, got: {test_val}') 2891b16049aSZach Atkins return '\n'.join(diff_lines) 2901b16049aSZach Atkins 2911b16049aSZach Atkins 2921b16049aSZach Atkinsdef diff_cgns(test_cgns: Path, true_cgns: Path, tolerance: float = 1e-12) -> str: 2931b16049aSZach Atkins """Compare CGNS results against an expected CGSN file with tolerance 2941b16049aSZach Atkins 2951b16049aSZach Atkins Args: 2961b16049aSZach Atkins test_cgns (Path): Path to output CGNS file 2971b16049aSZach Atkins true_cgns (Path): Path to expected CGNS file 2981b16049aSZach Atkins tolerance (float, optional): Tolerance for comparing floating-point values 2991b16049aSZach Atkins 3001b16049aSZach Atkins Returns: 3011b16049aSZach Atkins str: Diff output between result and expected CGNS files 3021b16049aSZach Atkins """ 3031b16049aSZach Atkins my_env: dict = os.environ.copy() 3041b16049aSZach Atkins 30578cb100bSJames Wright run_args: List[str] = ['cgnsdiff', '-d', '-t', f'{tolerance}', str(test_cgns), str(true_cgns)] 3061b16049aSZach Atkins proc = subprocess.run(' '.join(run_args), 3071b16049aSZach Atkins shell=True, 3081b16049aSZach Atkins stdout=subprocess.PIPE, 3091b16049aSZach Atkins stderr=subprocess.PIPE, 3101b16049aSZach Atkins env=my_env) 3111b16049aSZach Atkins 3121b16049aSZach Atkins return proc.stderr.decode('utf-8') + proc.stdout.decode('utf-8') 3131b16049aSZach Atkins 3141b16049aSZach Atkins 315*e17e35bbSJames Wrightdef test_case_output_string(test_case: TestCase, spec: TestSpec, mode: RunMode, 316*e17e35bbSJames Wright backend: str, test: str, index: int) -> str: 317*e17e35bbSJames Wright output_str = '' 318*e17e35bbSJames Wright if mode is RunMode.TAP: 319*e17e35bbSJames Wright # print incremental output if TAP mode 320*e17e35bbSJames Wright if test_case.is_skipped(): 321*e17e35bbSJames Wright output_str += f' ok {index} - {spec.name}, {backend} # SKIP {test_case.skipped[0]["message"]}\n' 322*e17e35bbSJames Wright elif test_case.is_failure() or test_case.is_error(): 323*e17e35bbSJames Wright output_str += f' not ok {index} - {spec.name}, {backend}\n' 324*e17e35bbSJames Wright else: 325*e17e35bbSJames Wright output_str += f' ok {index} - {spec.name}, {backend}\n' 326*e17e35bbSJames Wright output_str += f' ---\n' 327*e17e35bbSJames Wright if spec.only: 328*e17e35bbSJames Wright output_str += f' only: {",".join(spec.only)}\n' 329*e17e35bbSJames Wright output_str += f' args: {test_case.args}\n' 330*e17e35bbSJames Wright if test_case.is_error(): 331*e17e35bbSJames Wright output_str += f' error: {test_case.errors[0]["message"]}\n' 332*e17e35bbSJames Wright if test_case.is_failure(): 333*e17e35bbSJames Wright output_str += f' num_failures: {len(test_case.failures)}\n' 334*e17e35bbSJames Wright for i, failure in enumerate(test_case.failures): 335*e17e35bbSJames Wright output_str += f' failure_{i}: {failure["message"]}\n' 336*e17e35bbSJames Wright output_str += f' message: {failure["message"]}\n' 337*e17e35bbSJames Wright if failure["output"]: 338*e17e35bbSJames Wright out = failure["output"].strip().replace('\n', '\n ') 339*e17e35bbSJames Wright output_str += f' output: |\n {out}\n' 340*e17e35bbSJames Wright output_str += f' ...\n' 341*e17e35bbSJames Wright else: 342*e17e35bbSJames Wright # print error or failure information if JUNIT mode 343*e17e35bbSJames Wright if test_case.is_error() or test_case.is_failure(): 344*e17e35bbSJames Wright output_str += f'Test: {test} {spec.name}\n' 345*e17e35bbSJames Wright output_str += f' $ {test_case.args}\n' 346*e17e35bbSJames Wright if test_case.is_error(): 347*e17e35bbSJames Wright output_str += 'ERROR: {}\n'.format((test_case.errors[0]['message'] or 'NO MESSAGE').strip()) 348*e17e35bbSJames Wright output_str += 'Output: \n{}\n'.format((test_case.errors[0]['output'] or 'NO MESSAGE').strip()) 349*e17e35bbSJames Wright if test_case.is_failure(): 350*e17e35bbSJames Wright for failure in test_case.failures: 351*e17e35bbSJames Wright output_str += 'FAIL: {}\n'.format((failure['message'] or 'NO MESSAGE').strip()) 352*e17e35bbSJames Wright output_str += 'Output: \n{}\n'.format((failure['output'] or 'NO MESSAGE').strip()) 353*e17e35bbSJames Wright return output_str 354*e17e35bbSJames Wright 355*e17e35bbSJames Wright 35619868e18SZach Atkinsdef run_test(index: int, test: str, spec: TestSpec, backend: str, 35719868e18SZach Atkins mode: RunMode, nproc: int, suite_spec: SuiteSpec) -> TestCase: 35819868e18SZach Atkins """Run a single test case and backend combination 3591b16049aSZach Atkins 3601b16049aSZach Atkins Args: 3618938a869SZach Atkins index (int): Index of backend for current spec 36219868e18SZach Atkins test (str): Path to test 36319868e18SZach Atkins spec (TestSpec): Specification of test case 36419868e18SZach Atkins backend (str): CEED backend 36519868e18SZach Atkins mode (RunMode): Output mode 36619868e18SZach Atkins nproc (int): Number of MPI processes to use when running test case 36719868e18SZach Atkins suite_spec (SuiteSpec): Specification of test suite 3681b16049aSZach Atkins 3691b16049aSZach Atkins Returns: 37019868e18SZach Atkins TestCase: Test case result 3711b16049aSZach Atkins """ 3721b16049aSZach Atkins source_path: Path = suite_spec.get_source_path(test) 3738938a869SZach Atkins run_args: List = [f'{suite_spec.get_run_path(test)}', *map(str, spec.args)] 3741b16049aSZach Atkins 3751b16049aSZach Atkins if '{ceed_resource}' in run_args: 37619868e18SZach Atkins run_args[run_args.index('{ceed_resource}')] = backend 3778938a869SZach Atkins for i, arg in enumerate(run_args): 3788938a869SZach Atkins if '{ceed_resource}' in arg: 3798938a869SZach Atkins run_args[i] = arg.replace('{ceed_resource}', backend.replace('/', '-')) 3801b16049aSZach Atkins if '{nproc}' in run_args: 3811b16049aSZach Atkins run_args[run_args.index('{nproc}')] = f'{nproc}' 3821b16049aSZach Atkins elif nproc > 1 and source_path.suffix != '.py': 3831b16049aSZach Atkins run_args = ['mpiexec', '-n', f'{nproc}', *run_args] 3841b16049aSZach Atkins 3851b16049aSZach Atkins # run test 38619868e18SZach Atkins skip_reason: str = suite_spec.check_pre_skip(test, spec, backend, nproc) 3871b16049aSZach Atkins if skip_reason: 38819868e18SZach Atkins test_case: TestCase = TestCase(f'{test}, "{spec.name}", n{nproc}, {backend}', 3891b16049aSZach Atkins elapsed_sec=0, 3901b16049aSZach Atkins timestamp=time.strftime('%Y-%m-%d %H:%M:%S %Z', time.localtime()), 3911b16049aSZach Atkins stdout='', 3928938a869SZach Atkins stderr='', 3938938a869SZach Atkins category=spec.name,) 3941b16049aSZach Atkins test_case.add_skipped_info(skip_reason) 3951b16049aSZach Atkins else: 3961b16049aSZach Atkins start: float = time.time() 3971b16049aSZach Atkins proc = subprocess.run(' '.join(str(arg) for arg in run_args), 3981b16049aSZach Atkins shell=True, 3991b16049aSZach Atkins stdout=subprocess.PIPE, 4001b16049aSZach Atkins stderr=subprocess.PIPE, 4011b16049aSZach Atkins env=my_env) 4021b16049aSZach Atkins 40319868e18SZach Atkins test_case = TestCase(f'{test}, "{spec.name}", n{nproc}, {backend}', 4041b16049aSZach Atkins classname=source_path.parent, 4051b16049aSZach Atkins elapsed_sec=time.time() - start, 4061b16049aSZach Atkins timestamp=time.strftime('%Y-%m-%d %H:%M:%S %Z', time.localtime(start)), 4071b16049aSZach Atkins stdout=proc.stdout.decode('utf-8'), 4081b16049aSZach Atkins stderr=proc.stderr.decode('utf-8'), 4098938a869SZach Atkins allow_multiple_subelements=True, 4108938a869SZach Atkins category=spec.name,) 41178cb100bSJames Wright ref_csvs: List[Path] = [] 4128938a869SZach Atkins output_files: List[str] = [arg for arg in run_args if 'ascii:' in arg] 41397fab443SJeremy L Thompson if output_files: 4141b16049aSZach Atkins ref_csvs = [suite_spec.get_output_path(test, file.split('ascii:')[-1]) for file in output_files] 41578cb100bSJames Wright ref_cgns: List[Path] = [] 4168938a869SZach Atkins output_files = [arg for arg in run_args if 'cgns:' in arg] 41797fab443SJeremy L Thompson if output_files: 4181b16049aSZach Atkins ref_cgns = [suite_spec.get_output_path(test, file.split('cgns:')[-1]) for file in output_files] 4191b16049aSZach Atkins ref_stdout: Path = suite_spec.get_output_path(test, test + '.out') 4201b16049aSZach Atkins suite_spec.post_test_hook(test, spec) 4211b16049aSZach Atkins 4221b16049aSZach Atkins # check allowed failures 4231b16049aSZach Atkins if not test_case.is_skipped() and test_case.stderr: 42419868e18SZach Atkins skip_reason: str = suite_spec.check_post_skip(test, spec, backend, test_case.stderr) 4251b16049aSZach Atkins if skip_reason: 4261b16049aSZach Atkins test_case.add_skipped_info(skip_reason) 4271b16049aSZach Atkins 4281b16049aSZach Atkins # check required failures 4291b16049aSZach Atkins if not test_case.is_skipped(): 4302fee3251SSebastian Grimberg required_message, did_fail = suite_spec.check_required_failure( 43119868e18SZach Atkins test, spec, backend, test_case.stderr) 4321b16049aSZach Atkins if required_message and did_fail: 4331b16049aSZach Atkins test_case.status = f'fails with required: {required_message}' 4341b16049aSZach Atkins elif required_message: 4351b16049aSZach Atkins test_case.add_failure_info(f'required failure missing: {required_message}') 4361b16049aSZach Atkins 4371b16049aSZach Atkins # classify other results 4381b16049aSZach Atkins if not test_case.is_skipped() and not test_case.status: 4391b16049aSZach Atkins if test_case.stderr: 4401b16049aSZach Atkins test_case.add_failure_info('stderr', test_case.stderr) 4411b16049aSZach Atkins if proc.returncode != 0: 4421b16049aSZach Atkins test_case.add_error_info(f'returncode = {proc.returncode}') 4431b16049aSZach Atkins if ref_stdout.is_file(): 4441b16049aSZach Atkins diff = list(difflib.unified_diff(ref_stdout.read_text().splitlines(keepends=True), 4451b16049aSZach Atkins test_case.stdout.splitlines(keepends=True), 4461b16049aSZach Atkins fromfile=str(ref_stdout), 4471b16049aSZach Atkins tofile='New')) 4481b16049aSZach Atkins if diff: 4491b16049aSZach Atkins test_case.add_failure_info('stdout', output=''.join(diff)) 4501b16049aSZach Atkins elif test_case.stdout and not suite_spec.check_allowed_stdout(test): 4511b16049aSZach Atkins test_case.add_failure_info('stdout', output=test_case.stdout) 4521b16049aSZach Atkins # expected CSV output 4531b16049aSZach Atkins for ref_csv in ref_csvs: 4548938a869SZach Atkins csv_name = ref_csv.name 4558938a869SZach Atkins if not ref_csv.is_file(): 4568938a869SZach Atkins # remove _{ceed_backend} from path name 4578938a869SZach Atkins ref_csv = (ref_csv.parent / ref_csv.name.rsplit('_', 1)[0]).with_suffix('.csv') 4581b16049aSZach Atkins if not ref_csv.is_file(): 4591b16049aSZach Atkins test_case.add_failure_info('csv', output=f'{ref_csv} not found') 4601b16049aSZach Atkins else: 4618938a869SZach Atkins diff: str = diff_csv(Path.cwd() / csv_name, ref_csv) 4621b16049aSZach Atkins if diff: 4631b16049aSZach Atkins test_case.add_failure_info('csv', output=diff) 4641b16049aSZach Atkins else: 4658938a869SZach Atkins (Path.cwd() / csv_name).unlink() 4661b16049aSZach Atkins # expected CGNS output 4671b16049aSZach Atkins for ref_cgn in ref_cgns: 4688938a869SZach Atkins cgn_name = ref_cgn.name 4698938a869SZach Atkins if not ref_cgn.is_file(): 4708938a869SZach Atkins # remove _{ceed_backend} from path name 4718938a869SZach Atkins ref_cgn = (ref_cgn.parent / ref_cgn.name.rsplit('_', 1)[0]).with_suffix('.cgns') 4721b16049aSZach Atkins if not ref_cgn.is_file(): 4731b16049aSZach Atkins test_case.add_failure_info('cgns', output=f'{ref_cgn} not found') 4741b16049aSZach Atkins else: 4758938a869SZach Atkins diff = diff_cgns(Path.cwd() / cgn_name, ref_cgn) 4761b16049aSZach Atkins if diff: 4771b16049aSZach Atkins test_case.add_failure_info('cgns', output=diff) 4781b16049aSZach Atkins else: 4798938a869SZach Atkins (Path.cwd() / cgn_name).unlink() 4801b16049aSZach Atkins 4811b16049aSZach Atkins # store result 4821b16049aSZach Atkins test_case.args = ' '.join(str(arg) for arg in run_args) 483*e17e35bbSJames Wright output_str = test_case_output_string(test_case, spec, mode, backend, test, index) 48419868e18SZach Atkins 48519868e18SZach Atkins return test_case, output_str 48619868e18SZach Atkins 48719868e18SZach Atkins 48819868e18SZach Atkinsdef init_process(): 48919868e18SZach Atkins """Initialize multiprocessing process""" 49019868e18SZach Atkins # set up error handler 49119868e18SZach Atkins global my_env 49219868e18SZach Atkins my_env = os.environ.copy() 49319868e18SZach Atkins my_env['CEED_ERROR_HANDLER'] = 'exit' 49419868e18SZach Atkins 49519868e18SZach Atkins 49678cb100bSJames Wrightdef run_tests(test: str, ceed_backends: List[str], mode: RunMode, nproc: int, 49719868e18SZach Atkins suite_spec: SuiteSpec, pool_size: int = 1) -> TestSuite: 49819868e18SZach Atkins """Run all test cases for `test` with each of the provided `ceed_backends` 49919868e18SZach Atkins 50019868e18SZach Atkins Args: 50119868e18SZach Atkins test (str): Name of test 5028938a869SZach Atkins ceed_backends (List[str]): List of libCEED backends 50319868e18SZach Atkins mode (RunMode): Output mode, either `RunMode.TAP` or `RunMode.JUNIT` 50419868e18SZach Atkins nproc (int): Number of MPI processes to use when running each test case 50519868e18SZach Atkins suite_spec (SuiteSpec): Object defining required methods for running tests 50619868e18SZach Atkins pool_size (int, optional): Number of processes to use when running tests in parallel. Defaults to 1. 50719868e18SZach Atkins 50819868e18SZach Atkins Returns: 50919868e18SZach Atkins TestSuite: JUnit `TestSuite` containing results of all test cases 51019868e18SZach Atkins """ 51178cb100bSJames Wright test_specs: List[TestSpec] = get_test_args(suite_spec.get_source_path(test)) 51219868e18SZach Atkins if mode is RunMode.TAP: 5138938a869SZach Atkins print('TAP version 13') 5148938a869SZach Atkins print(f'1..{len(test_specs)}') 51519868e18SZach Atkins 51619868e18SZach Atkins with mp.Pool(processes=pool_size, initializer=init_process) as pool: 5178938a869SZach Atkins async_outputs: List[List[mp.AsyncResult]] = [ 5188938a869SZach Atkins [pool.apply_async(run_test, (i, test, spec, backend, mode, nproc, suite_spec)) 5198938a869SZach Atkins for (i, backend) in enumerate(ceed_backends, start=1)] 5208938a869SZach Atkins for spec in test_specs 5218938a869SZach Atkins ] 52219868e18SZach Atkins 52319868e18SZach Atkins test_cases = [] 5248938a869SZach Atkins for (i, subtest) in enumerate(async_outputs, start=1): 5258938a869SZach Atkins is_new_subtest = True 5268938a869SZach Atkins subtest_ok = True 5278938a869SZach Atkins for async_output in subtest: 52819868e18SZach Atkins test_case, print_output = async_output.get() 52919868e18SZach Atkins test_cases.append(test_case) 5308938a869SZach Atkins if is_new_subtest and mode == RunMode.TAP: 5318938a869SZach Atkins is_new_subtest = False 5328938a869SZach Atkins print(f'# Subtest: {test_case.category}') 5338938a869SZach Atkins print(f' 1..{len(ceed_backends)}') 53419868e18SZach Atkins print(print_output, end='') 5358938a869SZach Atkins if test_case.is_failure() or test_case.is_error(): 5368938a869SZach Atkins subtest_ok = False 5378938a869SZach Atkins if mode == RunMode.TAP: 5388938a869SZach Atkins print(f'{"" if subtest_ok else "not "}ok {i} - {test_case.category}') 5391b16049aSZach Atkins 5401b16049aSZach Atkins return TestSuite(test, test_cases) 5411b16049aSZach Atkins 5421b16049aSZach Atkins 5431b16049aSZach Atkinsdef write_junit_xml(test_suite: TestSuite, output_file: Optional[Path], batch: str = '') -> None: 5441b16049aSZach Atkins """Write a JUnit XML file containing the results of a `TestSuite` 5451b16049aSZach Atkins 5461b16049aSZach Atkins Args: 5471b16049aSZach Atkins test_suite (TestSuite): JUnit `TestSuite` to write 5481b16049aSZach Atkins output_file (Optional[Path]): Path to output file, or `None` to generate automatically as `build/{test_suite.name}{batch}.junit` 5491b16049aSZach Atkins batch (str): Name of JUnit batch, defaults to empty string 5501b16049aSZach Atkins """ 5511b16049aSZach Atkins output_file: Path = output_file or Path('build') / (f'{test_suite.name}{batch}.junit') 5521b16049aSZach Atkins output_file.write_text(to_xml_report_string([test_suite])) 5531b16049aSZach Atkins 5541b16049aSZach Atkins 5551b16049aSZach Atkinsdef has_failures(test_suite: TestSuite) -> bool: 5561b16049aSZach Atkins """Check whether any test cases in a `TestSuite` failed 5571b16049aSZach Atkins 5581b16049aSZach Atkins Args: 5591b16049aSZach Atkins test_suite (TestSuite): JUnit `TestSuite` to check 5601b16049aSZach Atkins 5611b16049aSZach Atkins Returns: 5621b16049aSZach Atkins bool: True if any test cases failed 5631b16049aSZach Atkins """ 5641b16049aSZach Atkins return any(c.is_failure() or c.is_error() for c in test_suite.test_cases) 565