xref: /libCEED/tests/junit.py (revision 372821a4499bffb1332f71594d61dd58a71a8b9a)
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