Skip to content

Commit ecbf638

Browse files
committed
slot_fuzzer: minor improvements, added tp_richcompare
1 parent 48a8296 commit ecbf638

File tree

1 file changed

+94
-59
lines changed

1 file changed

+94
-59
lines changed

scripts/slot_fuzzer.py

Lines changed: 94 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -37,26 +37,42 @@
3737
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3838
# SOFTWARE.
3939

40-
# Generates self-contained Python scripts that:
40+
41+
# Simplistic fuzzer testing interactions between slots, magic methods and builtin functions
42+
#
43+
# How to use:
44+
#
45+
# Run the fuzzer as follows:
4146
#
42-
# 1) contain from 1 to 3 classes, each class is either pure Python or native heap type
43-
# (the script itself takes care of compiling and loading the extension). Python class
44-
# N inherits from class N-1.
47+
# python3 slot_fuzzer.py --graalpy /path/to/bin/graalpy --cpython /path/to/bin/python3 --iterations 100
4548
#
46-
# 2) invoke various Python operations on the last class in the hierarchy. The results are
47-
# printed to the stdout. If there is an exception, only its type is printed to the stdout
49+
# It is recommended to use debug build of CPython, so that wrong usages of the C API fail the CPython run,
50+
# and we do not even run such bogus tests on GraalPy.
4851
#
49-
# After each such Python script is generated it is executed on both CPython and GraalPy
50-
# and the output is compared. If CPython segfaults, we ignore the test case.
52+
# Triaging a failed test, for example, test number 42:
53+
# cpython42.out is the CPython output
54+
# graalpy42.out is GraaPy output -> compare the two using some diff tool
55+
# test42.py is the self-contained test, running it again is as simple as:
56+
# `/path/to/bin/graalpy test42.py` or `/path/to/bin/python3 test42.py`
57+
# the test itself compiles and loads the native extension
58+
# test42.c is the native extension code
59+
# backup test42.py and manually reduce the code in it to get a smaller reproducer suitable for debugging
5160
#
52-
# After the experiment, one can simply manually rerun the problematic test scripts.
61+
# Adding a new slot:
62+
# - add example implementations to "SLOTS" and "MAGIC"
63+
# - single line implementation is assumed to be an expression and will be transformed to a proper function body
64+
# - multiline implementation is taken as-is
65+
# - add tests to 'slots_tester' function
66+
67+
5368
import os.path
5469
import sys
5570
import dataclasses
5671
import random
5772
import itertools
5873
import subprocess
5974
import textwrap
75+
import time
6076
from argparse import ArgumentParser
6177
from collections import defaultdict
6278

