37
37
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38
38
# SOFTWARE.
39
39
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:
41
46
#
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
45
48
#
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.
48
51
#
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
51
60
#
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
+
53
68
import os .path
54
69
import sys
55
70
import dataclasses
56
71
import random
57
72
import itertools
58
73
import subprocess
59
74
import textwrap
75
+ import time
60
76
from argparse import ArgumentParser
61
77
from collections import defaultdict
62
78
67
83
import traceback
68
84
import operator
69
85
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
+
70
89
def test(fun, name):
71
90
try:
72
91
print(f'{name}:', end='')
73
92
result = repr(fun())
74
- result = re.sub(r'object at 0x[0-9a-fA-F]+', '', result)
93
+ result = normalize_output( result)
75
94
print(result)
76
95
except Exception as e:
77
96
if '--verbose' in sys.argv or '-v' in sys.argv:
@@ -81,9 +100,9 @@ def test(fun, name):
81
100
82
101
def test_dunder(obj, fun_name, *args):
83
102
# 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} ")
87
106
88
107
def write_attr(obj, attr, value):
89
108
if attr == 'foo':
@@ -152,18 +171,32 @@ def del_descr(self): del self.descr
152
171
other_objs = [K() for K in other_klasses] + [42, 3.14, 'string', (1,2,3)]
153
172
for obj2 in other_objs:
154
173
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")
167
200
'''
168
201
169
202
# language=Python
@@ -285,7 +318,15 @@ def tp_decl(self, name_prefix):
285
318
global_stash2 = value;
286
319
return 0;
287
320
''' ]),
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 )]]),
289
330
]
290
331
291
332
PY_GLOBALS = '''
@@ -321,15 +362,12 @@ def tp_decl(self, name_prefix):
321
362
return None
322
363
''' ],
323
364
'__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__' ]}
325
368
}
326
369
327
370
328
- def all_magic_impls (key ):
329
- for body in MAGIC [key ]:
330
- yield magic_impl (key , body )
331
-
332
-
333
371
def magic_impl (magic_key , magic_body ):
334
372
if magic_body is None :
335
373
return ''
@@ -340,8 +378,8 @@ def magic_impl(magic_key, magic_body):
340
378
return f" def { magic_key } :\n { b } "
341
379
342
380
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 ()])
345
383
346
384
347
385
def managed_class_impl (name , bases , magic_impls ):
@@ -355,11 +393,6 @@ def managed_class_impl(name, bases, magic_impls):
355
393
return code
356
394
357
395
358
- def all_slot_impls (slot :Slot ):
359
- for body in slot .impls :
360
- yield (slot , body )
361
-
362
-
363
396
def native_heap_type_impl (name , mod_name , slots ):
364
397
slot_defs = '\n ' .join ([s .impl (name , body ) for (s , body ) in slots if body ])
365
398
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):
436
469
''' )
437
470
438
471
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
+
440
479
441
480
parser = ArgumentParser ()
442
481
parser .add_argument ('--graalpy' , dest = 'graalpy' , required = True )
443
482
parser .add_argument ('--cpython' , dest = 'cpython' , required = True )
444
483
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}" ' )
447
486
parser .add_argument ('--seed' , type = int , default = 0 )
448
487
args , _ = parser .parse_known_args ()
449
488
450
489
graalpy = args .graalpy
451
490
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 ()) } '
453
492
454
493
if os .path .exists (output_dir ):
455
494
if not os .path .isdir (output_dir ):
@@ -478,12 +517,6 @@ def log(*args, **kwargs):
478
517
log ()
479
518
rand = random .Random (seed )
480
519
481
-
482
- def choose_random (l ):
483
- index = rand .randint (0 , len (l )- 1 )
484
- return l [index ]
485
-
486
-
487
520
for test_case_idx in range (args .iterations ):
488
521
classes_count = max (3 , rand .randint (1 , 5 )) # Make it more likely that it's 3...
489
522
classes = []
@@ -497,14 +530,14 @@ def choose_random(l):
497
530
class_name = 'Native' + str (i )
498
531
native_classes .append (class_name )
499
532
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 ( ))
501
534
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 ( ))
503
536
c_source += '\n '
504
537
py_source += f"{ class_name } = { test_module_name } .create_{ class_name } (({ base } , ))\n "
505
538
else :
506
539
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 ( ))
508
541
py_source += '\n '
509
542
510
543
classes .append (class_name )
@@ -550,26 +583,28 @@ def choose_random(l):
550
583
write_all (os .path .join (output_dir , py_filename ), py_source )
551
584
552
585
log (f"Test case { test_case_idx :5} : " , end = '' )
586
+ cpy_output_path = os .path .join (output_dir , f"cpython{ test_case_idx } .out" )
553
587
try :
554
588
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 )
556
590
except subprocess .CalledProcessError as e :
557
591
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 )
560
594
continue
561
595
562
596
log ("CPython succeeded; " , end = '' )
597
+ gpy_output_path = os .path .join (output_dir , f"graalpy{ test_case_idx } .out" )
563
598
try :
564
599
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 )
566
601
except subprocess .CalledProcessError as e :
567
602
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 )
570
605
continue
571
606
572
607
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 ) } " )
574
609
else :
575
610
log ("✅ GraalPy succeeded" )
0 commit comments