18ec9d54bSJed Brown#!/usr/bin/env python3 28ec9d54bSJed Brown 3*372821a4SZach Atkinsfrom dataclasses import dataclass, field 4*372821a4SZach Atkinsimport difflib 5*372821a4SZach Atkinsfrom itertools import combinations 68ec9d54bSJed Brownimport os 7*372821a4SZach Atkinsfrom pathlib import Path 8*372821a4SZach Atkinsimport re 9*372821a4SZach Atkinsimport subprocess 108ec9d54bSJed Brownimport sys 11*372821a4SZach Atkinsimport time 12*372821a4SZach Atkinssys.path.insert(0, str(Path(__file__).parent / "junit-xml")) 138ec9d54bSJed Brownfrom junit_xml import TestCase, TestSuite 148ec9d54bSJed Brown 153d94f746Srezgarshakeri 16*372821a4SZach Atkins@dataclass 17*372821a4SZach Atkinsclass TestSpec: 18*372821a4SZach Atkins name: str 19*372821a4SZach Atkins only: list = field(default_factory=list) 20*372821a4SZach Atkins args: list = field(default_factory=list) 218ec9d54bSJed Brown 223d94f746Srezgarshakeri 23*372821a4SZach Atkinsdef parse_test_line(line: str) -> TestSpec: 24*372821a4SZach Atkins args = line.strip().split() 25*372821a4SZach Atkins if args[0] == 'TESTARGS': 26*372821a4SZach Atkins return TestSpec(name='', args=args[1:]) 27*372821a4SZach Atkins test_args = args[0][args[0].index('TESTARGS(')+9:args[0].rindex(')')] 28*372821a4SZach Atkins # transform 'name="myname",only="serial,int32"' into {'name': 'myname', 'only': 'serial,int32'} 29*372821a4SZach Atkins test_args = dict([''.join(t).split('=') for t in re.findall(r"""([^,=]+)(=)"([^"]*)\"""", test_args)]) 30*372821a4SZach Atkins constraints = test_args['only'].split(',') if 'only' in test_args else [] 31*372821a4SZach Atkins if len(args) > 1: 32*372821a4SZach Atkins return TestSpec(name=test_args['name'], only=constraints, args=args[1:]) 335435e910SJed Brown else: 34*372821a4SZach Atkins return TestSpec(name=test_args['name'], only=constraints) 358ec9d54bSJed Brown 363d94f746Srezgarshakeri 37*372821a4SZach Atkinsdef get_testargs(file : Path) -> list[TestSpec]: 38*372821a4SZach Atkins if file.suffix in ['.c', '.cpp']: comment_str = '//' 39*372821a4SZach Atkins elif file.suffix in ['.py']: comment_str = '#' 40*372821a4SZach Atkins elif file.suffix in ['.usr']: comment_str = 'C_' 41*372821a4SZach Atkins elif file.suffix in ['.f90']: comment_str = '! ' 42*372821a4SZach Atkins else: raise RuntimeError(f'Unrecognized extension for file: {file}') 43*372821a4SZach Atkins 44*372821a4SZach Atkins return [parse_test_line(line.strip(comment_str)) 45*372821a4SZach Atkins for line in file.read_text().splitlines() 46*372821a4SZach Atkins if line.startswith(f'{comment_str}TESTARGS')] or [TestSpec('', args=['{ceed_resource}'])] 478ec9d54bSJed Brown 483d94f746Srezgarshakeri 49*372821a4SZach Atkinsdef get_source(test: str) -> Path: 50*372821a4SZach Atkins prefix, rest = test.split('-', 1) 51*372821a4SZach Atkins if prefix == 'petsc': 52*372821a4SZach Atkins return (Path('examples') / 'petsc' / rest).with_suffix('.c') 53*372821a4SZach Atkins elif prefix == 'mfem': 54*372821a4SZach Atkins return (Path('examples') / 'mfem' / rest).with_suffix('.cpp') 55*372821a4SZach Atkins elif prefix == 'nek': 56*372821a4SZach Atkins return (Path('examples') / 'nek' / 'bps' / rest).with_suffix('.usr') 57*372821a4SZach Atkins elif prefix == 'fluids': 58*372821a4SZach Atkins return (Path('examples') / 'fluids' / rest).with_suffix('.c') 59*372821a4SZach Atkins elif prefix == 'solids': 60*372821a4SZach Atkins return (Path('examples') / 'solids' / rest).with_suffix('.c') 61*372821a4SZach Atkins elif test.startswith('ex'): 62*372821a4SZach Atkins return (Path('examples') / 'ceed' / test).with_suffix('.c') 63*372821a4SZach Atkins elif test.endswith('-f'): 64*372821a4SZach Atkins return (Path('tests') / test).with_suffix('.f90') 65*372821a4SZach Atkins else: 66*372821a4SZach Atkins return (Path('tests') / test).with_suffix('.c') 67*372821a4SZach Atkins 68*372821a4SZach Atkins 69*372821a4SZach Atkinsdef check_required_failure(test_case: TestCase, stderr: str, required: str) -> None: 70bdb0bdbbSJed Brown if required in stderr: 714a2fcf2fSJeremy L Thompson test_case.status = 'fails with required: {}'.format(required) 72bdb0bdbbSJed Brown else: 734a2fcf2fSJeremy L Thompson test_case.add_failure_info('required: {}'.format(required)) 74bdb0bdbbSJed Brown 753d94f746Srezgarshakeri 76*372821a4SZach Atkinsdef contains_any(resource: str, substrings: list[str]) -> bool: 77b974e86eSJed Brown return any((sub in resource for sub in substrings)) 78b974e86eSJed Brown 793d94f746Srezgarshakeri 80*372821a4SZach Atkinsdef skip_rule(test: str, resource: str) -> bool: 81b974e86eSJed Brown return any(( 820be03a92SJeremy L Thompson test.startswith('t4') and contains_any(resource, ['occa']), 830be03a92SJeremy L Thompson test.startswith('t5') and contains_any(resource, ['occa']), 840be03a92SJeremy L Thompson test.startswith('ex') and contains_any(resource, ['occa']), 850be03a92SJeremy L Thompson test.startswith('mfem') and contains_any(resource, ['occa']), 860be03a92SJeremy L Thompson test.startswith('nek') and contains_any(resource, ['occa']), 870be03a92SJeremy L Thompson test.startswith('petsc-') and contains_any(resource, ['occa']), 8812070e41Snbeams test.startswith('fluids-') and contains_any(resource, ['occa']), 89ccaff030SJeremy L Thompson test.startswith('solids-') and contains_any(resource, ['occa']), 9012070e41Snbeams test.startswith('t318') and contains_any(resource, ['/gpu/cuda/ref']), 9112070e41Snbeams test.startswith('t506') and contains_any(resource, ['/gpu/cuda/shared']), 92b974e86eSJed Brown )) 93b974e86eSJed Brown 943d94f746Srezgarshakeri 95*372821a4SZach Atkinsdef run(test: str, backends: list[str], mode: str) -> TestSuite: 965435e910SJed Brown source = get_source(test) 97*372821a4SZach Atkins test_specs = get_testargs(source) 98288c0443SJeremy L Thompson 994a2fcf2fSJeremy L Thompson if mode.lower() == "tap": 100*372821a4SZach Atkins print('1..' + str(len(test_specs) * len(backends))) 1014a2fcf2fSJeremy L Thompson 1023d94f746Srezgarshakeri test_cases = [] 1032777116bSjeremylt my_env = os.environ.copy() 1043d94f746Srezgarshakeri my_env["CEED_ERROR_HANDLER"] = 'exit' 1054a2fcf2fSJeremy L Thompson index = 1 106*372821a4SZach Atkins for spec in test_specs: 1078ec9d54bSJed Brown for ceed_resource in backends: 108*372821a4SZach Atkins rargs = [str(Path('build') / test), *spec.args] 1098ec9d54bSJed Brown rargs[rargs.index('{ceed_resource}')] = ceed_resource 110b974e86eSJed Brown 1114a2fcf2fSJeremy L Thompson # run test 112b974e86eSJed Brown if skip_rule(test, ceed_resource): 113*372821a4SZach Atkins test_case = TestCase(f'{test} {ceed_resource}', 114b974e86eSJed Brown elapsed_sec=0, 11537e4ed59SJed Brown timestamp=time.strftime('%Y-%m-%d %H:%M:%S %Z', time.localtime()), 116b974e86eSJed Brown stdout='', 117b974e86eSJed Brown stderr='') 1184a2fcf2fSJeremy L Thompson test_case.add_skipped_info('Pre-run skip rule') 119b974e86eSJed Brown else: 1208ec9d54bSJed Brown start = time.time() 1218ec9d54bSJed Brown proc = subprocess.run(rargs, 1228ec9d54bSJed Brown stdout=subprocess.PIPE, 1232777116bSjeremylt stderr=subprocess.PIPE, 1242777116bSjeremylt env=my_env) 12573132ccbSJed Brown proc.stdout = proc.stdout.decode('utf-8') 12673132ccbSJed Brown proc.stderr = proc.stderr.decode('utf-8') 1278ec9d54bSJed Brown 128*372821a4SZach Atkins test_case = TestCase(f'{test} {spec.name} {ceed_resource}', 129*372821a4SZach Atkins classname=source.parent, 1308ec9d54bSJed Brown elapsed_sec=time.time() - start, 1318ec9d54bSJed Brown timestamp=time.strftime('%Y-%m-%d %H:%M:%S %Z', time.localtime(start)), 1328ec9d54bSJed Brown stdout=proc.stdout, 1338ec9d54bSJed Brown stderr=proc.stderr) 134*372821a4SZach Atkins ref_stdout = (Path('tests') / 'output' / test).with_suffix('.out') 135bdb0bdbbSJed Brown 1364a2fcf2fSJeremy L Thompson # check for allowed errors 1374a2fcf2fSJeremy L Thompson if not test_case.is_skipped() and proc.stderr: 1388ec9d54bSJed Brown if 'OCCA backend failed to use' in proc.stderr: 1394a2fcf2fSJeremy L Thompson test_case.add_skipped_info('occa mode not supported {} {}'.format(test, ceed_resource)) 1408ec9d54bSJed Brown elif 'Backend does not implement' in proc.stderr: 1414a2fcf2fSJeremy L Thompson test_case.add_skipped_info('not implemented {} {}'.format(test, ceed_resource)) 1429c774eddSJeremy L Thompson elif 'Can only provide HOST memory for this backend' in proc.stderr: 1434a2fcf2fSJeremy L Thompson test_case.add_skipped_info('device memory not supported {} {}'.format(test, ceed_resource)) 14480a9ef05SNatalie Beams elif 'Test not implemented in single precision' in proc.stderr: 1454a2fcf2fSJeremy L Thompson test_case.add_skipped_info('not implemented {} {}'.format(test, ceed_resource)) 146bd882c8aSJames Wright elif 'No SYCL devices of the requested type are available' in proc.stderr: 147bd882c8aSJames Wright test_case.add_skipped_info('sycl device type not available {} {}'.format(test, ceed_resource)) 148bdb0bdbbSJed Brown 1494a2fcf2fSJeremy L Thompson # check required failures 1504a2fcf2fSJeremy L Thompson if not test_case.is_skipped(): 151*372821a4SZach Atkins if test[:4] in ['t006', 't007']: 1524a2fcf2fSJeremy L Thompson check_required_failure(test_case, proc.stderr, 'No suitable backend:') 153*372821a4SZach Atkins if test[:4] in ['t008']: 1544a2fcf2fSJeremy L Thompson check_required_failure(test_case, proc.stderr, 'Available backend resources:') 155*372821a4SZach Atkins if test[:4] in ['t110', 't111', 't112', 't113', 't114']: 1564a2fcf2fSJeremy L Thompson check_required_failure(test_case, proc.stderr, 'Cannot grant CeedVector array access') 157*372821a4SZach Atkins if test[:4] in ['t115']: 1584a2fcf2fSJeremy L Thompson check_required_failure(test_case, proc.stderr, 'Cannot grant CeedVector read-only array access, the access lock is already in use') 159*372821a4SZach Atkins if test[:4] in ['t116']: 1604a2fcf2fSJeremy L Thompson check_required_failure(test_case, proc.stderr, 'Cannot destroy CeedVector, the writable access lock is in use') 161*372821a4SZach Atkins if test[:4] in ['t117']: 1624a2fcf2fSJeremy L Thompson check_required_failure(test_case, proc.stderr, 'Cannot restore CeedVector array access, access was not granted') 163*372821a4SZach Atkins if test[:4] in ['t118']: 1644a2fcf2fSJeremy L Thompson check_required_failure(test_case, proc.stderr, 'Cannot sync CeedVector, the access lock is already in use') 165*372821a4SZach Atkins if test[:4] in ['t215']: 1664a2fcf2fSJeremy L Thompson check_required_failure(test_case, proc.stderr, 'Cannot destroy CeedElemRestriction, a process has read access to the offset data') 167*372821a4SZach Atkins if test[:4] in ['t303']: 1684a2fcf2fSJeremy L Thompson check_required_failure(test_case, proc.stderr, 'Length of input/output vectors incompatible with basis dimensions') 169*372821a4SZach Atkins if test[:4] in ['t408']: 1704a2fcf2fSJeremy L Thompson check_required_failure(test_case, proc.stderr, 'CeedQFunctionContextGetData(): Cannot grant CeedQFunctionContext data access, a process has read access') 171*372821a4SZach Atkins if test[:4] in ['t409'] and contains_any(ceed_resource, ['memcheck']): 1724a2fcf2fSJeremy L Thompson check_required_failure(test_case, proc.stderr, 'Context data changed while accessed in read-only mode') 173bdb0bdbbSJed Brown 1744a2fcf2fSJeremy L Thompson # classify other results 1754a2fcf2fSJeremy L Thompson if not test_case.is_skipped() and not test_case.status: 176bdb0bdbbSJed Brown if proc.stderr: 1774a2fcf2fSJeremy L Thompson test_case.add_failure_info('stderr', proc.stderr) 178bdb0bdbbSJed Brown elif proc.returncode != 0: 179*372821a4SZach Atkins test_case.add_error_info(f'returncode = {proc.returncode}') 180*372821a4SZach Atkins elif ref_stdout.is_file(): 181*372821a4SZach Atkins diff = list(difflib.unified_diff(ref_stdout.read_text().splitlines(keepends=True), 1828ec9d54bSJed Brown proc.stdout.splitlines(keepends=True), 183*372821a4SZach Atkins fromfile=str(ref_stdout), 1848ec9d54bSJed Brown tofile='New')) 1858ec9d54bSJed Brown if diff: 1864a2fcf2fSJeremy L Thompson test_case.add_failure_info('stdout', output=''.join(diff)) 1870a0da059Sjeremylt elif proc.stdout and test[:4] not in 't003': 1884a2fcf2fSJeremy L Thompson test_case.add_failure_info('stdout', output=proc.stdout) 1894a2fcf2fSJeremy L Thompson 1904a2fcf2fSJeremy L Thompson # store result 1914a2fcf2fSJeremy L Thompson test_case.args = ' '.join(rargs) 1924a2fcf2fSJeremy L Thompson test_cases.append(test_case) 1934a2fcf2fSJeremy L Thompson 1944a2fcf2fSJeremy L Thompson if mode.lower() == "tap": 1954a2fcf2fSJeremy L Thompson # print incremental output if TAP mode 1964a2fcf2fSJeremy L Thompson print('# Test: {}'.format(test_case.name.split(' ')[1])) 1974a2fcf2fSJeremy L Thompson print('# $ {}'.format(test_case.args)) 1984a2fcf2fSJeremy L Thompson if test_case.is_error(): 1996e6d197dSJeremy L Thompson print('not ok {} - ERROR: {}'.format(index, (test_case.errors[0]['message'] or "NO MESSAGE").strip())) 20018f23d85SSebastian Grimberg print('Output: \n{}'.format((test_case.errors[0]['output'] or "NO OUTPUT").strip())) 2019c702cb5SJeremy L Thompson if test_case.is_failure(): 2029c702cb5SJeremy L Thompson print(' FAIL: {}'.format(index, (test_case.failures[0]['message'] or "NO MESSAGE").strip())) 20318f23d85SSebastian Grimberg print('Output: \n{}'.format((test_case.failures[0]['output'] or "NO OUTPUT").strip())) 2044a2fcf2fSJeremy L Thompson elif test_case.is_failure(): 2056e6d197dSJeremy L Thompson print('not ok {} - FAIL: {}'.format(index, (test_case.failures[0]['message'] or "NO MESSAGE").strip())) 20618f23d85SSebastian Grimberg print('Output: \n{}'.format((test_case.failures[0]['output'] or "NO OUTPUT").strip())) 2074a2fcf2fSJeremy L Thompson elif test_case.is_skipped(): 2086e6d197dSJeremy L Thompson print('ok {} - SKIP: {}'.format(index, (test_case.skipped[0]['message'] or "NO MESSAGE").strip())) 2094a2fcf2fSJeremy L Thompson else: 2104a2fcf2fSJeremy L Thompson print('ok {} - PASS'.format(index)) 2114a2fcf2fSJeremy L Thompson sys.stdout.flush() 2124a2fcf2fSJeremy L Thompson else: 2134a2fcf2fSJeremy L Thompson # print error or failure information if JUNIT mode 2149c702cb5SJeremy L Thompson if test_case.is_error() or test_case.is_failure(): 2159c702cb5SJeremy L Thompson print('Test: {} {}'.format(test_case.name.split(' ')[0], test_case.name.split(' ')[1])) 2169c702cb5SJeremy L Thompson print(' $ {}'.format(test_case.args)) 2174a2fcf2fSJeremy L Thompson if test_case.is_error(): 2186e6d197dSJeremy L Thompson print('ERROR: {}'.format((test_case.errors[0]['message'] or "NO MESSAGE").strip())) 2199c702cb5SJeremy L Thompson print('Output: \n{}'.format((test_case.errors[0]['output'] or "NO OUTPUT").strip())) 2209c702cb5SJeremy L Thompson if test_case.is_failure(): 2216e6d197dSJeremy L Thompson print('FAIL: {}'.format((test_case.failures[0]['message'] or "NO MESSAGE").strip())) 2229c702cb5SJeremy L Thompson print('Output: \n{}'.format((test_case.failures[0]['output'] or "NO OUTPUT").strip())) 2234a2fcf2fSJeremy L Thompson sys.stdout.flush() 2244a2fcf2fSJeremy L Thompson index += 1 2254a2fcf2fSJeremy L Thompson 2263d94f746Srezgarshakeri return TestSuite(test, test_cases) 2278ec9d54bSJed Brown 2288ec9d54bSJed Brownif __name__ == '__main__': 2298ec9d54bSJed Brown import argparse 2303d94f746Srezgarshakeri parser = argparse.ArgumentParser('Test runner with JUnit and TAP output') 2313d94f746Srezgarshakeri parser.add_argument('--mode', help='Output mode, JUnit or TAP', default="JUnit") 2328ec9d54bSJed Brown parser.add_argument('--output', help='Output file to write test', default=None) 2338ec9d54bSJed Brown parser.add_argument('--gather', help='Gather all *.junit files into XML', action='store_true') 2348ec9d54bSJed Brown parser.add_argument('test', help='Test executable', nargs='?') 2358ec9d54bSJed Brown args = parser.parse_args() 2368ec9d54bSJed Brown 2378ec9d54bSJed Brown if args.gather: 2388ec9d54bSJed Brown gather() 2398ec9d54bSJed Brown else: 2408ec9d54bSJed Brown backends = os.environ['BACKENDS'].split() 2418ec9d54bSJed Brown 2424a2fcf2fSJeremy L Thompson # run tests 2434a2fcf2fSJeremy L Thompson result = run(args.test, backends, args.mode) 244add335c8SJeremy L Thompson 2454a2fcf2fSJeremy L Thompson # build output 2463d94f746Srezgarshakeri if args.mode.lower() == "junit": 247add335c8SJeremy L Thompson junit_batch = '' 248add335c8SJeremy L Thompson try: 249add335c8SJeremy L Thompson junit_batch = '-' + os.environ['JUNIT_BATCH'] 250add335c8SJeremy L Thompson except: 251add335c8SJeremy L Thompson pass 252*372821a4SZach Atkins output = Path('build') / (args.test + junit_batch + '.junit') if args.output is None else Path(args.output) 253add335c8SJeremy L Thompson 254*372821a4SZach Atkins with output.open('w') as fd: 2558ec9d54bSJed Brown TestSuite.to_file(fd, [result]) 2564a2fcf2fSJeremy L Thompson elif args.mode.lower() != "tap": 2573d94f746Srezgarshakeri raise Exception("output mode not recognized") 258add335c8SJeremy L Thompson 2594a2fcf2fSJeremy L Thompson # check return code 2604d57a9fcSJed Brown for t in result.test_cases: 2614d57a9fcSJed Brown failures = len([c for c in result.test_cases if c.is_failure()]) 2624d57a9fcSJed Brown errors = len([c for c in result.test_cases if c.is_error()]) 26339656d8bSJeremy L Thompson if failures + errors > 0 and args.mode.lower() != "tap": 2644d57a9fcSJed Brown sys.exit(1) 265