diff --git a/fusil/python/write_python_code.py b/fusil/python/write_python_code.py index 66d01bb..22c7c3e 100644 --- a/fusil/python/write_python_code.py +++ b/fusil/python/write_python_code.py @@ -2320,6 +2320,14 @@ def _dispatch_fuzz_on_instance( ) if level is not None: L_else_generic = level + # Without a plugin dispatcher no `else:` was ever opened, so the generic fallback + # ran even for an object the skip-trivial guard had just reported as skipped -- the + # guard printed and nothing else. Open the else ourselves in that case so the + # fallback is genuinely guarded; with a dispatcher, its own else still owns it. + if L_else_generic is None: + self.write(0, "else:") + L_else_generic = self.addLevel(1) + self.write(0, "pass") try: self.write(0, "try:") self.write_print_to_stderr( @@ -2443,12 +2451,22 @@ def _fuzz_methods_on_object_or_specific_types( f'f"--- Fuzzing instance: {target_obj_expr_str} (type hint: {target_obj_class_name}, prefix: {current_prefix}) ---"', ) + # The guard has to OWN the method fuzzing below: for four fleets it emitted only the + # "Skipping" print and then fuzzed the object anyway, so a cffi FFI object reached as a + # module attribute (PyPy's _curses_panel.ffi, _ssl.ffi, ...) still had ffi.memmove() and + # friends called on it -- raw-pointer entry points that fault by contract on whatever + # arguments they are handed. Hence the else: the print alone was never a skip. self.write(0, f"if skip_trivial_type({target_obj_expr_str}):") with self.indented(): self.write_print_to_stderr( 0, f"f'Skipping deep diving on {target_obj_expr_str} {{type({target_obj_expr_str})}}'", ) + self.write(0, "else:") + skip_guard_level = self.addLevel(1) + # Keeps the block syntactically valid when the object turns out to have no methods to + # fuzz (methods_dict empty), which would otherwise leave `else:` with only a comment. + self.write(0, "pass") # General method fuzzing for other types self.write(0, f"# General method fuzzing for {target_obj_expr_str}") @@ -2481,6 +2499,9 @@ def _fuzz_methods_on_object_or_specific_types( is_method_call=True, generation_depth=0, ) + # Close the `else:` opened by the skip-trivial guard above; the summary print belongs + # outside it so a skipped object still reports. + self.restoreLevel(skip_guard_level) self.write_print_to_stderr( 0, f'f"--- Finished fuzzing instance: {target_obj_expr_str} ---"' ) diff --git a/tests/python/golden/fakemod_seed1234.py b/tests/python/golden/fakemod_seed1234.py index a1f4520..6f60400 100644 --- a/tests/python/golden/fakemod_seed1234.py +++ b/tests/python/golden/fakemod_seed1234.py @@ -1456,50 +1456,54 @@ def callFunc(prefix, func_name_str, *arguments, verbose=True): except Exception as e: print(f"--- (Depth 0) Error calling repr() prefix: c1_widget_ops) ---", file=stderr) if instance_c1_widget is not None: - if skip_trivial_type(instance_c1_widget): - print(f'Skipping deep diving on instance_c1_widget {type(instance_c1_widget)}', file=stderr) - try: - print(f'Instance {instance_c1_widget!r} (actual type {type(instance_c1_widget).__name__}) has no specific fuzzer type, doing generic calls.', file=stderr) - except Exception as e: - print(f'Error printing instance repr() { e } (actual type {type(instance_c1_widget).__name__}) has no specific fuzzer type, doing generic calls.', file=stderr) if skip_trivial_type(instance_c1_widget): print(f'Skipping deep diving on instance_c1_widget {type(instance_c1_widget)}', file=stderr) else: - print(f'Instance instance_c1_widget (type {type(instance_c1_widget).__name__}) has no specific fuzzer, doing generic calls.', file=stderr) - c1_widget_ops_generic_methods = [] + pass try: - for c1_widget_ops_generic_attr_name in dir(instance_c1_widget): - if c1_widget_ops_generic_attr_name.startswith('_'): continue - try: - c1_widget_ops_generic_attr_val = getattr(instance_c1_widget, c1_widget_ops_generic_attr_name) - if callable(c1_widget_ops_generic_attr_val) and c1_widget_ops_generic_attr_name not in _FUSIL_METHOD_BLACKLIST: c1_widget_ops_generic_methods.append((c1_widget_ops_generic_attr_name, c1_widget_ops_generic_attr_val)) - except Exception: pass - except Exception: c1_widget_ops_generic_methods = [] # Failed to get methods - if c1_widget_ops_generic_methods: - print(f'Found {len(c1_widget_ops_generic_methods)} callable methods for generic fuzzing of instance_c1_widget', file=stderr) - for _i_c1_widget_ops_generic in range(min(len(c1_widget_ops_generic_methods), 2)): - c1_widget_ops_generic_method_name_to_call, c1_widget_ops_generic_method_obj_to_call = choice(c1_widget_ops_generic_methods) - # Conceptual call to generic method fuzzer - if c1_widget_ops_generic_method_name_to_call not in _FUSIL_METHOD_BLACKLIST: callMethod(f'c1_widget_ops_generic_gen{_i_c1_widget_ops_generic}', instance_c1_widget, c1_widget_ops_generic_method_name_to_call) + print(f'Instance {instance_c1_widget!r} (actual type {type(instance_c1_widget).__name__}) has no specific fuzzer type, doing generic calls.', file=stderr) + except Exception as e: + print(f'Error printing instance repr() { e } (actual type {type(instance_c1_widget).__name__}) has no specific fuzzer type, doing generic calls.', file=stderr) + if skip_trivial_type(instance_c1_widget): + print(f'Skipping deep diving on instance_c1_widget {type(instance_c1_widget)}', file=stderr) + else: + print(f'Instance instance_c1_widget (type {type(instance_c1_widget).__name__}) has no specific fuzzer, doing generic calls.', file=stderr) + c1_widget_ops_generic_methods = [] + try: + for c1_widget_ops_generic_attr_name in dir(instance_c1_widget): + if c1_widget_ops_generic_attr_name.startswith('_'): continue + try: + c1_widget_ops_generic_attr_val = getattr(instance_c1_widget, c1_widget_ops_generic_attr_name) + if callable(c1_widget_ops_generic_attr_val) and c1_widget_ops_generic_attr_name not in _FUSIL_METHOD_BLACKLIST: c1_widget_ops_generic_methods.append((c1_widget_ops_generic_attr_name, c1_widget_ops_generic_attr_val)) + except Exception: pass + except Exception: c1_widget_ops_generic_methods = [] # Failed to get methods + if c1_widget_ops_generic_methods: + print(f'Found {len(c1_widget_ops_generic_methods)} callable methods for generic fuzzing of instance_c1_widget', file=stderr) + for _i_c1_widget_ops_generic in range(min(len(c1_widget_ops_generic_methods), 2)): + c1_widget_ops_generic_method_name_to_call, c1_widget_ops_generic_method_obj_to_call = choice(c1_widget_ops_generic_methods) + # Conceptual call to generic method fuzzer + if c1_widget_ops_generic_method_name_to_call not in _FUSIL_METHOD_BLACKLIST: callMethod(f'c1_widget_ops_generic_gen{_i_c1_widget_ops_generic}', instance_c1_widget, c1_widget_ops_generic_method_name_to_call) if instance_c1_widget is not None and instance_c1_widget is not SENTINEL_VALUE: print(f"--- Fuzzing instance: instance_c1_widget (type hint: Widget, prefix: c1m) ---", file=stderr) if skip_trivial_type(instance_c1_widget): print(f'Skipping deep diving on instance_c1_widget {type(instance_c1_widget)}', file=stderr) - # General method fuzzing for instance_c1_widget - try: - res_c1m1 = callMethod("c1m1", instance_c1_widget, "method_two", - list[weird_classes['weird_OrderedDict']] | weird_classes['weird_set'] | big_union, - verbose=True) - except Exception as _argexc_c1m1: - print("[c1m1] call skipped (argument build failed):", repr(_argexc_c1m1), file=stderr) + else: + pass + # General method fuzzing for instance_c1_widget + try: + res_c1m1 = callMethod("c1m1", instance_c1_widget, "method_two", + list[weird_classes['weird_OrderedDict']] | weird_classes['weird_set'] | big_union, + verbose=True) + except Exception as _argexc_c1m1: + print("[c1m1] call skipped (argument build failed):", repr(_argexc_c1m1), file=stderr) - try: - res_c1m2 = callMethod("c1m2", instance_c1_widget, "method_one", - verbose=True) - except Exception as _argexc_c1m2: - print("[c1m2] call skipped (argument build failed):", repr(_argexc_c1m2), file=stderr) + try: + res_c1m2 = callMethod("c1m2", instance_c1_widget, "method_one", + verbose=True) + except Exception as _argexc_c1m2: + print("[c1m2] call skipped (argument build failed):", repr(_argexc_c1m2), file=stderr) print(f"--- Finished fuzzing instance: instance_c1_widget ---", file=stderr) diff --git a/tests/python/test_golden_output.py b/tests/python/test_golden_output.py index b1a26b9..ec36c92 100644 --- a/tests/python/test_golden_output.py +++ b/tests/python/test_golden_output.py @@ -236,6 +236,65 @@ class ffi: # noqa: N801 - deliberately named like the cffi attribute self.assertFalse(skip(ffi())) +class SkipTrivialTypeGuardOwnsTheBlockTests(unittest.TestCase): + """The skip-trivial guard must OWN the fuzzing it claims to skip. + + For four consecutive PyPy fleets the guard emitted only its "Skipping deep diving" print + and then fuzzed the object anyway. That is how ``_curses_panel.ffi`` -- a cffi ``FFI`` + object handed out as a module attribute, whose type IS defined in ``_cffi_backend`` and + which ``skip_trivial_type`` therefore correctly identified -- still had ``ffi.memmove()`` + called on it and took the process down: a raw-pointer entry point that faults by contract + on whatever arguments it is given, so never a target defect. + + Asserting on the emitted text is not enough (the print was always there); assert on the + parsed structure instead -- every ``if skip_trivial_type(x):`` must carry the real work in + its ``orelse``, never after the statement. + """ + + def _guards(self, tree): + found = [] + for node in ast.walk(tree): + if not isinstance(node, ast.If): + continue + test = node.test + if ( + isinstance(test, ast.Call) + and isinstance(test.func, ast.Name) + and test.func.id == "skip_trivial_type" + ): + found.append(node) + return found + + def test_every_guard_has_an_else_branch(self): + guards = self._guards(ast.parse(generate())) + self.assertTrue(guards, "no skip_trivial_type guard emitted at all") + for node in guards: + self.assertTrue( + node.orelse, + "skip_trivial_type guard at line %d has no else: it prints 'Skipping' and " + "then falls through to the fuzzing it claims to skip" % node.lineno, + ) + + def test_guard_body_only_reports_and_never_calls(self): + """The taken branch must not itself call the object -- it is the skip path.""" + for node in self._guards(ast.parse(generate())): + for sub in ast.walk(ast.Module(body=node.body, type_ignores=[])): + if isinstance(sub, ast.Call) and isinstance(sub.func, ast.Name): + self.assertIn( + sub.func.id, + {"print", "type", "repr"}, + "the skip branch calls %s(); it must only report" % sub.func.id, + ) + + def test_emitted_guard_skips_a_cffi_typed_object(self): + """Exec the emitted function against an object typed like PyPy's ``ffi``.""" + skip = SkipTrivialTypeTests()._emitted_skip_trivial_type() + cffi_like = type("FFI", (), {}) + cffi_like.__module__ = "_cffi_backend" + self.assertTrue(skip(cffi_like()), "a _cffi_backend-typed instance must be skipped") + self.assertFalse(skip(object()), "an ordinary object must NOT be skipped") + + class SelfNoiseVocabularyTests(unittest.TestCase): """Emitted COMMENTS must not contain fusil's own crash vocabulary.