Skip to content

Commit 5d842aa

Browse files
authored
Refactor python fuzz script (#2182)
Create a class for handling the current fuzz testcase, and implement subclasses for the various fuzz things we do. This disentangles a lot of code.
1 parent 08e52f9 commit 5d842aa

1 file changed

Lines changed: 134 additions & 73 deletions

File tree

scripts/fuzz_opt.py

Lines changed: 134 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@
4848

4949
LOG_LIMIT = 125
5050

51-
WASM2JS = False
52-
5351

5452
# utilities
5553

@@ -85,10 +83,10 @@ def randomize_pass_debug():
8583
IGNORE = '[binaryen-fuzzer-ignore]'
8684

8785

88-
def compare(x, y, comment):
86+
def compare(x, y, context):
8987
if x != y and x != IGNORE and y != IGNORE:
90-
message = ''.join([a.rstrip() + '\n' for a in difflib.unified_diff(x.split('\n'), y.split('\n'), fromfile='expected', tofile='actual')])
91-
raise Exception(str(comment) + ": Expected to have '%s' == '%s', diff:\n\n%s" % (
88+
message = ''.join([a + '\n' for a in difflib.unified_diff(x.splitlines(), y.splitlines(), fromfile='expected', tofile='actual')])
89+
raise Exception(context + " comparison error, expected to have '%s' == '%s', diff:\n\n%s" % (
9290
x, y,
9391
message
9492
))
@@ -111,15 +109,15 @@ def fix_double(x):
111109

112110
# exceptions may differ when optimizing, but an exception should occur. so ignore their types
113111
# also js engines print them out slightly differently
114-
return '\n'.join(map(lambda x: ' *exception*' if 'exception' in x else x, out.split('\n')))
112+
return '\n'.join(map(lambda x: ' *exception*' if 'exception' in x else x, out.splitlines()))
115113

116114

117115
def fix_spec_output(out):
118116
out = fix_output(out)
119117
# spec shows a pointer when it traps, remove that
120-
out = '\n'.join(map(lambda x: x if 'runtime trap' not in x else x[x.find('runtime trap'):], out.split('\n')))
118+
out = '\n'.join(map(lambda x: x if 'runtime trap' not in x else x[x.find('runtime trap'):], out.splitlines()))
121119
# https://github.com/WebAssembly/spec/issues/543 , float consts are messed up
122-
out = '\n'.join(map(lambda x: x if 'f32' not in x and 'f64' not in x else '', out.split('\n')))
120+
out = '\n'.join(map(lambda x: x if 'f32' not in x and 'f64' not in x else '', out.splitlines()))
123121
return out
124122

125123

@@ -133,7 +131,7 @@ def run_vm(cmd):
133131
]
134132
try:
135133
return run(cmd)
136-
except:
134+
except subprocess.CalledProcessError:
137135
output = run_unchecked(cmd)
138136
for issue in known_issues:
139137
if issue in output:
@@ -145,83 +143,152 @@ def run_bynterp(wasm):
145143
return fix_output(run_vm([in_bin('wasm-opt'), wasm, '--fuzz-exec-before'] + FEATURE_OPTS))
146144

147145

148-
def run_wasm2js(wasm):
149-
wrapper = run([in_bin('wasm-opt'), wasm, '--emit-js-wrapper=/dev/stdout'] + FEATURE_OPTS)
150-
cmd = [in_bin('wasm2js'), wasm, '--emscripten']
151-
if random.random() < 0.5:
152-
cmd += ['-O']
153-
main = run(cmd + FEATURE_OPTS)
154-
with open(os.path.join(options.binaryen_root, 'scripts', 'wasm2js.js')) as f:
155-
glue = f.read()
156-
with open('js.js', 'w') as f:
157-
f.write(glue)
158-
f.write(main)
159-
f.write(wrapper)
160-
out = fix_output(run_vm([NODEJS, 'js.js', 'a.wasm']))
161-
if 'exception' in out:
162-
# exception, so ignoring - wasm2js does not have normal wasm trapping, so opts can eliminate a trap
163-
out = IGNORE
164-
return out
165-
166-
167-
def run_vms(prefix):
168-
wasm = prefix + 'wasm'
169-
results = []
170-
results.append(run_bynterp(wasm))
171-
results.append(fix_output(run_vm([os.path.expanduser('d8'), prefix + 'js'] + V8_OPTS + ['--', wasm])))
172-
if WASM2JS:
173-
results.append(run_wasm2js(wasm))
174-
175-
# append to add results from VMs
176-
# results += [fix_output(run_vm([os.path.expanduser('d8'), prefix + 'js'] + V8_OPTS + ['--', prefix + 'wasm']))]
177-
# results += [fix_output(run_vm([os.path.expanduser('~/.jsvu/jsc'), prefix + 'js', '--', prefix + 'wasm']))]
178-
# spec has no mechanism to not halt on a trap. so we just check until the first trap, basically
179-
# run(['../spec/interpreter/wasm', prefix + 'wasm'])
180-
# results += [fix_spec_output(run_unchecked(['../spec/interpreter/wasm', prefix + 'wasm', '-e', open(prefix + 'wat').read()]))]
181-
182-
if len(results) == 0:
183-
results = [0]
184-
185-
# NaNs are a source of nondeterminism between VMs; don't compare them
186-
if not NANS:
187-
first = results[0]
188-
for i in range(len(results)):
189-
compare(first, results[i], 'comparing between vms at ' + str(i))
190-
191-
return results
146+
# Each test case handler receives two wasm files, one before and one after some changes
147+
# that should have kept it equivalent. It also receives the optimizations that the
148+
# fuzzer chose to run.
149+
class TestCaseHandler:
150+
# If the core handle_pair() method is not overridden, it calls handle_single()
151+
# on each of the pair. That is useful if you just want the two wasms, and don't
152+
# care about their relationship
153+
def handle_pair(self, before_wasm, after_wasm, opts):
154+
self.handle(before_wasm)
155+
self.handle(after_wasm)
156+
157+
158+
# Run VMs and compare results
159+
class CompareVMs(TestCaseHandler):
160+
def handle_pair(self, before_wasm, after_wasm, opts):
161+
run([in_bin('wasm-opt'), before_wasm, '--emit-js-wrapper=a.js', '--emit-spec-wrapper=a.wat'])
162+
run([in_bin('wasm-opt'), after_wasm, '--emit-js-wrapper=b.js', '--emit-spec-wrapper=b.wat'])
163+
before = self.run_vms('a.js', before_wasm)
164+
after = self.run_vms('b.js', after_wasm)
165+
self.compare_vs(before, after)
166+
167+
def run_vms(self, js, wasm):
168+
results = []
169+
results.append(run_bynterp(wasm))
170+
results.append(fix_output(run_vm(['d8', js] + V8_OPTS + ['--', wasm])))
171+
172+
# append to add results from VMs
173+
# results += [fix_output(run_vm(['d8', js] + V8_OPTS + ['--', wasm]))]
174+
# results += [fix_output(run_vm([os.path.expanduser('~/.jsvu/jsc'), js, '--', wasm]))]
175+
# spec has no mechanism to not halt on a trap. so we just check until the first trap, basically
176+
# run(['../spec/interpreter/wasm', wasm])
177+
# results += [fix_spec_output(run_unchecked(['../spec/interpreter/wasm', wasm, '-e', open(prefix + 'wat').read()]))]
178+
179+
if len(results) == 0:
180+
results = [0]
181+
182+
# NaNs are a source of nondeterminism between VMs; don't compare them
183+
if not NANS:
184+
first = results[0]
185+
for i in range(len(results)):
186+
compare(first, results[i], 'CompareVMs at ' + str(i))
187+
188+
return results
189+
190+
def compare_vs(self, before, after):
191+
for i in range(len(before)):
192+
compare(before[i], after[i], 'CompareVMs at ' + str(i))
193+
# with nans, we can only compare the binaryen interpreter to itself
194+
if NANS:
195+
break
196+
197+
198+
# Fuzz the interpreter with --fuzz-exec. This tests everything in a single command (no
199+
# two separate binaries) so it's easy to reproduce.
200+
class FuzzExec(TestCaseHandler):
201+
def handle_pair(self, before_wasm, after_wasm, opts):
202+
# fuzz binaryen interpreter itself. separate invocation so result is easily fuzzable
203+
run([in_bin('wasm-opt'), before_wasm, '--fuzz-exec', '--fuzz-binary'] + opts)
204+
205+
206+
# Check for determinism - the same command must have the same output
207+
class CheckDeterminism(TestCaseHandler):
208+
def handle_pair(self, before_wasm, after_wasm, opts):
209+
# check for determinism
210+
run([in_bin('wasm-opt'), before_wasm, '-o', 'b1.wasm'] + opts)
211+
run([in_bin('wasm-opt'), before_wasm, '-o', 'b2.wasm'] + opts)
212+
assert open('b1.wasm').read() == open('b2.wasm').read(), 'output must be deterministic'
213+
214+
215+
class Wasm2JS(TestCaseHandler):
216+
def handle_pair(self, before_wasm, after_wasm, opts):
217+
compare(self.run(before_wasm), self.run(after_wasm), 'Wasm2JS')
218+
219+
def run(self, wasm):
220+
# TODO: wasm2js does not handle nans precisely, and does not
221+
# handle oob loads etc. with traps, should we use
222+
# FUZZ_OPTS += ['--no-fuzz-nans']
223+
# FUZZ_OPTS += ['--no-fuzz-oob']
224+
# ?
225+
wrapper = run([in_bin('wasm-opt'), wasm, '--emit-js-wrapper=/dev/stdout'] + FEATURE_OPTS)
226+
cmd = [in_bin('wasm2js'), wasm, '--emscripten']
227+
if random.random() < 0.5:
228+
cmd += ['-O']
229+
main = run(cmd + FEATURE_OPTS)
230+
with open(os.path.join(options.binaryen_root, 'scripts', 'wasm2js.js')) as f:
231+
glue = f.read()
232+
with open('js.js', 'w') as f:
233+
f.write(glue)
234+
f.write(main)
235+
f.write(wrapper)
236+
out = fix_output(run_vm([NODEJS, 'js.js', 'a.wasm']))
237+
if 'exception' in out:
238+
# exception, so ignoring - wasm2js does not have normal wasm trapping, so opts can eliminate a trap
239+
out = IGNORE
240+
return out
241+
242+
243+
class Bysyncify(TestCaseHandler):
244+
def handle(self, wasm):
245+
# run normally and run in an async manner, and compare
246+
before = run([in_bin('wasm-opt'), wasm, '--fuzz-exec'])
247+
# TODO: also something that actually does async sleeps in the code, say
248+
# on the logging commands?
249+
# --remove-unused-module-elements removes the bysyncify intrinsics, which are not valid to call
250+
cmd = [in_bin('wasm-opt'), wasm, '--bysyncify', '--remove-unused-module-elements', '-o', 'by.wasm']
251+
if random.random() < 0.5:
252+
cmd += ['--optimize-level=3'] # TODO: more
253+
run(cmd)
254+
after = run([in_bin('wasm-opt'), 'by.wasm', '--fuzz-exec'])
255+
after = '\n'.join([line for line in after.splitlines() if '[fuzz-exec] calling $bysyncify' not in line])
256+
compare(before, after, 'Bysyncify')
257+
258+
259+
# The global list of all test case handlers
260+
testcase_handlers = [
261+
CompareVMs(),
262+
FuzzExec(),
263+
CheckDeterminism(),
264+
Wasm2JS(),
265+
# TODO Bysyncify(),
266+
]
192267

193268

269+
# Do one test, given an input file for -ttf and some optimizations to run
194270
def test_one(infile, opts):
195271
randomize_pass_debug()
196272

197273
bytes = 0
198274

199275
# fuzz vms
200276
# gather VM outputs on input file
201-
run([in_bin('wasm-opt'), infile, '-ttf', '--emit-js-wrapper=a.js', '--emit-spec-wrapper=a.wat', '-o', 'a.wasm'] + FUZZ_OPTS + FEATURE_OPTS)
277+
run([in_bin('wasm-opt'), infile, '-ttf', '-o', 'a.wasm'] + FUZZ_OPTS + FEATURE_OPTS)
202278
wasm_size = os.stat('a.wasm').st_size
203279
bytes += wasm_size
204280
print('pre js size :', os.stat('a.js').st_size, ' wasm size:', wasm_size)
205-
before = run_vms('a.')
206281
print('----------------')
282+
207283
# gather VM outputs on processed file
208284
run([in_bin('wasm-opt'), 'a.wasm', '-o', 'b.wasm'] + opts + FUZZ_OPTS + FEATURE_OPTS)
209285
wasm_size = os.stat('b.wasm').st_size
210286
bytes += wasm_size
211287
print('post js size:', os.stat('a.js').st_size, ' wasm size:', wasm_size)
212288
shutil.copyfile('a.js', 'b.js')
213-
after = run_vms('b.')
214-
for i in range(len(before)):
215-
compare(before[i], after[i], 'comparing between builds at ' + str(i))
216-
# with nans, we can only compare the binaryen interpreter to itself
217-
if NANS:
218-
break
219-
# fuzz binaryen interpreter itself. separate invocation so result is easily fuzzable
220-
run([in_bin('wasm-opt'), 'a.wasm', '--fuzz-exec', '--fuzz-binary'] + opts + FUZZ_OPTS + FEATURE_OPTS)
221-
# check for determinism
222-
run([in_bin('wasm-opt'), 'a.wasm', '-o', 'b.wasm'] + opts + FUZZ_OPTS + FEATURE_OPTS)
223-
run([in_bin('wasm-opt'), 'a.wasm', '-o', 'c.wasm'] + opts + FUZZ_OPTS + FEATURE_OPTS)
224-
assert open('b.wasm').read() == open('c.wasm').read(), 'output must be deterministic'
289+
290+
for testcase_handler in testcase_handlers:
291+
testcase_handler.handle_pair(before_wasm='a.wasm', after_wasm='b.wasm', opts=opts + FUZZ_OPTS + FEATURE_OPTS)
225292

226293
return bytes
227294

@@ -298,12 +365,6 @@ def get_multiple_opt_choices():
298365
if not NANS:
299366
FUZZ_OPTS += ['--no-fuzz-nans']
300367

301-
if WASM2JS:
302-
# wasm2js does not handle nans precisely, and does not
303-
# handle oob loads etc. with traps
304-
FUZZ_OPTS += ['--no-fuzz-nans']
305-
FUZZ_OPTS += ['--no-fuzz-oob']
306-
307368
if __name__ == '__main__':
308369
print('checking infinite random inputs')
309370
random.seed(time.time() * os.getpid())

0 commit comments

Comments
 (0)