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 111b16049aSZach Atkinsimport sys 121b16049aSZach Atkinsimport time 131b16049aSZach Atkinsfrom typing import Optional 141b16049aSZach Atkins 151b16049aSZach Atkinssys.path.insert(0, str(Path(__file__).parent / "junit-xml")) 161b16049aSZach Atkinsfrom junit_xml import TestCase, TestSuite, to_xml_report_string # nopep8 171b16049aSZach Atkins 181b16049aSZach Atkins 191b16049aSZach Atkinsclass CaseInsensitiveEnumAction(argparse.Action): 201b16049aSZach Atkins """Action to convert input values to lower case prior to converting to an Enum type""" 211b16049aSZach Atkins 221b16049aSZach Atkins def __init__(self, option_strings, dest, type, default, **kwargs): 231b16049aSZach Atkins if not (issubclass(type, Enum) and issubclass(type, str)): 241b16049aSZach Atkins raise ValueError(f"{type} must be a StrEnum or str and Enum") 251b16049aSZach Atkins # store provided enum type 261b16049aSZach Atkins self.enum_type = type 271b16049aSZach Atkins if isinstance(default, str): 281b16049aSZach Atkins default = self.enum_type(default.lower()) 291b16049aSZach Atkins else: 301b16049aSZach Atkins default = [self.enum_type(v.lower()) for v in default] 311b16049aSZach Atkins # prevent automatic type conversion 321b16049aSZach Atkins super().__init__(option_strings, dest, default=default, **kwargs) 331b16049aSZach Atkins 341b16049aSZach Atkins def __call__(self, parser, namespace, values, option_string=None): 351b16049aSZach Atkins if isinstance(values, str): 361b16049aSZach Atkins values = self.enum_type(values.lower()) 371b16049aSZach Atkins else: 381b16049aSZach Atkins values = [self.enum_type(v.lower()) for v in values] 391b16049aSZach Atkins setattr(namespace, self.dest, values) 401b16049aSZach Atkins 411b16049aSZach Atkins 421b16049aSZach Atkins@dataclass 431b16049aSZach Atkinsclass TestSpec: 441b16049aSZach Atkins """Dataclass storing information about a single test case""" 451b16049aSZach Atkins name: str 461b16049aSZach Atkins only: list = field(default_factory=list) 471b16049aSZach Atkins args: list = field(default_factory=list) 481b16049aSZach Atkins 491b16049aSZach Atkins 501b16049aSZach Atkinsclass RunMode(str, Enum): 511b16049aSZach Atkins """Enumeration of run modes, either `RunMode.TAP` or `RunMode.JUNIT`""" 521b16049aSZach Atkins __str__ = str.__str__ 531b16049aSZach Atkins __format__ = str.__format__ 541b16049aSZach Atkins TAP: str = 'tap' 551b16049aSZach Atkins JUNIT: str = 'junit' 561b16049aSZach Atkins 571b16049aSZach Atkins 581b16049aSZach Atkinsclass SuiteSpec(ABC): 591b16049aSZach Atkins """Abstract Base Class defining the required interface for running a test suite""" 601b16049aSZach Atkins @abstractmethod 611b16049aSZach Atkins def get_source_path(self, test: str) -> Path: 621b16049aSZach Atkins """Compute path to test source file 631b16049aSZach Atkins 641b16049aSZach Atkins Args: 651b16049aSZach Atkins test (str): Name of test 661b16049aSZach Atkins 671b16049aSZach Atkins Returns: 681b16049aSZach Atkins Path: Path to source file 691b16049aSZach Atkins """ 701b16049aSZach Atkins raise NotImplementedError 711b16049aSZach Atkins 721b16049aSZach Atkins @abstractmethod 731b16049aSZach Atkins def get_run_path(self, test: str) -> Path: 741b16049aSZach Atkins """Compute path to built test executable file 751b16049aSZach Atkins 761b16049aSZach Atkins Args: 771b16049aSZach Atkins test (str): Name of test 781b16049aSZach Atkins 791b16049aSZach Atkins Returns: 801b16049aSZach Atkins Path: Path to test executable 811b16049aSZach Atkins """ 821b16049aSZach Atkins raise NotImplementedError 831b16049aSZach Atkins 841b16049aSZach Atkins @abstractmethod 851b16049aSZach Atkins def get_output_path(self, test: str, output_file: str) -> Path: 861b16049aSZach Atkins """Compute path to expected output file 871b16049aSZach Atkins 881b16049aSZach Atkins Args: 891b16049aSZach Atkins test (str): Name of test 901b16049aSZach Atkins output_file (str): File name of output file 911b16049aSZach Atkins 921b16049aSZach Atkins Returns: 931b16049aSZach Atkins Path: Path to expected output file 941b16049aSZach Atkins """ 951b16049aSZach Atkins raise NotImplementedError 961b16049aSZach Atkins 971b16049aSZach Atkins def post_test_hook(self, test: str, spec: TestSpec) -> None: 981b16049aSZach Atkins """Function callback ran after each test case 991b16049aSZach Atkins 1001b16049aSZach Atkins Args: 1011b16049aSZach Atkins test (str): Name of test 1021b16049aSZach Atkins spec (TestSpec): Test case specification 1031b16049aSZach Atkins """ 1041b16049aSZach Atkins pass 1051b16049aSZach Atkins 1061b16049aSZach Atkins def check_pre_skip(self, test: str, spec: TestSpec, resource: str, nproc: int) -> Optional[str]: 1071b16049aSZach Atkins """Check if a test case should be skipped prior to running, returning the reason for skipping 1081b16049aSZach Atkins 1091b16049aSZach Atkins Args: 1101b16049aSZach Atkins test (str): Name of test 1111b16049aSZach Atkins spec (TestSpec): Test case specification 1121b16049aSZach Atkins resource (str): libCEED backend 1131b16049aSZach Atkins nproc (int): Number of MPI processes to use when running test case 1141b16049aSZach Atkins 1151b16049aSZach Atkins Returns: 1161b16049aSZach Atkins Optional[str]: Skip reason, or `None` if test case should not be skipped 1171b16049aSZach Atkins """ 1181b16049aSZach Atkins return None 1191b16049aSZach Atkins 1201b16049aSZach Atkins def check_post_skip(self, test: str, spec: TestSpec, resource: str, stderr: str) -> Optional[str]: 1211b16049aSZach Atkins """Check if a test case should be allowed to fail, based on its stderr output 1221b16049aSZach Atkins 1231b16049aSZach Atkins Args: 1241b16049aSZach Atkins test (str): Name of test 1251b16049aSZach Atkins spec (TestSpec): Test case specification 1261b16049aSZach Atkins resource (str): libCEED backend 1271b16049aSZach Atkins stderr (str): Standard error output from test case execution 1281b16049aSZach Atkins 1291b16049aSZach Atkins Returns: 1301b16049aSZach Atkins Optional[str]: Skip reason, or `None` if unexpeced error 1311b16049aSZach Atkins """ 1321b16049aSZach Atkins return None 1331b16049aSZach Atkins 1341b16049aSZach Atkins def check_required_failure(self, test: str, spec: TestSpec, resource: str, stderr: str) -> tuple[str, bool]: 1351b16049aSZach Atkins """Check whether a test case is expected to fail and if it failed expectedly 1361b16049aSZach Atkins 1371b16049aSZach Atkins Args: 1381b16049aSZach Atkins test (str): Name of test 1391b16049aSZach Atkins spec (TestSpec): Test case specification 1401b16049aSZach Atkins resource (str): libCEED backend 1411b16049aSZach Atkins stderr (str): Standard error output from test case execution 1421b16049aSZach Atkins 1431b16049aSZach Atkins Returns: 1441b16049aSZach Atkins tuple[str, bool]: Tuple of the expected failure string and whether it was present in `stderr` 1451b16049aSZach Atkins """ 1461b16049aSZach Atkins return '', True 1471b16049aSZach Atkins 1481b16049aSZach Atkins def check_allowed_stdout(self, test: str) -> bool: 1491b16049aSZach Atkins """Check whether a test is allowed to print console output 1501b16049aSZach Atkins 1511b16049aSZach Atkins Args: 1521b16049aSZach Atkins test (str): Name of test 1531b16049aSZach Atkins 1541b16049aSZach Atkins Returns: 1551b16049aSZach Atkins bool: True if the test is allowed to print console output 1561b16049aSZach Atkins """ 1571b16049aSZach Atkins return False 1581b16049aSZach Atkins 1591b16049aSZach Atkins 1601b16049aSZach Atkinsdef has_cgnsdiff() -> bool: 1611b16049aSZach Atkins """Check whether `cgnsdiff` is an executable program in the current environment 1621b16049aSZach Atkins 1631b16049aSZach Atkins Returns: 1641b16049aSZach Atkins bool: True if `cgnsdiff` is found 1651b16049aSZach Atkins """ 1661b16049aSZach Atkins my_env: dict = os.environ.copy() 1671b16049aSZach Atkins proc = subprocess.run('cgnsdiff', 1681b16049aSZach Atkins shell=True, 1691b16049aSZach Atkins stdout=subprocess.PIPE, 1701b16049aSZach Atkins stderr=subprocess.PIPE, 1711b16049aSZach Atkins env=my_env) 1721b16049aSZach Atkins return 'not found' not in proc.stderr.decode('utf-8') 1731b16049aSZach Atkins 1741b16049aSZach Atkins 1751b16049aSZach Atkinsdef contains_any(base: str, substrings: list[str]) -> bool: 1761b16049aSZach Atkins """Helper function, checks if any of the substrings are included in the base string 1771b16049aSZach Atkins 1781b16049aSZach Atkins Args: 1791b16049aSZach Atkins base (str): Base string to search in 1801b16049aSZach Atkins substrings (list[str]): List of potential substrings 1811b16049aSZach Atkins 1821b16049aSZach Atkins Returns: 1831b16049aSZach Atkins bool: True if any substrings are included in base string 1841b16049aSZach Atkins """ 1851b16049aSZach Atkins return any((sub in base for sub in substrings)) 1861b16049aSZach Atkins 1871b16049aSZach Atkins 1881b16049aSZach Atkinsdef startswith_any(base: str, prefixes: list[str]) -> bool: 1891b16049aSZach Atkins """Helper function, checks if the base string is prefixed by any of `prefixes` 1901b16049aSZach Atkins 1911b16049aSZach Atkins Args: 1921b16049aSZach Atkins base (str): Base string to search 1931b16049aSZach Atkins prefixes (list[str]): List of potential prefixes 1941b16049aSZach Atkins 1951b16049aSZach Atkins Returns: 1961b16049aSZach Atkins bool: True if base string is prefixed by any of the prefixes 1971b16049aSZach Atkins """ 1981b16049aSZach Atkins return any((base.startswith(prefix) for prefix in prefixes)) 1991b16049aSZach Atkins 2001b16049aSZach Atkins 2011b16049aSZach Atkinsdef parse_test_line(line: str) -> TestSpec: 2021b16049aSZach Atkins """Parse a single line of TESTARGS and CLI arguments into a `TestSpec` object 2031b16049aSZach Atkins 2041b16049aSZach Atkins Args: 2051b16049aSZach Atkins line (str): String containing TESTARGS specification and CLI arguments 2061b16049aSZach Atkins 2071b16049aSZach Atkins Returns: 2081b16049aSZach Atkins TestSpec: Parsed specification of test case 2091b16049aSZach Atkins """ 2101b16049aSZach Atkins args: list[str] = re.findall("(?:\".*?\"|\\S)+", line.strip()) 2111b16049aSZach Atkins if args[0] == 'TESTARGS': 2121b16049aSZach Atkins return TestSpec(name='', args=args[1:]) 2131b16049aSZach Atkins raw_test_args: str = args[0][args[0].index('TESTARGS(') + 9:args[0].rindex(')')] 2141b16049aSZach Atkins # transform 'name="myname",only="serial,int32"' into {'name': 'myname', 'only': 'serial,int32'} 2151b16049aSZach Atkins test_args: dict = dict([''.join(t).split('=') for t in re.findall(r"""([^,=]+)(=)"([^"]*)\"""", raw_test_args)]) 216*f85e4a7bSJeremy L Thompson name: str = test_args.get('name', '') 2171b16049aSZach Atkins constraints: list[str] = test_args['only'].split(',') if 'only' in test_args else [] 2181b16049aSZach Atkins if len(args) > 1: 219*f85e4a7bSJeremy L Thompson return TestSpec(name=name, only=constraints, args=args[1:]) 2201b16049aSZach Atkins else: 221*f85e4a7bSJeremy L Thompson return TestSpec(name=name, only=constraints) 2221b16049aSZach Atkins 2231b16049aSZach Atkins 2241b16049aSZach Atkinsdef get_test_args(source_file: Path) -> list[TestSpec]: 2251b16049aSZach Atkins """Parse all test cases from a given source file 2261b16049aSZach Atkins 2271b16049aSZach Atkins Args: 2281b16049aSZach Atkins source_file (Path): Path to source file 2291b16049aSZach Atkins 2301b16049aSZach Atkins Raises: 2311b16049aSZach Atkins RuntimeError: Errors if source file extension is unsupported 2321b16049aSZach Atkins 2331b16049aSZach Atkins Returns: 2341b16049aSZach Atkins list[TestSpec]: List of parsed `TestSpec` objects, or a list containing a single, default `TestSpec` if none were found 2351b16049aSZach Atkins """ 2361b16049aSZach Atkins comment_str: str = '' 2371b16049aSZach Atkins if source_file.suffix in ['.c', '.cpp']: 2381b16049aSZach Atkins comment_str = '//' 2391b16049aSZach Atkins elif source_file.suffix in ['.py']: 2401b16049aSZach Atkins comment_str = '#' 2411b16049aSZach Atkins elif source_file.suffix in ['.usr']: 2421b16049aSZach Atkins comment_str = 'C_' 2431b16049aSZach Atkins elif source_file.suffix in ['.f90']: 2441b16049aSZach Atkins comment_str = '! ' 2451b16049aSZach Atkins else: 2461b16049aSZach Atkins raise RuntimeError(f'Unrecognized extension for file: {source_file}') 2471b16049aSZach Atkins 2481b16049aSZach Atkins return [parse_test_line(line.strip(comment_str)) 2491b16049aSZach Atkins for line in source_file.read_text().splitlines() 2501b16049aSZach Atkins if line.startswith(f'{comment_str}TESTARGS')] or [TestSpec('', args=['{ceed_resource}'])] 2511b16049aSZach Atkins 2521b16049aSZach Atkins 2531b16049aSZach Atkinsdef diff_csv(test_csv: Path, true_csv: Path, zero_tol: float = 3e-10, rel_tol: float = 1e-2) -> str: 2541b16049aSZach Atkins """Compare CSV results against an expected CSV file with tolerances 2551b16049aSZach Atkins 2561b16049aSZach Atkins Args: 2571b16049aSZach Atkins test_csv (Path): Path to output CSV results 2581b16049aSZach Atkins true_csv (Path): Path to expected CSV results 2591b16049aSZach Atkins zero_tol (float, optional): Tolerance below which values are considered to be zero. Defaults to 3e-10. 2601b16049aSZach Atkins rel_tol (float, optional): Relative tolerance for comparing non-zero values. Defaults to 1e-2. 2611b16049aSZach Atkins 2621b16049aSZach Atkins Returns: 2631b16049aSZach Atkins str: Diff output between result and expected CSVs 2641b16049aSZach Atkins """ 2651b16049aSZach Atkins test_lines: list[str] = test_csv.read_text().splitlines() 2661b16049aSZach Atkins true_lines: list[str] = true_csv.read_text().splitlines() 2671b16049aSZach Atkins 2681b16049aSZach Atkins if test_lines[0] != true_lines[0]: 2691b16049aSZach Atkins return ''.join(difflib.unified_diff([f'{test_lines[0]}\n'], [f'{true_lines[0]}\n'], 2701b16049aSZach Atkins tofile='found CSV columns', fromfile='expected CSV columns')) 2711b16049aSZach Atkins 2721b16049aSZach Atkins diff_lines: list[str] = list() 2731b16049aSZach Atkins column_names: list[str] = true_lines[0].strip().split(',') 2741b16049aSZach Atkins for test_line, true_line in zip(test_lines[1:], true_lines[1:]): 2751b16049aSZach Atkins test_vals: list[float] = [float(val.strip()) for val in test_line.strip().split(',')] 2761b16049aSZach Atkins true_vals: list[float] = [float(val.strip()) for val in true_line.strip().split(',')] 2771b16049aSZach Atkins for test_val, true_val, column_name in zip(test_vals, true_vals, column_names): 2781b16049aSZach Atkins true_zero: bool = abs(true_val) < zero_tol 2791b16049aSZach Atkins test_zero: bool = abs(test_val) < zero_tol 2801b16049aSZach Atkins fail: bool = False 2811b16049aSZach Atkins if true_zero: 2821b16049aSZach Atkins fail = not test_zero 2831b16049aSZach Atkins else: 2841b16049aSZach Atkins fail = not isclose(test_val, true_val, rel_tol=rel_tol) 2851b16049aSZach Atkins if fail: 2861b16049aSZach Atkins diff_lines.append(f'step: {true_line[0]}, column: {column_name}, expected: {true_val}, got: {test_val}') 2871b16049aSZach Atkins return '\n'.join(diff_lines) 2881b16049aSZach Atkins 2891b16049aSZach Atkins 2901b16049aSZach Atkinsdef diff_cgns(test_cgns: Path, true_cgns: Path, tolerance: float = 1e-12) -> str: 2911b16049aSZach Atkins """Compare CGNS results against an expected CGSN file with tolerance 2921b16049aSZach Atkins 2931b16049aSZach Atkins Args: 2941b16049aSZach Atkins test_cgns (Path): Path to output CGNS file 2951b16049aSZach Atkins true_cgns (Path): Path to expected CGNS file 2961b16049aSZach Atkins tolerance (float, optional): Tolerance for comparing floating-point values 2971b16049aSZach Atkins 2981b16049aSZach Atkins Returns: 2991b16049aSZach Atkins str: Diff output between result and expected CGNS files 3001b16049aSZach Atkins """ 3011b16049aSZach Atkins my_env: dict = os.environ.copy() 3021b16049aSZach Atkins 3031b16049aSZach Atkins run_args: list[str] = ['cgnsdiff', '-d', '-t', f'{tolerance}', str(test_cgns), str(true_cgns)] 3041b16049aSZach Atkins proc = subprocess.run(' '.join(run_args), 3051b16049aSZach Atkins shell=True, 3061b16049aSZach Atkins stdout=subprocess.PIPE, 3071b16049aSZach Atkins stderr=subprocess.PIPE, 3081b16049aSZach Atkins env=my_env) 3091b16049aSZach Atkins 3101b16049aSZach Atkins return proc.stderr.decode('utf-8') + proc.stdout.decode('utf-8') 3111b16049aSZach Atkins 3121b16049aSZach Atkins 3131b16049aSZach Atkinsdef run_tests(test: str, ceed_backends: list[str], mode: RunMode, nproc: int, suite_spec: SuiteSpec) -> TestSuite: 3141b16049aSZach Atkins """Run all test cases for `test` with each of the provided `ceed_backends` 3151b16049aSZach Atkins 3161b16049aSZach Atkins Args: 3171b16049aSZach Atkins test (str): Name of test 3181b16049aSZach Atkins ceed_backends (list[str]): List of libCEED backends 3191b16049aSZach Atkins mode (RunMode): Output mode, either `RunMode.TAP` or `RunMode.JUNIT` 3201b16049aSZach Atkins nproc (int): Number of MPI processes to use when running each test case 3211b16049aSZach Atkins suite_spec (SuiteSpec): Object defining required methods for running tests 3221b16049aSZach Atkins 3231b16049aSZach Atkins Returns: 3241b16049aSZach Atkins TestSuite: JUnit `TestSuite` containing results of all test cases 3251b16049aSZach Atkins """ 3261b16049aSZach Atkins source_path: Path = suite_spec.get_source_path(test) 3271b16049aSZach Atkins test_specs: list[TestSpec] = get_test_args(source_path) 3281b16049aSZach Atkins 3291b16049aSZach Atkins if mode is RunMode.TAP: 3301b16049aSZach Atkins print('1..' + str(len(test_specs) * len(ceed_backends))) 3311b16049aSZach Atkins 3321b16049aSZach Atkins test_cases: list[TestCase] = [] 3331b16049aSZach Atkins my_env: dict = os.environ.copy() 3341b16049aSZach Atkins my_env['CEED_ERROR_HANDLER'] = 'exit' 3351b16049aSZach Atkins 3361b16049aSZach Atkins index: int = 1 3371b16049aSZach Atkins for spec in test_specs: 3381b16049aSZach Atkins for ceed_resource in ceed_backends: 3391b16049aSZach Atkins run_args: list = [suite_spec.get_run_path(test), *spec.args] 3401b16049aSZach Atkins 3411b16049aSZach Atkins if '{ceed_resource}' in run_args: 3421b16049aSZach Atkins run_args[run_args.index('{ceed_resource}')] = ceed_resource 3431b16049aSZach Atkins if '{nproc}' in run_args: 3441b16049aSZach Atkins run_args[run_args.index('{nproc}')] = f'{nproc}' 3451b16049aSZach Atkins elif nproc > 1 and source_path.suffix != '.py': 3461b16049aSZach Atkins run_args = ['mpiexec', '-n', f'{nproc}', *run_args] 3471b16049aSZach Atkins 3481b16049aSZach Atkins # run test 3491b16049aSZach Atkins skip_reason: str = suite_spec.check_pre_skip(test, spec, ceed_resource, nproc) 3501b16049aSZach Atkins if skip_reason: 3511b16049aSZach Atkins test_case: TestCase = TestCase(f'{test}, "{spec.name}", n{nproc}, {ceed_resource}', 3521b16049aSZach Atkins elapsed_sec=0, 3531b16049aSZach Atkins timestamp=time.strftime('%Y-%m-%d %H:%M:%S %Z', time.localtime()), 3541b16049aSZach Atkins stdout='', 3551b16049aSZach Atkins stderr='') 3561b16049aSZach Atkins test_case.add_skipped_info(skip_reason) 3571b16049aSZach Atkins else: 3581b16049aSZach Atkins start: float = time.time() 3591b16049aSZach Atkins proc = subprocess.run(' '.join(str(arg) for arg in run_args), 3601b16049aSZach Atkins shell=True, 3611b16049aSZach Atkins stdout=subprocess.PIPE, 3621b16049aSZach Atkins stderr=subprocess.PIPE, 3631b16049aSZach Atkins env=my_env) 3641b16049aSZach Atkins 3651b16049aSZach Atkins test_case = TestCase(f'{test}, "{spec.name}", n{nproc}, {ceed_resource}', 3661b16049aSZach Atkins classname=source_path.parent, 3671b16049aSZach Atkins elapsed_sec=time.time() - start, 3681b16049aSZach Atkins timestamp=time.strftime('%Y-%m-%d %H:%M:%S %Z', time.localtime(start)), 3691b16049aSZach Atkins stdout=proc.stdout.decode('utf-8'), 3701b16049aSZach Atkins stderr=proc.stderr.decode('utf-8'), 3711b16049aSZach Atkins allow_multiple_subelements=True) 3721b16049aSZach Atkins ref_csvs: list[Path] = [] 37397fab443SJeremy L Thompson output_files: list[str] = [arg for arg in spec.args if 'ascii:' in arg] 37497fab443SJeremy L Thompson if output_files: 3751b16049aSZach Atkins ref_csvs = [suite_spec.get_output_path(test, file.split('ascii:')[-1]) for file in output_files] 3761b16049aSZach Atkins ref_cgns: list[Path] = [] 37797fab443SJeremy L Thompson output_files = [arg for arg in spec.args if 'cgns:' in arg] 37897fab443SJeremy L Thompson if output_files: 3791b16049aSZach Atkins ref_cgns = [suite_spec.get_output_path(test, file.split('cgns:')[-1]) for file in output_files] 3801b16049aSZach Atkins ref_stdout: Path = suite_spec.get_output_path(test, test + '.out') 3811b16049aSZach Atkins suite_spec.post_test_hook(test, spec) 3821b16049aSZach Atkins 3831b16049aSZach Atkins # check allowed failures 3841b16049aSZach Atkins if not test_case.is_skipped() and test_case.stderr: 3851b16049aSZach Atkins skip_reason: str = suite_spec.check_post_skip(test, spec, ceed_resource, test_case.stderr) 3861b16049aSZach Atkins if skip_reason: 3871b16049aSZach Atkins test_case.add_skipped_info(skip_reason) 3881b16049aSZach Atkins 3891b16049aSZach Atkins # check required failures 3901b16049aSZach Atkins if not test_case.is_skipped(): 3911b16049aSZach Atkins required_message, did_fail = suite_spec.check_required_failure(test, spec, ceed_resource, test_case.stderr) 3921b16049aSZach Atkins if required_message and did_fail: 3931b16049aSZach Atkins test_case.status = f'fails with required: {required_message}' 3941b16049aSZach Atkins elif required_message: 3951b16049aSZach Atkins test_case.add_failure_info(f'required failure missing: {required_message}') 3961b16049aSZach Atkins 3971b16049aSZach Atkins # classify other results 3981b16049aSZach Atkins if not test_case.is_skipped() and not test_case.status: 3991b16049aSZach Atkins if test_case.stderr: 4001b16049aSZach Atkins test_case.add_failure_info('stderr', test_case.stderr) 4011b16049aSZach Atkins if proc.returncode != 0: 4021b16049aSZach Atkins test_case.add_error_info(f'returncode = {proc.returncode}') 4031b16049aSZach Atkins if ref_stdout.is_file(): 4041b16049aSZach Atkins diff = list(difflib.unified_diff(ref_stdout.read_text().splitlines(keepends=True), 4051b16049aSZach Atkins test_case.stdout.splitlines(keepends=True), 4061b16049aSZach Atkins fromfile=str(ref_stdout), 4071b16049aSZach Atkins tofile='New')) 4081b16049aSZach Atkins if diff: 4091b16049aSZach Atkins test_case.add_failure_info('stdout', output=''.join(diff)) 4101b16049aSZach Atkins elif test_case.stdout and not suite_spec.check_allowed_stdout(test): 4111b16049aSZach Atkins test_case.add_failure_info('stdout', output=test_case.stdout) 4121b16049aSZach Atkins # expected CSV output 4131b16049aSZach Atkins for ref_csv in ref_csvs: 4141b16049aSZach Atkins if not ref_csv.is_file(): 4151b16049aSZach Atkins test_case.add_failure_info('csv', output=f'{ref_csv} not found') 4161b16049aSZach Atkins else: 4171b16049aSZach Atkins diff: str = diff_csv(Path.cwd() / ref_csv.name, ref_csv) 4181b16049aSZach Atkins if diff: 4191b16049aSZach Atkins test_case.add_failure_info('csv', output=diff) 4201b16049aSZach Atkins else: 4211b16049aSZach Atkins (Path.cwd() / ref_csv.name).unlink() 4221b16049aSZach Atkins # expected CGNS output 4231b16049aSZach Atkins for ref_cgn in ref_cgns: 4241b16049aSZach Atkins if not ref_cgn.is_file(): 4251b16049aSZach Atkins test_case.add_failure_info('cgns', output=f'{ref_cgn} not found') 4261b16049aSZach Atkins else: 4271b16049aSZach Atkins diff = diff_cgns(Path.cwd() / ref_cgn.name, ref_cgn) 4281b16049aSZach Atkins if diff: 4291b16049aSZach Atkins test_case.add_failure_info('cgns', output=diff) 4301b16049aSZach Atkins else: 4311b16049aSZach Atkins (Path.cwd() / ref_cgn.name).unlink() 4321b16049aSZach Atkins 4331b16049aSZach Atkins # store result 4341b16049aSZach Atkins test_case.args = ' '.join(str(arg) for arg in run_args) 4351b16049aSZach Atkins test_cases.append(test_case) 4361b16049aSZach Atkins 4371b16049aSZach Atkins if mode is RunMode.TAP: 4381b16049aSZach Atkins # print incremental output if TAP mode 4391b16049aSZach Atkins print(f'# Test: {spec.name}') 4401b16049aSZach Atkins if spec.only: 4411b16049aSZach Atkins print('# Only: {}'.format(','.join(spec.only))) 4421b16049aSZach Atkins print(f'# $ {test_case.args}') 4431b16049aSZach Atkins if test_case.is_skipped(): 4441b16049aSZach Atkins print('ok {} - SKIP: {}'.format(index, (test_case.skipped[0]['message'] or 'NO MESSAGE').strip())) 4451b16049aSZach Atkins elif test_case.is_failure() or test_case.is_error(): 4461b16049aSZach Atkins print(f'not ok {index}') 4471b16049aSZach Atkins if test_case.is_error(): 4481b16049aSZach Atkins print(f' ERROR: {test_case.errors[0]["message"]}') 4491b16049aSZach Atkins if test_case.is_failure(): 4501b16049aSZach Atkins for i, failure in enumerate(test_case.failures): 4511b16049aSZach Atkins print(f' FAILURE {i}: {failure["message"]}') 4521b16049aSZach Atkins print(f' Output: \n{failure["output"]}') 4531b16049aSZach Atkins else: 4541b16049aSZach Atkins print(f'ok {index} - PASS') 4551b16049aSZach Atkins sys.stdout.flush() 4561b16049aSZach Atkins else: 4571b16049aSZach Atkins # print error or failure information if JUNIT mode 4581b16049aSZach Atkins if test_case.is_error() or test_case.is_failure(): 4591b16049aSZach Atkins print(f'Test: {test} {spec.name}') 4601b16049aSZach Atkins print(f' $ {test_case.args}') 4611b16049aSZach Atkins if test_case.is_error(): 4621b16049aSZach Atkins print('ERROR: {}'.format((test_case.errors[0]['message'] or 'NO MESSAGE').strip())) 4631b16049aSZach Atkins print('Output: \n{}'.format((test_case.errors[0]['output'] or 'NO MESSAGE').strip())) 4641b16049aSZach Atkins if test_case.is_failure(): 4651b16049aSZach Atkins for failure in test_case.failures: 4661b16049aSZach Atkins print('FAIL: {}'.format((failure['message'] or 'NO MESSAGE').strip())) 4671b16049aSZach Atkins print('Output: \n{}'.format((failure['output'] or 'NO MESSAGE').strip())) 4681b16049aSZach Atkins sys.stdout.flush() 4691b16049aSZach Atkins index += 1 4701b16049aSZach Atkins 4711b16049aSZach Atkins return TestSuite(test, test_cases) 4721b16049aSZach Atkins 4731b16049aSZach Atkins 4741b16049aSZach Atkinsdef write_junit_xml(test_suite: TestSuite, output_file: Optional[Path], batch: str = '') -> None: 4751b16049aSZach Atkins """Write a JUnit XML file containing the results of a `TestSuite` 4761b16049aSZach Atkins 4771b16049aSZach Atkins Args: 4781b16049aSZach Atkins test_suite (TestSuite): JUnit `TestSuite` to write 4791b16049aSZach Atkins output_file (Optional[Path]): Path to output file, or `None` to generate automatically as `build/{test_suite.name}{batch}.junit` 4801b16049aSZach Atkins batch (str): Name of JUnit batch, defaults to empty string 4811b16049aSZach Atkins """ 4821b16049aSZach Atkins output_file: Path = output_file or Path('build') / (f'{test_suite.name}{batch}.junit') 4831b16049aSZach Atkins output_file.write_text(to_xml_report_string([test_suite])) 4841b16049aSZach Atkins 4851b16049aSZach Atkins 4861b16049aSZach Atkinsdef has_failures(test_suite: TestSuite) -> bool: 4871b16049aSZach Atkins """Check whether any test cases in a `TestSuite` failed 4881b16049aSZach Atkins 4891b16049aSZach Atkins Args: 4901b16049aSZach Atkins test_suite (TestSuite): JUnit `TestSuite` to check 4911b16049aSZach Atkins 4921b16049aSZach Atkins Returns: 4931b16049aSZach Atkins bool: True if any test cases failed 4941b16049aSZach Atkins """ 4951b16049aSZach Atkins return any(c.is_failure() or c.is_error() for c in test_suite.test_cases) 496