#!/usr/bin/env python3 # Copyright 2021 WebAssembly Community Group participants # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """A test case update script. This script is a utility to update wasm-opt based lit tests with new FileCheck patterns. It is based on LLVM's update_llc_test_checks.py script. """ import argparse import glob import os import re import subprocess import sys import tempfile script_name = os.path.basename(__file__) NOTICE = (f';; NOTE: Assertions have been generated by {script_name} and ' + 'should not be edited.') RUN_LINE_RE = re.compile(r'^\s*;;\s*RUN:\s*(.*)$') CHECK_PREFIX_RE = re.compile(r'.*--check-prefix[= ](\S+).*') FUNC_RE = re.compile(r'(^\s*)\(func \$(\S*).*$', re.MULTILINE) def warn(msg): print(f'WARNING: {msg}', file=sys.stderr) def itertests(args): """ Yield (filename, lines) for each test specified in the command line args """ for pattern in args.tests: tests = glob.glob(pattern, recursive=True) if not tests: warn(f'No tests matched {pattern}. Ignoring it.') continue for test in tests: with open(test) as f: lines = [line.rstrip() for line in f] first_line = lines[0] if lines else '' if script_name not in first_line and not args.force: warn(f'Skipping test {test} which was not generated by ' f'{script_name}. Use -f to override.') continue yield test, lines def find_run_lines(test, lines): line_matches = [RUN_LINE_RE.match(l) for l in lines] matches = [match.group(1) for match in line_matches if match] if not matches: warn(f'No RUN lines found in {test}. Ignoring.') return [] run_lines = [matches[0]] for line in matches[1:]: if run_lines[-1].endswith('\\'): run_lines[-1] = run_lines[-1].rstrip('\\') + ' ' + line else: run_lines.append(line) return run_lines def run_command(args, test, tmp, command): env = dict(os.environ) env['PATH'] = args.binaryen_bin + os.pathsep + env['PATH'] command = command.replace('%s', test) command = command.replace('%t', tmp) return subprocess.check_output(command, shell=True, env=env).decode('utf-8') def find_funcs(module): """Return a dict mapping each function name to lines in the function""" result = {} for match in FUNC_RE.finditer(module): name = match.group(2) depth = 1 for end in range(match.end(), len(module)): if depth == 0: break elif module[end] == '(': depth += 1 elif module[end] == ')': depth -= 1 result[name] = module[match.start():end].split('\n') return result def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( '--binaryen-bin', dest='binaryen_bin', default='bin', help=('Specifies the path to the Binaryen executables in the CMake build' ' directory. Default: bin/ of current directory (i.e. assume an' ' in-tree build).')) parser.add_argument( '-f', '--force', action='store_true', help=('Generate FileCheck patterns even for test files whose existing ' 'patterns were not generated by this script.')) parser.add_argument( '--dry-run', action='store_true', help=('Print the updated test file contents instead of changing the ' 'test files')) parser.add_argument('tests', nargs='+', help='The test files to update') args = parser.parse_args() args.binaryen_bin = os.path.abspath(args.binaryen_bin) tmp = tempfile.mktemp() for test, lines in itertests(args): run_list = [] for line in find_run_lines(test, lines): commands = [cmd.strip() for cmd in line.rsplit('|', 1)] filecheck_cmd = '' if len(commands) > 1 and commands[1].startswith('filecheck '): filecheck_cmd = commands[1] commands = commands[:1] check_prefix = '' if filecheck_cmd.startswith('filecheck '): prefix_match = CHECK_PREFIX_RE.match(filecheck_cmd) if prefix_match: check_prefix = prefix_match.group(1) else: check_prefix = 'CHECK' run_list.append((check_prefix, commands[0])) # Map check prefixes and function names to the corresponding output func_dict = {} for prefix, command, in run_list: output = run_command(args, test, tmp, command) if prefix: func_dict[prefix] = find_funcs(output) check_line_re = re.compile(r'^\s*;;\s*(' + '|'.join(func_dict.keys()) + r')(?:-NEXT|-LABEL|-NOT)?: .*$') output_lines = [NOTICE] if lines and script_name in lines[0]: lines = lines[1:] for line in lines: if check_line_re.match(line): continue func_match = FUNC_RE.match(line) if func_match: indent, name = func_match.groups() for prefix, funcs in func_dict.items(): body = funcs.get(name, []) if not body: continue output_lines.append(f'{indent};; {prefix}: {body[0]}') for l in body[1:]: output_lines.append(f'{indent};; {prefix}-NEXT:{l}') output_lines.append(line) if args.dry_run: print('\n'.join(output_lines)) else: with open(test, 'w') as f: for line in output_lines: f.write(line + '\n') if __name__ == '__main__': main()