@@ -67,11 +83,14 @@
6783
import traceback
6884
import operator
6985
def slots_tester(Klass, other_klasses):
86+
def normalize_output(text):
87+
return re.sub(r'object at 0x[0-9a-fA-F]+', '', text)
88+
7089
def test(fun, name):
7190
try:
7291
print(f'{name}:', end='')
7392
result = repr(fun())
74-
result = re.sub(r'object at 0x[0-9a-fA-F]+', '', result)
93+
result = normalize_output(result)
7594
print(result)
7695
except Exception as e:
7796
if '--verbose' in sys.argv or '-v' in sys.argv:
@@ -81,9 +100,9 @@ def test(fun, name):
81100
82101
def test_dunder(obj, fun_name, *args):
83102
# avoid going through tp_getattr/o, which may be overridden to something funky
84-
args_str = ','.join([repr(x) for x in args])
85-
test(lambda: Klass.__dict__[fun_name](obj, *args), f"{fun_name} via class dict")
86-
test(lambda: getattr(obj, fun_name)(*args), f"{fun_name}")
103+
args_str = normalize_output(','.join([repr(x) for x in [obj, *args]]))
104+
test(lambda: type(obj).__dict__[fun_name](obj, *args), f"{fun_name} via class dict for {args_str}")
105+
test(lambda: getattr(obj, fun_name)(*args), f"{fun_name} for {args_str}")
87106
88107
def write_attr(obj, attr, value):
89108
if attr == 'foo':
@@ -152,18 +171,32 @@ def del_descr(self): del self.descr
152171
other_objs = [K() for K in other_klasses] + [42, 3.14, 'string', (1,2,3)]
153172
for obj2 in other_objs:
154173
obj1 = Klass()
155-
test(lambda: obj1 + obj2, f"{Klass} + {type(obj2)}")
156-
test(lambda: obj2 + obj1, f"{type(obj2)} + {Klass}")
157-
test(lambda: obj1 * obj2, f"{Klass} * {type(obj2)}")
158-
test(lambda: obj2 * obj1, f"{type(obj2)} * {Klass}")
159-
test(lambda: operator.concat(obj1, obj2), f"operator.concat({type(obj2)}, {Klass})")
160-
test(lambda: operator.mul(obj1, obj2), f"operator.mul({type(obj2)}, {Klass})")
161-
test_dunder(obj1, "__add__", obj2)
162-
test_dunder(obj1, "__radd__", obj2)
163-
test_dunder(obj1, "__mul__", obj2)
164-
165-
166-
test_dunder(obj, '__hash__')
174+
for op in [operator.add, operator.mul, operator.lt, operator.le, operator.eq,
175+
operator.ne, operator.gt, operator.ge, operator.concat]:
176+
test(lambda: op(obj1, obj2), f"{op.__name__}: {Klass}, {type(obj2)}")
177+
test(lambda: op(obj2, obj1), f"{op.__name__}: {type(obj2)}, {Klass}")
178+
179+
for dunder in ["__add__", "__radd__", "__mul__", "__rmul__", "__lt__", "__le__", "__eq__", "__ne__", "__gt__", "__ge__"]:
180+
test_dunder(obj1, dunder, obj2)
181+
test_dunder(obj2, dunder, obj1)
182+
183+
def safe_hashattr(x, name):
184+
try:
185+
return hasattr(x, name)
186+
except:
187+
return False
188+
189+
if safe_hashattr(Klass(), '__hash__'):
190+
if Klass().__hash__ is None:
191+
print("Klass().__hash__ is None")
192+
else:
193+
# just check presence/absence of exception
194+
def test_hash():
195+
hash(Klass())
196+
return 'dummy result'
197+
test(test_hash, '__hash__')
198+
else:
199+
print("Klass().__hash__ does not exist")
167200
'''
168201

169202
# language=Python
@@ -285,7 +318,15 @@ def tp_decl(self, name_prefix):
285318
global_stash2 = value;
286319
return 0;
287320
''']),
288-
Slot(NO_GROUP, 'tp_hash', 'Py_hash_t $name$(PyObject* self)', ['0', None, '42', '-1', '-2', 'PY_SSIZE_T_MAX'])
321+
Slot(NO_GROUP, 'tp_hash', 'Py_hash_t $name$(PyObject* self)', ['0', None, '42', '-2', 'PY_SSIZE_T_MAX']),
322+
Slot(NO_GROUP, 'tp_richcompare', 'PyObject* $name$(PyObject* v, PyObject* w, int op)', [
323+
'Py_RETURN_FALSE', 'Py_RETURN_TRUE', None, 'Py_RETURN_NONE', 'Py_RETURN_NOTIMPLEMENTED',
324+
*[f'''
325+
if (op == {ops_true[0]} || op == {ops_true[1]}) Py_RETURN_TRUE;
326+
if (op == {ops_false[0]} || op == {ops_false[1]}) Py_RETURN_FALSE;
327+
Py_RETURN_NOTIMPLEMENTED;
328+
''' for ops_true in itertools.combinations(range(0, 6), 2)
329+
for ops_false in itertools.combinations(set(range(0, 6)) - set(ops_true), 2)]]),
289330
]
290331

291332
PY_GLOBALS = '''
@@ -321,15 +362,12 @@ def tp_decl(self, name_prefix):
321362
return None
322363
'''],
323364
'__getitem__(self, index)': [None, 'True', 'repr(index)'],
324-
'__hash__(self, index)': [None, '1', '-1', '-2', '44', '123456788901223442423234234']
365+
'__hash__(self)': [None, '1', '-2', '44', '123456788901223442423234234'],
366+
**{name + '(self, other)': [None, 'True', 'False', 'NotImplemented']
367+
for name in ['__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__']}
325368
}
326369

327370

328-
def all_magic_impls(key):
329-
for body in MAGIC[key]:
330-
yield magic_impl(key, body)
331-
332-
333371
def magic_impl(magic_key, magic_body):
334372
if magic_body is None:
335373
return ''
@@ -340,8 +378,8 @@ def magic_impl(magic_key, magic_body):
340378
return f" def {magic_key}:\n{b}"
341379

342380

343-
# One of the combinations is going to be all empty implementations
344-
magic_combinations = [x for x in itertools.product(*[all_magic_impls(key) for key in MAGIC.keys()])]
381+
def magic_combinations_choose_random():
382+
return list([magic_impl(key, impls[rand.randint(0, len(impls)-1)]) for (key, impls) in MAGIC.items()])
345383

346384

347385
def managed_class_impl(name, bases, magic_impls):
@@ -355,11 +393,6 @@ def managed_class_impl(name, bases, magic_impls):
355393
return code
356394

357395

