Skip to content

Commit 7a83bc7

Browse files
authored
Introduce a script for updating lit tests (#3503)
And demonstrate its capabilities by porting all tests of the optimize-instructions pass to use lit and FileCheck.
1 parent 3f4d3b3 commit 7a83bc7

16 files changed

+12820
-12655
lines changed

auto_update_tests.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,19 @@ def update_spec_tests():
138138
o.write(stdout)
139139

140140

141+
def update_lit_tests():
142+
print('\n[ updating lit testcases... ]\n')
143+
script = os.path.join(shared.options.binaryen_root,
144+
'scripts',
145+
'update_lit_checks.py')
146+
lit_dir = shared.get_test_dir('lit')
147+
subprocess.check_output([sys.executable,
148+
script,
149+
'--binaryen-bin=' + shared.options.binaryen_bin,
150+
os.path.join(lit_dir, '**', '*.wast'),
151+
os.path.join(lit_dir, '**', '*.wat')])
152+
153+
141154
TEST_SUITES = OrderedDict([
142155
('wasm-opt', wasm_opt.update_wasm_opt_tests),
143156
('wasm-dis', update_wasm_dis_tests),
@@ -149,6 +162,7 @@ def update_spec_tests():
149162
('lld', lld.update_lld_tests),
150163
('wasm2js', wasm2js.update_wasm2js_tests),
151164
('binaryenjs', binaryenjs.update_binaryen_js_tests),
165+
('lit', update_lit_tests),
152166
])
153167

154168

scripts/fuzz_opt.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def randomize_fuzz_settings():
151151

152152

153153
IMPORTANT_INITIAL_CONTENTS = [
154-
os.path.join('passes', 'optimize-instructions_all-features.wast'),
154+
os.path.join('lit', 'passes', 'optimize-instructions.wast'),
155155
os.path.join('passes', 'optimize-instructions_fuzz-exec.wast'),
156156
]
157157
IMPORTANT_INITIAL_CONTENTS = [os.path.join(shared.get_test_dir('.'), t) for t in IMPORTANT_INITIAL_CONTENTS]
@@ -833,7 +833,8 @@ def handle(self, wasm):
833833
wasm2js_tests = shared.get_tests(shared.get_test_dir('wasm2js'), test_suffixes)
834834
lld_tests = shared.get_tests(shared.get_test_dir('lld'), test_suffixes)
835835
unit_tests = shared.get_tests(shared.get_test_dir(os.path.join('unit', 'input')), test_suffixes)
836-
all_tests = core_tests + passes_tests + spec_tests + wasm2js_tests + lld_tests + unit_tests
836+
lit_tests = shared.get_tests(shared.get_test_dir('lit'), test_suffixes, recursive=True)
837+
all_tests = core_tests + passes_tests + spec_tests + wasm2js_tests + lld_tests + unit_tests + lit_tests
837838

838839

839840
# Do one test, given an input file for -ttf and some optimizations to run

scripts/test/shared.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -366,15 +366,16 @@ def get_test_dir(name):
366366
return os.path.join(options.binaryen_test, name)
367367

368368

369-
def get_tests(test_dir, extensions=[]):
369+
def get_tests(test_dir, extensions=[], recursive=False):
370370
"""Returns the list of test files in a given directory. 'extensions' is a
371371
list of file extensions. If 'extensions' is empty, returns all files.
372372
"""
373373
tests = []
374+
star = '**/*' if recursive else '*'
374375
if not extensions:
375-
tests += glob.glob(os.path.join(test_dir, '*'))
376+
tests += glob.glob(os.path.join(test_dir, star), recursive=True)
376377
for ext in extensions:
377-
tests += glob.glob(os.path.join(test_dir, '*' + ext))
378+
tests += glob.glob(os.path.join(test_dir, star + ext), recursive=True)
378379
if options.test_name_filter:
379380
tests = fnmatch.filter(tests, options.test_name_filter)
380381
return sorted(tests)

scripts/update_lit_checks.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2021 WebAssembly Community Group participants
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""A test case update script.
17+
18+
This script is a utility to update wasm-opt based lit tests with new FileCheck
19+
patterns. It is based on LLVM's update_llc_test_checks.py script.
20+
"""
21+
22+
import argparse
23+
import glob
24+
import os
25+
import re
26+
import subprocess
27+
import sys
28+
import tempfile
29+
30+
31+
script_name = os.path.basename(__file__)
32+
NOTICE = (f';; NOTE: Assertions have been generated by {script_name} and ' +
33+
'should not be edited.')
34+
RUN_LINE_RE = re.compile(r'^\s*;;\s*RUN:\s*(.*)$')
35+
CHECK_PREFIX_RE = re.compile(r'.*--check-prefix[= ](\S+).*')
36+
FUNC_RE = re.compile(r'(^\s*)\(func \$(\S*).*$', re.MULTILINE)
37+
38+
39+
def warn(msg):
40+
print(f'WARNING: {msg}', file=sys.stderr)
41+
42+
43+
def itertests(args):
44+
"""
45+
Yield (filename, lines) for each test specified in the command line args
46+
"""
47+
for pattern in args.tests:
48+
tests = glob.glob(pattern, recursive=True)
49+
if not tests:
50+
warn(f'No tests matched {pattern}. Ignoring it.')
51+
continue
52+
for test in tests:
53+
with open(test) as f:
54+
lines = [line.rstrip() for line in f]
55+
first_line = lines[0] if lines else ''
56+
if script_name not in first_line and not args.force:
57+
warn(f'Skipping test {test} which was not generated by '
58+
f'{script_name}. Use -f to override.')
59+
continue
60+
yield test, lines
61+
62+
63+
def find_run_lines(test, lines):
64+
line_matches = [RUN_LINE_RE.match(l) for l in lines]
65+
matches = [match.group(1) for match in line_matches if match]
66+
if not matches:
67+
warn(f'No RUN lines found in {test}. Ignoring.')
68+
return []
69+
run_lines = [matches[0]]
70+
for line in matches[1:]:
71+
if run_lines[-1].endswith('\\'):
72+
run_lines[-1] = run_lines[-1].rstrip('\\') + ' ' + line
73+
else:
74+
run_lines.append(line)
75+
return run_lines
76+
77+
78+
def run_command(args, test, tmp, command):
79+
env = dict(os.environ)
80+
env['PATH'] = args.binaryen_bin + os.pathsep + env['PATH']
81+
command = command.replace('%s', test)
82+
command = command.replace('%t', tmp)
83+
return subprocess.check_output(command, shell=True, env=env).decode('utf-8')
84+
85+
86+
def find_funcs(module):
87+
"""Return a dict mapping each function name to lines in the function"""
88+
result = {}
89+
for match in FUNC_RE.finditer(module):
90+
name = match.group(2)
91+
depth = 1
92+
for end in range(match.end(), len(module)):
93+
if depth == 0:
94+
break
95+
elif module[end] == '(':
96+
depth += 1
97+
elif module[end] == ')':
98+
depth -= 1
99+
result[name] = module[match.start():end].split('\n')
100+
return result
101+
102+
103+
def main():
104+
parser = argparse.ArgumentParser(description=__doc__)
105+
parser.add_argument(
106+
'--binaryen-bin', dest='binaryen_bin', default='bin',
107+
help=('Specifies the path to the Binaryen executables in the CMake build'
108+
' directory. Default: bin/ of current directory (i.e. assume an'
109+
' in-tree build).'))
110+
parser.add_argument(
111+
'-f', '--force', action='store_true',
112+
help=('Generate FileCheck patterns even for test files whose existing '
113+
'patterns were not generated by this script.'))
114+
parser.add_argument(
115+
'--dry-run', action='store_true',
116+
help=('Print the updated test file contents instead of changing the '
117+
'test files'))
118+
parser.add_argument('tests', nargs='+', help='The test files to update')
119+
args = parser.parse_args()
120+
args.binaryen_bin = os.path.abspath(args.binaryen_bin)
121+
122+
tmp = tempfile.mktemp()
123+
124+
for test, lines in itertests(args):
125+
run_list = []
126+
for line in find_run_lines(test, lines):
127+
commands = [cmd.strip() for cmd in line.rsplit('|', 1)]
128+
filecheck_cmd = ''
129+
if len(commands) > 1 and commands[1].startswith('filecheck '):
130+
filecheck_cmd = commands[1]
131+
commands = commands[:1]
132+
133+
check_prefix = ''
134+
if filecheck_cmd.startswith('filecheck '):
135+
prefix_match = CHECK_PREFIX_RE.match(filecheck_cmd)
136+
if prefix_match:
137+
check_prefix = prefix_match.group(1)
138+
else:
139+
check_prefix = 'CHECK'
140+
141+
run_list.append((check_prefix, commands[0]))
142+
143+
# Map check prefixes and function names to the corresponding output
144+
func_dict = {}
145+
for prefix, command, in run_list:
146+
output = run_command(args, test, tmp, command)
147+
if prefix:
148+
func_dict[prefix] = find_funcs(output)
149+
150+
check_line_re = re.compile(r'^\s*;;\s*(' + '|'.join(func_dict.keys()) +
151+
r')(?:-NEXT|-LABEL|-NOT)?: .*$')
152+
output_lines = [NOTICE]
153+
if lines and script_name in lines[0]:
154+
lines = lines[1:]
155+
for line in lines:
156+
if check_line_re.match(line):
157+
continue
158+
func_match = FUNC_RE.match(line)
159+
if func_match:
160+
indent, name = func_match.groups()
161+
for prefix, funcs in func_dict.items():
162+
body = funcs.get(name, [])
163+
if not body:
164+
continue
165+
output_lines.append(f'{indent};; {prefix}: {body[0]}')
166+
for l in body[1:]:
167+
output_lines.append(f'{indent};; {prefix}-NEXT:{l}')
168+
output_lines.append(line)
169+
170+
if args.dry_run:
171+
print('\n'.join(output_lines))
172+
else:
173+
with open(test, 'w') as f:
174+
for line in output_lines:
175+
f.write(line + '\n')
176+
177+
178+
if __name__ == '__main__':
179+
main()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited.
2+
;; RUN: wasm-opt %s --optimize-instructions --enable-threads -S -o - | filecheck %s
3+
4+
(module
5+
(import "env" "memory" (memory $0 (shared 256 256)))
6+
7+
;; CHECK: (func $x
8+
;; CHECK-NEXT: (drop
9+
;; CHECK-NEXT: (i32.shr_s
10+
;; CHECK-NEXT: (i32.shl
11+
;; CHECK-NEXT: (i32.atomic.load8_u
12+
;; CHECK-NEXT: (i32.const 100)
13+
;; CHECK-NEXT: )
14+
;; CHECK-NEXT: (i32.const 24)
15+
;; CHECK-NEXT: )
16+
;; CHECK-NEXT: (i32.const 24)
17+
;; CHECK-NEXT: )
18+
;; CHECK-NEXT: )
19+
;; CHECK-NEXT: )
20+
(func $x
21+
(drop
22+
(i32.shr_s
23+
(i32.shl
24+
(i32.atomic.load8_u ;; can't be signed
25+
(i32.const 100)
26+
)
27+
(i32.const 24)
28+
)
29+
(i32.const 24)
30+
)
31+
)
32+
)
33+
)

0 commit comments

Comments
 (0)