11b16049aSZach Atkinsfrom abc import ABC, abstractmethod 2f36e7531SZach Atkinsfrom collections.abc import Iterable 31b16049aSZach Atkinsimport argparse 469ef23b6SZach Atkinsimport csv 5f36e7531SZach Atkinsfrom dataclasses import dataclass, field, fields 61b16049aSZach Atkinsimport difflib 71b16049aSZach Atkinsfrom enum import Enum 81b16049aSZach Atkinsfrom math import isclose 91b16049aSZach Atkinsimport os 101b16049aSZach Atkinsfrom pathlib import Path 111b16049aSZach Atkinsimport re 121b16049aSZach Atkinsimport subprocess 1319868e18SZach Atkinsimport multiprocessing as mp 141b16049aSZach Atkinsimport sys 151b16049aSZach Atkinsimport time 16f36e7531SZach Atkinsfrom typing import Optional, Tuple, List, Dict, Callable, Iterable, get_origin 17f36e7531SZach Atkinsimport shutil 181b16049aSZach Atkins 191b16049aSZach Atkinssys.path.insert(0, str(Path(__file__).parent / "junit-xml")) 201b16049aSZach Atkinsfrom junit_xml import TestCase, TestSuite, to_xml_report_string # nopep8 211b16049aSZach Atkins 221b16049aSZach Atkins 23f36e7531SZach Atkinsclass ParseError(RuntimeError): 24f36e7531SZach Atkins """A custom exception for failed parsing.""" 25f36e7531SZach Atkins 26f36e7531SZach Atkins def __init__(self, message): 27f36e7531SZach Atkins super().__init__(message) 28f36e7531SZach Atkins 29f36e7531SZach Atkins 301b16049aSZach Atkinsclass CaseInsensitiveEnumAction(argparse.Action): 311b16049aSZach Atkins """Action to convert input values to lower case prior to converting to an Enum type""" 321b16049aSZach Atkins 331b16049aSZach Atkins def __init__(self, option_strings, dest, type, default, **kwargs): 34f36e7531SZach Atkins if not issubclass(type, Enum): 35f36e7531SZach Atkins raise ValueError(f"{type} must be an Enum") 361b16049aSZach Atkins # store provided enum type 371b16049aSZach Atkins self.enum_type = type 38f36e7531SZach Atkins if isinstance(default, self.enum_type): 39f36e7531SZach Atkins pass 40f36e7531SZach Atkins elif isinstance(default, str): 411b16049aSZach Atkins default = self.enum_type(default.lower()) 42f36e7531SZach Atkins elif isinstance(default, Iterable): 431b16049aSZach Atkins default = [self.enum_type(v.lower()) for v in default] 44f36e7531SZach Atkins else: 45f36e7531SZach Atkins raise argparse.ArgumentTypeError("Invalid value type, must be str or iterable") 461b16049aSZach Atkins # prevent automatic type conversion 471b16049aSZach Atkins super().__init__(option_strings, dest, default=default, **kwargs) 481b16049aSZach Atkins 491b16049aSZach Atkins def __call__(self, parser, namespace, values, option_string=None): 50f36e7531SZach Atkins if isinstance(values, self.enum_type): 51f36e7531SZach Atkins pass 52f36e7531SZach Atkins elif isinstance(values, str): 531b16049aSZach Atkins values = self.enum_type(values.lower()) 54f36e7531SZach Atkins elif isinstance(values, Iterable): 551b16049aSZach Atkins values = [self.enum_type(v.lower()) for v in values] 56f36e7531SZach Atkins else: 57f36e7531SZach Atkins raise argparse.ArgumentTypeError("Invalid value type, must be str or iterable") 581b16049aSZach Atkins setattr(namespace, self.dest, values) 591b16049aSZach Atkins 601b16049aSZach Atkins 611b16049aSZach Atkins@dataclass 621b16049aSZach Atkinsclass TestSpec: 631b16049aSZach Atkins """Dataclass storing information about a single test case""" 64f36e7531SZach Atkins name: str = field(default_factory=str) 65f36e7531SZach Atkins csv_rtol: float = -1 66f36e7531SZach Atkins csv_ztol: float = -1 67f36e7531SZach Atkins cgns_tol: float = -1 688938a869SZach Atkins only: List = field(default_factory=list) 698938a869SZach Atkins args: List = field(default_factory=list) 70f36e7531SZach Atkins key_values: Dict = field(default_factory=dict) 711b16049aSZach Atkins 721b16049aSZach Atkins 73f36e7531SZach Atkinsclass RunMode(Enum): 741b16049aSZach Atkins """Enumeration of run modes, either `RunMode.TAP` or `RunMode.JUNIT`""" 75f36e7531SZach Atkins TAP = 'tap' 76f36e7531SZach Atkins JUNIT = 'junit' 77f36e7531SZach Atkins 78f36e7531SZach Atkins def __str__(self): 79f36e7531SZach Atkins return self.value 80f36e7531SZach Atkins 81f36e7531SZach Atkins def __repr__(self): 82f36e7531SZach Atkins return self.value 831b16049aSZach Atkins 841b16049aSZach Atkins 851b16049aSZach Atkinsclass SuiteSpec(ABC): 861b16049aSZach Atkins """Abstract Base Class defining the required interface for running a test suite""" 871b16049aSZach Atkins @abstractmethod 881b16049aSZach Atkins def get_source_path(self, test: str) -> Path: 891b16049aSZach Atkins """Compute path to test source file 901b16049aSZach Atkins 911b16049aSZach Atkins Args: 921b16049aSZach Atkins test (str): Name of test 931b16049aSZach Atkins 941b16049aSZach Atkins Returns: 951b16049aSZach Atkins Path: Path to source file 961b16049aSZach Atkins """ 971b16049aSZach Atkins raise NotImplementedError 981b16049aSZach Atkins 991b16049aSZach Atkins @abstractmethod 1001b16049aSZach Atkins def get_run_path(self, test: str) -> Path: 1011b16049aSZach Atkins """Compute path to built test executable file 1021b16049aSZach Atkins 1031b16049aSZach Atkins Args: 1041b16049aSZach Atkins test (str): Name of test 1051b16049aSZach Atkins 1061b16049aSZach Atkins Returns: 1071b16049aSZach Atkins Path: Path to test executable 1081b16049aSZach Atkins """ 1091b16049aSZach Atkins raise NotImplementedError 1101b16049aSZach Atkins 1111b16049aSZach Atkins @abstractmethod 1121b16049aSZach Atkins def get_output_path(self, test: str, output_file: str) -> Path: 1131b16049aSZach Atkins """Compute path to expected output file 1141b16049aSZach Atkins 1151b16049aSZach Atkins Args: 1161b16049aSZach Atkins test (str): Name of test 1171b16049aSZach Atkins output_file (str): File name of output file 1181b16049aSZach Atkins 1191b16049aSZach Atkins Returns: 1201b16049aSZach Atkins Path: Path to expected output file 1211b16049aSZach Atkins """ 1221b16049aSZach Atkins raise NotImplementedError 1231b16049aSZach Atkins 124c0ad81e5SJeremy L Thompson @property 125f36e7531SZach Atkins def test_failure_artifacts_path(self) -> Path: 126f36e7531SZach Atkins """Path to test failure artifacts""" 127f36e7531SZach Atkins return Path('build') / 'test_failure_artifacts' 128f36e7531SZach Atkins 129f36e7531SZach Atkins @property 130c0ad81e5SJeremy L Thompson def cgns_tol(self): 131c0ad81e5SJeremy L Thompson """Absolute tolerance for CGNS diff""" 132c0ad81e5SJeremy L Thompson return getattr(self, '_cgns_tol', 1.0e-12) 13383ebc4c4SJeremy L Thompson 134c0ad81e5SJeremy L Thompson @cgns_tol.setter 135c0ad81e5SJeremy L Thompson def cgns_tol(self, val): 136c0ad81e5SJeremy L Thompson self._cgns_tol = val 13783ebc4c4SJeremy L Thompson 13812235d7fSJames Wright @property 139f36e7531SZach Atkins def csv_ztol(self): 14012235d7fSJames Wright """Keyword arguments to be passed to diff_csv()""" 141f36e7531SZach Atkins return getattr(self, '_csv_ztol', 3e-10) 14212235d7fSJames Wright 143f36e7531SZach Atkins @csv_ztol.setter 144f36e7531SZach Atkins def csv_ztol(self, val): 145f36e7531SZach Atkins self._csv_ztol = val 14612235d7fSJames Wright 147f36e7531SZach Atkins @property 148f36e7531SZach Atkins def csv_rtol(self): 149f36e7531SZach Atkins """Keyword arguments to be passed to diff_csv()""" 150f36e7531SZach Atkins return getattr(self, '_csv_rtol', 1e-6) 151f36e7531SZach Atkins 152f36e7531SZach Atkins @csv_rtol.setter 153f36e7531SZach Atkins def csv_rtol(self, val): 154f36e7531SZach Atkins self._csv_rtol = val 155f36e7531SZach Atkins 1565f1423ffSZach Atkins @property 1575f1423ffSZach Atkins def csv_comment_diff_fn(self): # -> Any | Callable[..., None]: 1585f1423ffSZach Atkins return getattr(self, '_csv_comment_diff_fn', None) 1595f1423ffSZach Atkins 1605f1423ffSZach Atkins @csv_comment_diff_fn.setter 1615f1423ffSZach Atkins def csv_comment_diff_fn(self, test_fn): 1625f1423ffSZach Atkins self._csv_comment_diff_fn = test_fn 1635f1423ffSZach Atkins 1645f1423ffSZach Atkins @property 1655f1423ffSZach Atkins def csv_comment_str(self): 1665f1423ffSZach Atkins return getattr(self, '_csv_comment_str', '#') 1675f1423ffSZach Atkins 1685f1423ffSZach Atkins @csv_comment_str.setter 1695f1423ffSZach Atkins def csv_comment_str(self, comment_str): 1705f1423ffSZach Atkins self._csv_comment_str = comment_str 1715f1423ffSZach Atkins 172f36e7531SZach Atkins def post_test_hook(self, test: str, spec: TestSpec, backend: str) -> None: 1731b16049aSZach Atkins """Function callback ran after each test case 1741b16049aSZach Atkins 1751b16049aSZach Atkins Args: 1761b16049aSZach Atkins test (str): Name of test 1771b16049aSZach Atkins spec (TestSpec): Test case specification 1781b16049aSZach Atkins """ 1791b16049aSZach Atkins pass 1801b16049aSZach Atkins 1811b16049aSZach Atkins def check_pre_skip(self, test: str, spec: TestSpec, resource: str, nproc: int) -> Optional[str]: 1821b16049aSZach Atkins """Check if a test case should be skipped prior to running, returning the reason for skipping 1831b16049aSZach Atkins 1841b16049aSZach Atkins Args: 1851b16049aSZach Atkins test (str): Name of test 1861b16049aSZach Atkins spec (TestSpec): Test case specification 1871b16049aSZach Atkins resource (str): libCEED backend 1881b16049aSZach Atkins nproc (int): Number of MPI processes to use when running test case 1891b16049aSZach Atkins 1901b16049aSZach Atkins Returns: 1911b16049aSZach Atkins Optional[str]: Skip reason, or `None` if test case should not be skipped 1921b16049aSZach Atkins """ 1931b16049aSZach Atkins return None 1941b16049aSZach Atkins 1951b16049aSZach Atkins def check_post_skip(self, test: str, spec: TestSpec, resource: str, stderr: str) -> Optional[str]: 1961b16049aSZach Atkins """Check if a test case should be allowed to fail, based on its stderr output 1971b16049aSZach Atkins 1981b16049aSZach Atkins Args: 1991b16049aSZach Atkins test (str): Name of test 2001b16049aSZach Atkins spec (TestSpec): Test case specification 2011b16049aSZach Atkins resource (str): libCEED backend 2021b16049aSZach Atkins stderr (str): Standard error output from test case execution 2031b16049aSZach Atkins 2041b16049aSZach Atkins Returns: 20519868e18SZach Atkins Optional[str]: Skip reason, or `None` if unexpected error 2061b16049aSZach Atkins """ 2071b16049aSZach Atkins return None 2081b16049aSZach Atkins 20978cb100bSJames Wright def check_required_failure(self, test: str, spec: TestSpec, resource: str, stderr: str) -> Tuple[str, bool]: 2101b16049aSZach Atkins """Check whether a test case is expected to fail and if it failed expectedly 2111b16049aSZach Atkins 2121b16049aSZach Atkins Args: 2131b16049aSZach Atkins test (str): Name of test 2141b16049aSZach Atkins spec (TestSpec): Test case specification 2151b16049aSZach Atkins resource (str): libCEED backend 2161b16049aSZach Atkins stderr (str): Standard error output from test case execution 2171b16049aSZach Atkins 2181b16049aSZach Atkins Returns: 2191b16049aSZach Atkins tuple[str, bool]: Tuple of the expected failure string and whether it was present in `stderr` 2201b16049aSZach Atkins """ 2211b16049aSZach Atkins return '', True 2221b16049aSZach Atkins 2231b16049aSZach Atkins def check_allowed_stdout(self, test: str) -> bool: 2241b16049aSZach Atkins """Check whether a test is allowed to print console output 2251b16049aSZach Atkins 2261b16049aSZach Atkins Args: 2271b16049aSZach Atkins test (str): Name of test 2281b16049aSZach Atkins 2291b16049aSZach Atkins Returns: 2301b16049aSZach Atkins bool: True if the test is allowed to print console output 2311b16049aSZach Atkins """ 2321b16049aSZach Atkins return False 2331b16049aSZach Atkins 2341b16049aSZach Atkins 2351b16049aSZach Atkinsdef has_cgnsdiff() -> bool: 2361b16049aSZach Atkins """Check whether `cgnsdiff` is an executable program in the current environment 2371b16049aSZach Atkins 2381b16049aSZach Atkins Returns: 2391b16049aSZach Atkins bool: True if `cgnsdiff` is found 2401b16049aSZach Atkins """ 2411b16049aSZach Atkins my_env: dict = os.environ.copy() 2421b16049aSZach Atkins proc = subprocess.run('cgnsdiff', 2431b16049aSZach Atkins shell=True, 2441b16049aSZach Atkins stdout=subprocess.PIPE, 2451b16049aSZach Atkins stderr=subprocess.PIPE, 2461b16049aSZach Atkins env=my_env) 2471b16049aSZach Atkins return 'not found' not in proc.stderr.decode('utf-8') 2481b16049aSZach Atkins 2491b16049aSZach Atkins 25078cb100bSJames Wrightdef contains_any(base: str, substrings: List[str]) -> bool: 2511b16049aSZach Atkins """Helper function, checks if any of the substrings are included in the base string 2521b16049aSZach Atkins 2531b16049aSZach Atkins Args: 2541b16049aSZach Atkins base (str): Base string to search in 2558938a869SZach Atkins substrings (List[str]): List of potential substrings 2561b16049aSZach Atkins 2571b16049aSZach Atkins Returns: 2581b16049aSZach Atkins bool: True if any substrings are included in base string 2591b16049aSZach Atkins """ 2601b16049aSZach Atkins return any((sub in base for sub in substrings)) 2611b16049aSZach Atkins 2621b16049aSZach Atkins 26378cb100bSJames Wrightdef startswith_any(base: str, prefixes: List[str]) -> bool: 2641b16049aSZach Atkins """Helper function, checks if the base string is prefixed by any of `prefixes` 2651b16049aSZach Atkins 2661b16049aSZach Atkins Args: 2671b16049aSZach Atkins base (str): Base string to search 2688938a869SZach Atkins prefixes (List[str]): List of potential prefixes 2691b16049aSZach Atkins 2701b16049aSZach Atkins Returns: 2711b16049aSZach Atkins bool: True if base string is prefixed by any of the prefixes 2721b16049aSZach Atkins """ 2731b16049aSZach Atkins return any((base.startswith(prefix) for prefix in prefixes)) 2741b16049aSZach Atkins 2751b16049aSZach Atkins 276f36e7531SZach Atkinsdef find_matching(line: str, open: str = '(', close: str = ')') -> Tuple[int, int]: 277f36e7531SZach Atkins """Find the start and end positions of the first outer paired delimeters 278f36e7531SZach Atkins 279f36e7531SZach Atkins Args: 280f36e7531SZach Atkins line (str): Line to search 281f36e7531SZach Atkins open (str, optional): Opening delimiter, must be different than `close`. Defaults to '('. 282f36e7531SZach Atkins close (str, optional): Closing delimeter, must be different than `open`. Defaults to ')'. 283f36e7531SZach Atkins 284f36e7531SZach Atkins Raises: 285f36e7531SZach Atkins RuntimeError: If open or close is not a single character 286f36e7531SZach Atkins RuntimeError: If open and close are the same characters 287f36e7531SZach Atkins 288f36e7531SZach Atkins Returns: 289f36e7531SZach Atkins Tuple[int]: If matching delimeters are found, return indices in `list`. Otherwise, return end < start. 290f36e7531SZach Atkins """ 291f36e7531SZach Atkins if len(open) != 1 or len(close) != 1: 292f36e7531SZach Atkins raise RuntimeError("`open` and `close` must be single characters") 293f36e7531SZach Atkins if open == close: 294f36e7531SZach Atkins raise RuntimeError("`open` and `close` must be different characters") 295f36e7531SZach Atkins start: int = line.find(open) 296f36e7531SZach Atkins if start < 0: 297f36e7531SZach Atkins return -1, -1 298f36e7531SZach Atkins count: int = 1 299f36e7531SZach Atkins for i in range(start + 1, len(line)): 300f36e7531SZach Atkins if line[i] == open: 301f36e7531SZach Atkins count += 1 302f36e7531SZach Atkins if line[i] == close: 303f36e7531SZach Atkins count -= 1 304f36e7531SZach Atkins if count == 0: 305f36e7531SZach Atkins return start, i 306f36e7531SZach Atkins return start, -1 307f36e7531SZach Atkins 308f36e7531SZach Atkins 3097b1ec880SZach Atkinsdef parse_test_line(line: str, fallback_name: str = '') -> TestSpec: 3101b16049aSZach Atkins """Parse a single line of TESTARGS and CLI arguments into a `TestSpec` object 3111b16049aSZach Atkins 3121b16049aSZach Atkins Args: 3131b16049aSZach Atkins line (str): String containing TESTARGS specification and CLI arguments 3141b16049aSZach Atkins 3151b16049aSZach Atkins Returns: 3161b16049aSZach Atkins TestSpec: Parsed specification of test case 3171b16049aSZach Atkins """ 318f36e7531SZach Atkins test_fields = fields(TestSpec) 319f36e7531SZach Atkins field_names = [f.name for f in test_fields] 320f36e7531SZach Atkins known: Dict = dict() 321f36e7531SZach Atkins other: Dict = dict() 322f36e7531SZach Atkins if line[0] == "(": 323f36e7531SZach Atkins # have key/value pairs to parse 324f36e7531SZach Atkins start, end = find_matching(line) 325f36e7531SZach Atkins if end < start: 326f36e7531SZach Atkins raise ParseError(f"Mismatched parentheses in TESTCASE: {line}") 327f36e7531SZach Atkins 328f36e7531SZach Atkins keyvalues_str = line[start:end + 1] 329f36e7531SZach Atkins keyvalues_pattern = re.compile(r''' 330f36e7531SZach Atkins (?:\(\s*|\s*,\s*) # start with open parentheses or comma, no capture 331f36e7531SZach Atkins ([A-Za-z]+[\w\-]+) # match key starting with alpha, containing alphanumeric, _, or -; captured as Group 1 332f36e7531SZach Atkins \s*=\s* # key is followed by = (whitespace ignored) 333f36e7531SZach Atkins (?: # uncaptured group for OR 334f36e7531SZach Atkins "((?:[^"]|\\")+)" # match quoted value (any internal " must be escaped as \"); captured as Group 2 335f36e7531SZach Atkins | ([^=]+) # OR match unquoted value (no equals signs allowed); captured as Group 3 336f36e7531SZach Atkins ) # end uncaptured group for OR 337f36e7531SZach Atkins \s*(?=,|\)) # lookahead for either next comma or closing parentheses 338f36e7531SZach Atkins ''', re.VERBOSE) 339f36e7531SZach Atkins 340f36e7531SZach Atkins for match in re.finditer(keyvalues_pattern, keyvalues_str): 341f36e7531SZach Atkins if not match: # empty 342f36e7531SZach Atkins continue 343f36e7531SZach Atkins key = match.group(1) 344f36e7531SZach Atkins value = match.group(2) if match.group(2) else match.group(3) 345f36e7531SZach Atkins try: 346f36e7531SZach Atkins index = field_names.index(key) 347f36e7531SZach Atkins if key == "only": # weird bc only is a list 348f36e7531SZach Atkins value = [constraint.strip() for constraint in value.split(',')] 349f36e7531SZach Atkins try: 350f36e7531SZach Atkins # TODO: stop supporting python <=3.8 351f36e7531SZach Atkins known[key] = test_fields[index].type(value) # type: ignore 352f36e7531SZach Atkins except TypeError: 353f36e7531SZach Atkins # TODO: this is still liable to fail for complex types 354f36e7531SZach Atkins known[key] = get_origin(test_fields[index].type)(value) # type: ignore 355f36e7531SZach Atkins except ValueError: 356f36e7531SZach Atkins other[key] = value 357f36e7531SZach Atkins 358f36e7531SZach Atkins line = line[end + 1:] 359f36e7531SZach Atkins 3607b1ec880SZach Atkins if not 'name' in known.keys(): 3617b1ec880SZach Atkins known['name'] = fallback_name 3627b1ec880SZach Atkins 363f36e7531SZach Atkins args_pattern = re.compile(r''' 364f36e7531SZach Atkins \s+( # remove leading space 365f36e7531SZach Atkins (?:"[^"]+") # match quoted CLI option 366f36e7531SZach Atkins | (?:[\S]+) # match anything else that is space separated 367f36e7531SZach Atkins ) 368f36e7531SZach Atkins ''', re.VERBOSE) 369f36e7531SZach Atkins args: List[str] = re.findall(args_pattern, line) 370f36e7531SZach Atkins for k, v in other.items(): 371f36e7531SZach Atkins print(f"warning, unknown TESTCASE option for test '{known['name']}': {k}={v}") 372f36e7531SZach Atkins return TestSpec(**known, key_values=other, args=args) 3731b16049aSZach Atkins 3741b16049aSZach Atkins 37578cb100bSJames Wrightdef get_test_args(source_file: Path) -> List[TestSpec]: 3761b16049aSZach Atkins """Parse all test cases from a given source file 3771b16049aSZach Atkins 3781b16049aSZach Atkins Args: 3791b16049aSZach Atkins source_file (Path): Path to source file 3801b16049aSZach Atkins 3811b16049aSZach Atkins Raises: 3821b16049aSZach Atkins RuntimeError: Errors if source file extension is unsupported 3831b16049aSZach Atkins 3841b16049aSZach Atkins Returns: 3858938a869SZach Atkins List[TestSpec]: List of parsed `TestSpec` objects, or a list containing a single, default `TestSpec` if none were found 3861b16049aSZach Atkins """ 3871b16049aSZach Atkins comment_str: str = '' 3888c81f8b0SPeter Munch if source_file.suffix in ['.c', '.cc', '.cpp']: 3891b16049aSZach Atkins comment_str = '//' 3901b16049aSZach Atkins elif source_file.suffix in ['.py']: 3911b16049aSZach Atkins comment_str = '#' 3921b16049aSZach Atkins elif source_file.suffix in ['.usr']: 3931b16049aSZach Atkins comment_str = 'C_' 3941b16049aSZach Atkins elif source_file.suffix in ['.f90']: 3951b16049aSZach Atkins comment_str = '! ' 3961b16049aSZach Atkins else: 3971b16049aSZach Atkins raise RuntimeError(f'Unrecognized extension for file: {source_file}') 3981b16049aSZach Atkins 3997b1ec880SZach Atkins return [parse_test_line(line.strip(comment_str).removeprefix("TESTARGS"), source_file.stem) 4001b16049aSZach Atkins for line in source_file.read_text().splitlines() 4017b1ec880SZach Atkins if line.startswith(f'{comment_str}TESTARGS')] or [TestSpec(source_file.stem, args=['{ceed_resource}'])] 4021b16049aSZach Atkins 4031b16049aSZach Atkins 404f36e7531SZach Atkinsdef diff_csv(test_csv: Path, true_csv: Path, zero_tol: float, rel_tol: float, 40512235d7fSJames Wright comment_str: str = '#', comment_func: Optional[Callable[[str, str], Optional[str]]] = None) -> str: 4061b16049aSZach Atkins """Compare CSV results against an expected CSV file with tolerances 4071b16049aSZach Atkins 4081b16049aSZach Atkins Args: 4091b16049aSZach Atkins test_csv (Path): Path to output CSV results 4101b16049aSZach Atkins true_csv (Path): Path to expected CSV results 411f36e7531SZach Atkins zero_tol (float): Tolerance below which values are considered to be zero. 412f36e7531SZach Atkins rel_tol (float): Relative tolerance for comparing non-zero values. 41312235d7fSJames Wright comment_str (str, optional): String to denoting commented line 41412235d7fSJames Wright comment_func (Callable, optional): Function to determine if test and true line are different 4151b16049aSZach Atkins 4161b16049aSZach Atkins Returns: 4171b16049aSZach Atkins str: Diff output between result and expected CSVs 4181b16049aSZach Atkins """ 41978cb100bSJames Wright test_lines: List[str] = test_csv.read_text().splitlines() 42078cb100bSJames Wright true_lines: List[str] = true_csv.read_text().splitlines() 42169ef23b6SZach Atkins # Files should not be empty 42269ef23b6SZach Atkins if len(test_lines) == 0: 42369ef23b6SZach Atkins return f'No lines found in test output {test_csv}' 42469ef23b6SZach Atkins if len(true_lines) == 0: 42569ef23b6SZach Atkins return f'No lines found in test source {true_csv}' 42612235d7fSJames Wright if len(test_lines) != len(true_lines): 42712235d7fSJames Wright return f'Number of lines in {test_csv} and {true_csv} do not match' 42812235d7fSJames Wright 42912235d7fSJames Wright # Process commented lines 43012235d7fSJames Wright uncommented_lines: List[int] = [] 43112235d7fSJames Wright for n, (test_line, true_line) in enumerate(zip(test_lines, true_lines)): 43212235d7fSJames Wright if test_line[0] == comment_str and true_line[0] == comment_str: 43312235d7fSJames Wright if comment_func: 43412235d7fSJames Wright output = comment_func(test_line, true_line) 43512235d7fSJames Wright if output: 43612235d7fSJames Wright return output 43712235d7fSJames Wright elif test_line[0] == comment_str and true_line[0] != comment_str: 43812235d7fSJames Wright return f'Commented line found in {test_csv} at line {n} but not in {true_csv}' 43912235d7fSJames Wright elif test_line[0] != comment_str and true_line[0] == comment_str: 44012235d7fSJames Wright return f'Commented line found in {true_csv} at line {n} but not in {test_csv}' 44112235d7fSJames Wright else: 44212235d7fSJames Wright uncommented_lines.append(n) 44312235d7fSJames Wright 44412235d7fSJames Wright # Remove commented lines 44512235d7fSJames Wright test_lines = [test_lines[line] for line in uncommented_lines] 44612235d7fSJames Wright true_lines = [true_lines[line] for line in uncommented_lines] 4471b16049aSZach Atkins 44869ef23b6SZach Atkins test_reader: csv.DictReader = csv.DictReader(test_lines) 44969ef23b6SZach Atkins true_reader: csv.DictReader = csv.DictReader(true_lines) 450f36e7531SZach Atkins if not test_reader.fieldnames: 451f36e7531SZach Atkins return f'No CSV columns found in test output {test_csv}' 452f36e7531SZach Atkins if not true_reader.fieldnames: 453f36e7531SZach Atkins return f'No CSV columns found in test source {true_csv}' 45469ef23b6SZach Atkins if test_reader.fieldnames != true_reader.fieldnames: 4551b16049aSZach Atkins return ''.join(difflib.unified_diff([f'{test_lines[0]}\n'], [f'{true_lines[0]}\n'], 4561b16049aSZach Atkins tofile='found CSV columns', fromfile='expected CSV columns')) 4571b16049aSZach Atkins 45878cb100bSJames Wright diff_lines: List[str] = list() 45969ef23b6SZach Atkins for test_line, true_line in zip(test_reader, true_reader): 46069ef23b6SZach Atkins for key in test_reader.fieldnames: 46169ef23b6SZach Atkins # Check if the value is numeric 46269ef23b6SZach Atkins try: 46369ef23b6SZach Atkins true_val: float = float(true_line[key]) 46469ef23b6SZach Atkins test_val: float = float(test_line[key]) 4651b16049aSZach Atkins true_zero: bool = abs(true_val) < zero_tol 4661b16049aSZach Atkins test_zero: bool = abs(test_val) < zero_tol 4671b16049aSZach Atkins fail: bool = False 4681b16049aSZach Atkins if true_zero: 4691b16049aSZach Atkins fail = not test_zero 4701b16049aSZach Atkins else: 4711b16049aSZach Atkins fail = not isclose(test_val, true_val, rel_tol=rel_tol) 4721b16049aSZach Atkins if fail: 47369ef23b6SZach Atkins diff_lines.append(f'column: {key}, expected: {true_val}, got: {test_val}') 47469ef23b6SZach Atkins except ValueError: 47569ef23b6SZach Atkins if test_line[key] != true_line[key]: 47669ef23b6SZach Atkins diff_lines.append(f'column: {key}, expected: {true_line[key]}, got: {test_line[key]}') 47769ef23b6SZach Atkins 4781b16049aSZach Atkins return '\n'.join(diff_lines) 4791b16049aSZach Atkins 4801b16049aSZach Atkins 481f36e7531SZach Atkinsdef diff_cgns(test_cgns: Path, true_cgns: Path, cgns_tol: float) -> str: 4821b16049aSZach Atkins """Compare CGNS results against an expected CGSN file with tolerance 4831b16049aSZach Atkins 4841b16049aSZach Atkins Args: 4851b16049aSZach Atkins test_cgns (Path): Path to output CGNS file 4861b16049aSZach Atkins true_cgns (Path): Path to expected CGNS file 487f36e7531SZach Atkins cgns_tol (float): Tolerance for comparing floating-point values 4881b16049aSZach Atkins 4891b16049aSZach Atkins Returns: 4901b16049aSZach Atkins str: Diff output between result and expected CGNS files 4911b16049aSZach Atkins """ 4921b16049aSZach Atkins my_env: dict = os.environ.copy() 4931b16049aSZach Atkins 49483ebc4c4SJeremy L Thompson run_args: List[str] = ['cgnsdiff', '-d', '-t', f'{cgns_tol}', str(test_cgns), str(true_cgns)] 4951b16049aSZach Atkins proc = subprocess.run(' '.join(run_args), 4961b16049aSZach Atkins shell=True, 4971b16049aSZach Atkins stdout=subprocess.PIPE, 4981b16049aSZach Atkins stderr=subprocess.PIPE, 4991b16049aSZach Atkins env=my_env) 5001b16049aSZach Atkins 5011b16049aSZach Atkins return proc.stderr.decode('utf-8') + proc.stdout.decode('utf-8') 5021b16049aSZach Atkins 5031b16049aSZach Atkins 504f36e7531SZach Atkinsdef diff_ascii(test_file: Path, true_file: Path, backend: str) -> str: 505f36e7531SZach Atkins """Compare ASCII results against an expected ASCII file 506f36e7531SZach Atkins 507f36e7531SZach Atkins Args: 508f36e7531SZach Atkins test_file (Path): Path to output ASCII file 509f36e7531SZach Atkins true_file (Path): Path to expected ASCII file 510f36e7531SZach Atkins 511f36e7531SZach Atkins Returns: 512f36e7531SZach Atkins str: Diff output between result and expected ASCII files 513f36e7531SZach Atkins """ 514f36e7531SZach Atkins tmp_backend: str = backend.replace('/', '-') 515f36e7531SZach Atkins true_str: str = true_file.read_text().replace('{ceed_resource}', tmp_backend) 516f36e7531SZach Atkins diff = list(difflib.unified_diff(test_file.read_text().splitlines(keepends=True), 517f36e7531SZach Atkins true_str.splitlines(keepends=True), 518f36e7531SZach Atkins fromfile=str(test_file), 519f36e7531SZach Atkins tofile=str(true_file))) 520f36e7531SZach Atkins return ''.join(diff) 521f36e7531SZach Atkins 522f36e7531SZach Atkins 523e17e35bbSJames Wrightdef test_case_output_string(test_case: TestCase, spec: TestSpec, mode: RunMode, 524f36e7531SZach Atkins backend: str, test: str, index: int, verbose: bool) -> str: 525e17e35bbSJames Wright output_str = '' 526e17e35bbSJames Wright if mode is RunMode.TAP: 527e17e35bbSJames Wright # print incremental output if TAP mode 528e17e35bbSJames Wright if test_case.is_skipped(): 529e17e35bbSJames Wright output_str += f' ok {index} - {spec.name}, {backend} # SKIP {test_case.skipped[0]["message"]}\n' 530e17e35bbSJames Wright elif test_case.is_failure() or test_case.is_error(): 531f36e7531SZach Atkins output_str += f' not ok {index} - {spec.name}, {backend} ({test_case.elapsed_sec} s)\n' 532e17e35bbSJames Wright else: 533f36e7531SZach Atkins output_str += f' ok {index} - {spec.name}, {backend} ({test_case.elapsed_sec} s)\n' 534f36e7531SZach Atkins if test_case.is_failure() or test_case.is_error() or verbose: 535e17e35bbSJames Wright output_str += f' ---\n' 536e17e35bbSJames Wright if spec.only: 537e17e35bbSJames Wright output_str += f' only: {",".join(spec.only)}\n' 538e17e35bbSJames Wright output_str += f' args: {test_case.args}\n' 539f36e7531SZach Atkins if spec.csv_ztol > 0: 540f36e7531SZach Atkins output_str += f' csv_ztol: {spec.csv_ztol}\n' 541f36e7531SZach Atkins if spec.csv_rtol > 0: 542f36e7531SZach Atkins output_str += f' csv_rtol: {spec.csv_rtol}\n' 543f36e7531SZach Atkins if spec.cgns_tol > 0: 544f36e7531SZach Atkins output_str += f' cgns_tol: {spec.cgns_tol}\n' 545f36e7531SZach Atkins for k, v in spec.key_values.items(): 546f36e7531SZach Atkins output_str += f' {k}: {v}\n' 547e17e35bbSJames Wright if test_case.is_error(): 548e17e35bbSJames Wright output_str += f' error: {test_case.errors[0]["message"]}\n' 549e17e35bbSJames Wright if test_case.is_failure(): 550f36e7531SZach Atkins output_str += f' failures:\n' 551e17e35bbSJames Wright for i, failure in enumerate(test_case.failures): 552f36e7531SZach Atkins output_str += f' -\n' 553e17e35bbSJames Wright output_str += f' message: {failure["message"]}\n' 554e17e35bbSJames Wright if failure["output"]: 555e17e35bbSJames Wright out = failure["output"].strip().replace('\n', '\n ') 556e17e35bbSJames Wright output_str += f' output: |\n {out}\n' 557e17e35bbSJames Wright output_str += f' ...\n' 558e17e35bbSJames Wright else: 559e17e35bbSJames Wright # print error or failure information if JUNIT mode 560e17e35bbSJames Wright if test_case.is_error() or test_case.is_failure(): 561e17e35bbSJames Wright output_str += f'Test: {test} {spec.name}\n' 562e17e35bbSJames Wright output_str += f' $ {test_case.args}\n' 563e17e35bbSJames Wright if test_case.is_error(): 564e17e35bbSJames Wright output_str += 'ERROR: {}\n'.format((test_case.errors[0]['message'] or 'NO MESSAGE').strip()) 565e17e35bbSJames Wright output_str += 'Output: \n{}\n'.format((test_case.errors[0]['output'] or 'NO MESSAGE').strip()) 566e17e35bbSJames Wright if test_case.is_failure(): 567e17e35bbSJames Wright for failure in test_case.failures: 568e17e35bbSJames Wright output_str += 'FAIL: {}\n'.format((failure['message'] or 'NO MESSAGE').strip()) 569e17e35bbSJames Wright output_str += 'Output: \n{}\n'.format((failure['output'] or 'NO MESSAGE').strip()) 570e17e35bbSJames Wright return output_str 571e17e35bbSJames Wright 572e17e35bbSJames Wright 573f36e7531SZach Atkinsdef save_failure_artifact(suite_spec: SuiteSpec, file: Path) -> Path: 574f36e7531SZach Atkins """Attach a file to a test case 575f36e7531SZach Atkins 576f36e7531SZach Atkins Args: 577f36e7531SZach Atkins test_case (TestCase): Test case to attach the file to 578f36e7531SZach Atkins file (Path): Path to the file to attach 579f36e7531SZach Atkins """ 580f36e7531SZach Atkins save_path: Path = suite_spec.test_failure_artifacts_path / file.name 581f36e7531SZach Atkins shutil.copyfile(file, save_path) 582f36e7531SZach Atkins return save_path 583f36e7531SZach Atkins 584f36e7531SZach Atkins 58519868e18SZach Atkinsdef run_test(index: int, test: str, spec: TestSpec, backend: str, 586f36e7531SZach Atkins mode: RunMode, nproc: int, suite_spec: SuiteSpec, verbose: bool = False) -> TestCase: 58719868e18SZach Atkins """Run a single test case and backend combination 5881b16049aSZach Atkins 5891b16049aSZach Atkins Args: 5908938a869SZach Atkins index (int): Index of backend for current spec 59119868e18SZach Atkins test (str): Path to test 59219868e18SZach Atkins spec (TestSpec): Specification of test case 59319868e18SZach Atkins backend (str): CEED backend 59419868e18SZach Atkins mode (RunMode): Output mode 59519868e18SZach Atkins nproc (int): Number of MPI processes to use when running test case 59619868e18SZach Atkins suite_spec (SuiteSpec): Specification of test suite 597f36e7531SZach Atkins verbose (bool, optional): Print detailed output for all runs, not just failures. Defaults to False. 5981b16049aSZach Atkins 5991b16049aSZach Atkins Returns: 60019868e18SZach Atkins TestCase: Test case result 6011b16049aSZach Atkins """ 6021b16049aSZach Atkins source_path: Path = suite_spec.get_source_path(test) 6038938a869SZach Atkins run_args: List = [f'{suite_spec.get_run_path(test)}', *map(str, spec.args)] 6041b16049aSZach Atkins 6051b16049aSZach Atkins if '{ceed_resource}' in run_args: 60619868e18SZach Atkins run_args[run_args.index('{ceed_resource}')] = backend 6078938a869SZach Atkins for i, arg in enumerate(run_args): 6088938a869SZach Atkins if '{ceed_resource}' in arg: 6098938a869SZach Atkins run_args[i] = arg.replace('{ceed_resource}', backend.replace('/', '-')) 6101b16049aSZach Atkins if '{nproc}' in run_args: 6111b16049aSZach Atkins run_args[run_args.index('{nproc}')] = f'{nproc}' 6121b16049aSZach Atkins elif nproc > 1 and source_path.suffix != '.py': 6131b16049aSZach Atkins run_args = ['mpiexec', '-n', f'{nproc}', *run_args] 6141b16049aSZach Atkins 6151b16049aSZach Atkins # run test 616f36e7531SZach Atkins skip_reason: Optional[str] = suite_spec.check_pre_skip(test, spec, backend, nproc) 6171b16049aSZach Atkins if skip_reason: 61819868e18SZach Atkins test_case: TestCase = TestCase(f'{test}, "{spec.name}", n{nproc}, {backend}', 6191b16049aSZach Atkins elapsed_sec=0, 6201b16049aSZach Atkins timestamp=time.strftime('%Y-%m-%d %H:%M:%S %Z', time.localtime()), 6211b16049aSZach Atkins stdout='', 6228938a869SZach Atkins stderr='', 6238938a869SZach Atkins category=spec.name,) 6241b16049aSZach Atkins test_case.add_skipped_info(skip_reason) 6251b16049aSZach Atkins else: 6261b16049aSZach Atkins start: float = time.time() 6271b16049aSZach Atkins proc = subprocess.run(' '.join(str(arg) for arg in run_args), 6281b16049aSZach Atkins shell=True, 6291b16049aSZach Atkins stdout=subprocess.PIPE, 6301b16049aSZach Atkins stderr=subprocess.PIPE, 6311b16049aSZach Atkins env=my_env) 6321b16049aSZach Atkins 63319868e18SZach Atkins test_case = TestCase(f'{test}, "{spec.name}", n{nproc}, {backend}', 6341b16049aSZach Atkins classname=source_path.parent, 6351b16049aSZach Atkins elapsed_sec=time.time() - start, 6361b16049aSZach Atkins timestamp=time.strftime('%Y-%m-%d %H:%M:%S %Z', time.localtime(start)), 6371b16049aSZach Atkins stdout=proc.stdout.decode('utf-8'), 6381b16049aSZach Atkins stderr=proc.stderr.decode('utf-8'), 6398938a869SZach Atkins allow_multiple_subelements=True, 6408938a869SZach Atkins category=spec.name,) 64178cb100bSJames Wright ref_csvs: List[Path] = [] 642f36e7531SZach Atkins ref_ascii: List[Path] = [] 6435f1423ffSZach Atkins output_files: List[str] = [arg.split(':')[1] for arg in run_args if arg.startswith('ascii:')] 64497fab443SJeremy L Thompson if output_files: 6455f1423ffSZach Atkins ref_csvs = [suite_spec.get_output_path(test, file) 646f36e7531SZach Atkins for file in output_files if file.endswith('.csv')] 6475f1423ffSZach Atkins ref_ascii = [suite_spec.get_output_path(test, file) 648f36e7531SZach Atkins for file in output_files if not file.endswith('.csv')] 64978cb100bSJames Wright ref_cgns: List[Path] = [] 6505f1423ffSZach Atkins output_files = [arg.split(':')[1] for arg in run_args if arg.startswith('cgns:')] 65197fab443SJeremy L Thompson if output_files: 6525f1423ffSZach Atkins ref_cgns = [suite_spec.get_output_path(test, file) for file in output_files] 6531b16049aSZach Atkins ref_stdout: Path = suite_spec.get_output_path(test, test + '.out') 654f36e7531SZach Atkins suite_spec.post_test_hook(test, spec, backend) 6551b16049aSZach Atkins 6561b16049aSZach Atkins # check allowed failures 6571b16049aSZach Atkins if not test_case.is_skipped() and test_case.stderr: 658f36e7531SZach Atkins skip_reason: Optional[str] = suite_spec.check_post_skip(test, spec, backend, test_case.stderr) 6591b16049aSZach Atkins if skip_reason: 6601b16049aSZach Atkins test_case.add_skipped_info(skip_reason) 6611b16049aSZach Atkins 6621b16049aSZach Atkins # check required failures 6631b16049aSZach Atkins if not test_case.is_skipped(): 6642fee3251SSebastian Grimberg required_message, did_fail = suite_spec.check_required_failure( 66519868e18SZach Atkins test, spec, backend, test_case.stderr) 6661b16049aSZach Atkins if required_message and did_fail: 6671b16049aSZach Atkins test_case.status = f'fails with required: {required_message}' 6681b16049aSZach Atkins elif required_message: 6691b16049aSZach Atkins test_case.add_failure_info(f'required failure missing: {required_message}') 6701b16049aSZach Atkins 6711b16049aSZach Atkins # classify other results 6721b16049aSZach Atkins if not test_case.is_skipped() and not test_case.status: 6731b16049aSZach Atkins if test_case.stderr: 6741b16049aSZach Atkins test_case.add_failure_info('stderr', test_case.stderr) 6751b16049aSZach Atkins if proc.returncode != 0: 6761b16049aSZach Atkins test_case.add_error_info(f'returncode = {proc.returncode}') 6771b16049aSZach Atkins if ref_stdout.is_file(): 6781b16049aSZach Atkins diff = list(difflib.unified_diff(ref_stdout.read_text().splitlines(keepends=True), 6791b16049aSZach Atkins test_case.stdout.splitlines(keepends=True), 6801b16049aSZach Atkins fromfile=str(ref_stdout), 6811b16049aSZach Atkins tofile='New')) 6821b16049aSZach Atkins if diff: 6831b16049aSZach Atkins test_case.add_failure_info('stdout', output=''.join(diff)) 6841b16049aSZach Atkins elif test_case.stdout and not suite_spec.check_allowed_stdout(test): 6851b16049aSZach Atkins test_case.add_failure_info('stdout', output=test_case.stdout) 6861b16049aSZach Atkins # expected CSV output 6871b16049aSZach Atkins for ref_csv in ref_csvs: 6888938a869SZach Atkins csv_name = ref_csv.name 689f36e7531SZach Atkins out_file = Path.cwd() / csv_name 6908938a869SZach Atkins if not ref_csv.is_file(): 6918938a869SZach Atkins # remove _{ceed_backend} from path name 6928938a869SZach Atkins ref_csv = (ref_csv.parent / ref_csv.name.rsplit('_', 1)[0]).with_suffix('.csv') 6931b16049aSZach Atkins if not ref_csv.is_file(): 6941b16049aSZach Atkins test_case.add_failure_info('csv', output=f'{ref_csv} not found') 695f36e7531SZach Atkins elif not out_file.is_file(): 696f36e7531SZach Atkins test_case.add_failure_info('csv', output=f'{out_file} not found') 6971b16049aSZach Atkins else: 698f36e7531SZach Atkins csv_ztol: float = spec.csv_ztol if spec.csv_ztol > 0 else suite_spec.csv_ztol 699f36e7531SZach Atkins csv_rtol: float = spec.csv_rtol if spec.csv_rtol > 0 else suite_spec.csv_rtol 7005f1423ffSZach Atkins diff = diff_csv( 7015f1423ffSZach Atkins out_file, 7025f1423ffSZach Atkins ref_csv, 7035f1423ffSZach Atkins csv_ztol, 7045f1423ffSZach Atkins csv_rtol, 7055f1423ffSZach Atkins suite_spec.csv_comment_str, 7065f1423ffSZach Atkins suite_spec.csv_comment_diff_fn) 7071b16049aSZach Atkins if diff: 708f36e7531SZach Atkins save_path: Path = suite_spec.test_failure_artifacts_path / csv_name 709f36e7531SZach Atkins shutil.move(out_file, save_path) 710f36e7531SZach Atkins test_case.add_failure_info(f'csv: {save_path}', output=diff) 7111b16049aSZach Atkins else: 712f36e7531SZach Atkins out_file.unlink() 7131b16049aSZach Atkins # expected CGNS output 7141b16049aSZach Atkins for ref_cgn in ref_cgns: 7158938a869SZach Atkins cgn_name = ref_cgn.name 716f36e7531SZach Atkins out_file = Path.cwd() / cgn_name 7178938a869SZach Atkins if not ref_cgn.is_file(): 7188938a869SZach Atkins # remove _{ceed_backend} from path name 7198938a869SZach Atkins ref_cgn = (ref_cgn.parent / ref_cgn.name.rsplit('_', 1)[0]).with_suffix('.cgns') 7201b16049aSZach Atkins if not ref_cgn.is_file(): 7211b16049aSZach Atkins test_case.add_failure_info('cgns', output=f'{ref_cgn} not found') 722f36e7531SZach Atkins elif not out_file.is_file(): 723f36e7531SZach Atkins test_case.add_failure_info('cgns', output=f'{out_file} not found') 7241b16049aSZach Atkins else: 725f36e7531SZach Atkins cgns_tol = spec.cgns_tol if spec.cgns_tol > 0 else suite_spec.cgns_tol 726f36e7531SZach Atkins diff = diff_cgns(out_file, ref_cgn, cgns_tol=cgns_tol) 7271b16049aSZach Atkins if diff: 728f36e7531SZach Atkins save_path: Path = suite_spec.test_failure_artifacts_path / cgn_name 729f36e7531SZach Atkins shutil.move(out_file, save_path) 730f36e7531SZach Atkins test_case.add_failure_info(f'cgns: {save_path}', output=diff) 7311b16049aSZach Atkins else: 732f36e7531SZach Atkins out_file.unlink() 733f36e7531SZach Atkins # expected ASCII output 734f36e7531SZach Atkins for ref_file in ref_ascii: 735f36e7531SZach Atkins ref_name = ref_file.name 736f36e7531SZach Atkins out_file = Path.cwd() / ref_name 737f36e7531SZach Atkins if not ref_file.is_file(): 738f36e7531SZach Atkins # remove _{ceed_backend} from path name 739f36e7531SZach Atkins ref_file = (ref_file.parent / ref_file.name.rsplit('_', 1)[0]).with_suffix(ref_file.suffix) 740f36e7531SZach Atkins if not ref_file.is_file(): 741f36e7531SZach Atkins test_case.add_failure_info('ascii', output=f'{ref_file} not found') 742f36e7531SZach Atkins elif not out_file.is_file(): 743f36e7531SZach Atkins test_case.add_failure_info('ascii', output=f'{out_file} not found') 744f36e7531SZach Atkins else: 745f36e7531SZach Atkins diff = diff_ascii(out_file, ref_file, backend) 746f36e7531SZach Atkins if diff: 747f36e7531SZach Atkins save_path: Path = suite_spec.test_failure_artifacts_path / ref_name 748f36e7531SZach Atkins shutil.move(out_file, save_path) 749f36e7531SZach Atkins test_case.add_failure_info(f'ascii: {save_path}', output=diff) 750f36e7531SZach Atkins else: 751f36e7531SZach Atkins out_file.unlink() 7521b16049aSZach Atkins 7531b16049aSZach Atkins # store result 7541b16049aSZach Atkins test_case.args = ' '.join(str(arg) for arg in run_args) 755f36e7531SZach Atkins output_str = test_case_output_string(test_case, spec, mode, backend, test, index, verbose) 75619868e18SZach Atkins 75719868e18SZach Atkins return test_case, output_str 75819868e18SZach Atkins 75919868e18SZach Atkins 76019868e18SZach Atkinsdef init_process(): 76119868e18SZach Atkins """Initialize multiprocessing process""" 76219868e18SZach Atkins # set up error handler 76319868e18SZach Atkins global my_env 76419868e18SZach Atkins my_env = os.environ.copy() 76519868e18SZach Atkins my_env['CEED_ERROR_HANDLER'] = 'exit' 76619868e18SZach Atkins 76719868e18SZach Atkins 76878cb100bSJames Wrightdef run_tests(test: str, ceed_backends: List[str], mode: RunMode, nproc: int, 769f36e7531SZach Atkins suite_spec: SuiteSpec, pool_size: int = 1, search: str = ".*", verbose: bool = False) -> TestSuite: 77019868e18SZach Atkins """Run all test cases for `test` with each of the provided `ceed_backends` 77119868e18SZach Atkins 77219868e18SZach Atkins Args: 77319868e18SZach Atkins test (str): Name of test 7748938a869SZach Atkins ceed_backends (List[str]): List of libCEED backends 77519868e18SZach Atkins mode (RunMode): Output mode, either `RunMode.TAP` or `RunMode.JUNIT` 77619868e18SZach Atkins nproc (int): Number of MPI processes to use when running each test case 77719868e18SZach Atkins suite_spec (SuiteSpec): Object defining required methods for running tests 77819868e18SZach Atkins pool_size (int, optional): Number of processes to use when running tests in parallel. Defaults to 1. 779f36e7531SZach Atkins search (str, optional): Regular expression used to match tests. Defaults to ".*". 780f36e7531SZach Atkins verbose (bool, optional): Print detailed output for all runs, not just failures. Defaults to False. 78119868e18SZach Atkins 78219868e18SZach Atkins Returns: 78319868e18SZach Atkins TestSuite: JUnit `TestSuite` containing results of all test cases 78419868e18SZach Atkins """ 785f36e7531SZach Atkins test_specs: List[TestSpec] = [ 786f36e7531SZach Atkins t for t in get_test_args(suite_spec.get_source_path(test)) if re.search(search, t.name, re.IGNORECASE) 787f36e7531SZach Atkins ] 788f36e7531SZach Atkins suite_spec.test_failure_artifacts_path.mkdir(parents=True, exist_ok=True) 78919868e18SZach Atkins if mode is RunMode.TAP: 7908938a869SZach Atkins print('TAP version 13') 7918938a869SZach Atkins print(f'1..{len(test_specs)}') 79219868e18SZach Atkins 79319868e18SZach Atkins with mp.Pool(processes=pool_size, initializer=init_process) as pool: 794f36e7531SZach Atkins async_outputs: List[List[mp.pool.AsyncResult]] = [ 795f36e7531SZach Atkins [pool.apply_async(run_test, (i, test, spec, backend, mode, nproc, suite_spec, verbose)) 7968938a869SZach Atkins for (i, backend) in enumerate(ceed_backends, start=1)] 7978938a869SZach Atkins for spec in test_specs 7988938a869SZach Atkins ] 79919868e18SZach Atkins 80019868e18SZach Atkins test_cases = [] 8018938a869SZach Atkins for (i, subtest) in enumerate(async_outputs, start=1): 8028938a869SZach Atkins is_new_subtest = True 8038938a869SZach Atkins subtest_ok = True 8048938a869SZach Atkins for async_output in subtest: 80519868e18SZach Atkins test_case, print_output = async_output.get() 80619868e18SZach Atkins test_cases.append(test_case) 8078938a869SZach Atkins if is_new_subtest and mode == RunMode.TAP: 8088938a869SZach Atkins is_new_subtest = False 8098938a869SZach Atkins print(f'# Subtest: {test_case.category}') 8108938a869SZach Atkins print(f' 1..{len(ceed_backends)}') 81119868e18SZach Atkins print(print_output, end='') 8128938a869SZach Atkins if test_case.is_failure() or test_case.is_error(): 8138938a869SZach Atkins subtest_ok = False 8148938a869SZach Atkins if mode == RunMode.TAP: 8158938a869SZach Atkins print(f'{"" if subtest_ok else "not "}ok {i} - {test_case.category}') 8161b16049aSZach Atkins 8171b16049aSZach Atkins return TestSuite(test, test_cases) 8181b16049aSZach Atkins 8191b16049aSZach Atkins 820*681c4c48SJeremy L Thompsondef write_junit_xml(test_suite: TestSuite, batch: str = '') -> None: 8211b16049aSZach Atkins """Write a JUnit XML file containing the results of a `TestSuite` 8221b16049aSZach Atkins 8231b16049aSZach Atkins Args: 8241b16049aSZach Atkins test_suite (TestSuite): JUnit `TestSuite` to write 8251b16049aSZach Atkins batch (str): Name of JUnit batch, defaults to empty string 8261b16049aSZach Atkins """ 827*681c4c48SJeremy L Thompson output_file = Path('build') / (f'{test_suite.name}{batch}.junit') 8281b16049aSZach Atkins output_file.write_text(to_xml_report_string([test_suite])) 8291b16049aSZach Atkins 8301b16049aSZach Atkins 8311b16049aSZach Atkinsdef has_failures(test_suite: TestSuite) -> bool: 8321b16049aSZach Atkins """Check whether any test cases in a `TestSuite` failed 8331b16049aSZach Atkins 8341b16049aSZach Atkins Args: 8351b16049aSZach Atkins test_suite (TestSuite): JUnit `TestSuite` to check 8361b16049aSZach Atkins 8371b16049aSZach Atkins Returns: 8381b16049aSZach Atkins bool: True if any test cases failed 8391b16049aSZach Atkins """ 8401b16049aSZach Atkins return any(c.is_failure() or c.is_error() for c in test_suite.test_cases) 841