358-
def all_slot_impls(slot:Slot):
359-
for body in slot.impls:
360-
yield (slot, body)
361-
362-
363396
def native_heap_type_impl(name, mod_name, slots):
364397
slot_defs = '\n'.join([s.impl(name, body) for (s, body) in slots if body])
365398
slot_decls = ',\n'.join([s.tp_decl(name) for (s, body) in slots if body] + ["{0}"])
@@ -436,20 +469,26 @@ def native_static_type_impl(name, mod_name, slots):
436469
''')
437470

438471

439-
slot_combinations = [x for x in itertools.product(*[all_slot_impls(slot) for slot in SLOTS])]
472+
def slot_combinations_choose_random():
473+
return list([(slot, slot.impls[rand.randint(0, len(slot.impls) - 1)]) for slot in SLOTS])
474+
475+
476+
def choose_random(l):
477+
return l[rand.randint(0, len(l)-1)]
478+
440479

441480
parser = ArgumentParser()
442481
parser.add_argument('--graalpy', dest='graalpy', required=True)
443482
parser.add_argument('--cpython', dest='cpython', required=True)
444483
parser.add_argument('--iterations', type=int, default=200)
445-
parser.add_argument('--output-dir', dest='output_dir', required=True,
446-
help='where to store generated test cases and logs from test runs')
484+
parser.add_argument('--output-dir', dest='output_dir',
485+
help='Where to store generated test cases and logs from test runs. If not provided the program uses "experiment-{timestamp}"')
447486
parser.add_argument('--seed', type=int, default=0)
448487
args, _ = parser.parse_known_args()
449488

450489
graalpy = args.graalpy
451490
cpython = args.cpython
452-
output_dir = args.output_dir
491+
output_dir = args.output_dir if args.output_dir else f'./experiment-{int(time.time())}'
453492

454493
if os.path.exists(output_dir):
455494
if not os.path.isdir(output_dir):
@@ -478,12 +517,6 @@ def log(*args, **kwargs):
478517
log()
479518
rand = random.Random(seed)
480519

481-
482-
def choose_random(l):
483-
index = rand.randint(0, len(l)-1)
484-
return l[index]
485-
486-
487520
for test_case_idx in range(args.iterations):
488521
classes_count = max(3, rand.randint(1, 5)) # Make it more likely that it's 3...
489522
classes = []
@@ -497,14 +530,14 @@ def choose_random(l):
497530
class_name = 'Native' + str(i)
498531
native_classes.append(class_name)
499532
if i == 0 and rand.randint(0, 2) < 2:
500-
c_source += native_static_type_impl(class_name, test_module_name, choose_random(slot_combinations))
533+
c_source += native_static_type_impl(class_name, test_module_name, slot_combinations_choose_random())
501534
else:
502-
c_source += native_heap_type_impl(class_name, test_module_name, choose_random(slot_combinations))
535+
c_source += native_heap_type_impl(class_name, test_module_name, slot_combinations_choose_random())
503536
c_source += '\n'
504537
py_source += f"{class_name} = {test_module_name}.create_{class_name}(({base}, ))\n"
505538
else:
506539
class_name = 'Managed' + str(i)
507-
py_source += managed_class_impl(class_name, (base,), choose_random(magic_combinations))
540+
py_source += managed_class_impl(class_name, (base,), magic_combinations_choose_random())
508541
py_source += '\n'
509542

510543
classes.append(class_name)
@@ -550,26 +583,28 @@ def choose_random(l):
550583
write_all(os.path.join(output_dir, py_filename), py_source)
551584

552585
log(f"Test case {test_case_idx:5}: ", end='')
586+
cpy_output_path = os.path.join(output_dir, f"cpython{test_case_idx}.out")
553587
try:
554588
cpython_out = subprocess.check_output([cpython, py_filename], stderr=subprocess.STDOUT, cwd=output_dir).decode()
555-
write_all(os.path.join(output_dir, f"cpython{test_case_idx}.out"), cpython_out)
589+
write_all(cpy_output_path, cpython_out)
556590
except subprocess.CalledProcessError as e:
557591
output = e.output.decode()
558-
log("CPython error; ⚠️")
559-
write_all(os.path.join(output_dir, f"cpython{test_case_idx}.out"), output)
592+
log(f"CPython error; ⚠️ {os.path.abspath(cpy_output_path)}")
593+
write_all(cpy_output_path, output)
560594
continue
561595

562596
log("CPython succeeded; ", end='')
597+
gpy_output_path = os.path.join(output_dir, f"graalpy{test_case_idx}.out")
563598
try:
564599
graalpy_out = subprocess.check_output([graalpy, '--vm.ea', '--vm.esa', py_filename], stderr=subprocess.STDOUT, cwd=output_dir).decode()
565-
write_all(os.path.join(output_dir, f"graalpy{test_case_idx}.out"), graalpy_out)
600+
write_all(gpy_output_path, graalpy_out)
566601
except subprocess.CalledProcessError as e:
567602
output = e.output.decode()
568-
log("❌ fatal error in GraalPy")
569-
write_all(os.path.join(output_dir, f"graalpy{test_case_idx}.out"), output)
603+
log(f"❌ fatal error in GraalPy {os.path.abspath(gpy_output_path)}")
604+
write_all(gpy_output_path, output)
570605
continue
571606

572607
if cpython_out != graalpy_out:
573-
log(f"❌ output does not match!")
608+
log(f"❌ output does not match! {os.path.abspath(cpy_output_path)} {os.path.abspath(gpy_output_path)}")
574609
else:
575610
log("✅ GraalPy succeeded")

0 commit comments

Comments
 (0)