# SPDX-License-Identifier: GPL-2.0-or-later # semi-useful script, runs all operators in a number of different # contexts, cheap way to find misc small bugs but is in no way a complete test. # # only error checked for here is a segfault. import bpy import sys USE_ATTRSET = False USE_FILES = "" # "/mango/" USE_RANDOM = False USE_RANDOM_SCREEN = False RANDOM_SEED = [1] # so we can redo crashes RANDOM_RESET = 0.1 # 10% chance of resetting on each new operator RANDOM_MULTIPLY = 10 STATE = { "counter": 0, } op_blacklist = ( "script.reload", "export*.*", "import*.*", "*.save_*", "*.read_*", "*.open_*", "*.link_append", "render.render", "render.play_rendered_anim", "sound.bake_animation", # OK but slow "sound.mixdown", # OK but slow "object.bake_image", # OK but slow "object.paths_calculate", # OK but slow "object.paths_update", # OK but slow "ptcache.bake_all", # OK but slow "nla.bake", # OK but slow "*.*_export", "*.*_import", "ed.undo", "ed.undo_push", "preferences.studiolight_new", "script.autoexec_warn_clear", "screen.delete", # already used for random screens "wm.blenderplayer_start", "wm.recover_auto_save", "wm.quit_blender", "wm.window_close", "wm.url_open", "wm.doc_view", "wm.doc_edit", "wm.doc_view_manual", "wm.path_open", "wm.copy_prev_settings", "wm.theme_install", "wm.context_*", "wm.properties_add", "wm.properties_remove", "wm.properties_edit", "wm.properties_context_change", "wm.operator_cheat_sheet", "wm.interface_theme_*", "wm.previews_ensure", # slow - but harmless "wm.keyitem_add", # just annoying - but harmless "wm.keyconfig_activate", # just annoying - but harmless "wm.keyconfig_preset_add", # just annoying - but harmless "wm.keyconfig_test", # just annoying - but harmless "wm.memory_statistics", # another annoying one "wm.dependency_relations", # another annoying one "wm.keymap_restore", # another annoying one "wm.addon_*", # harmless, but don't change state "console.*", # just annoying - but harmless "wm.url_open_preset", # Annoying but harmless (opens web pages). # FIXME: # Crashes with non-trivial fixes. # # Expects undo stack. "object.voxel_remesh", "mesh.paint_mask_slice", "paint.mask_flood_fill", "sculpt.mask_from_cavity", # TODO: use empty temp dir to avoid behavior depending on local setup. "view3d.pastebuffer", # Needs active window. "scene.new", ) def blend_list(mainpath): import os from os.path import join, splitext def file_list(path, filename_check=None): for dirpath, dirnames, filenames in os.walk(path): # skip '.git' dirnames[:] = [d for d in dirnames if not d.startswith(".")] for filename in filenames: filepath = join(dirpath, filename) if filename_check is None or filename_check(filepath): yield filepath def is_blend(filename): ext = splitext(filename)[1] return (ext in {".blend", }) return list(sorted(file_list(mainpath, is_blend))) if USE_FILES: USE_FILES_LS = blend_list(USE_FILES) # print(USE_FILES_LS) def filter_op_list(operators): from fnmatch import fnmatchcase def is_op_ok(op): for op_match in op_blacklist: if fnmatchcase(op, op_match): print(" skipping: %s (%s)" % (op, op_match)) return False return True operators[:] = [op for op in operators if is_op_ok(op[0])] def reset_blend(): bpy.ops.wm.read_factory_settings() for scene in bpy.data.scenes: # reduce range so any bake action doesn't take too long scene.frame_start = 1 scene.frame_end = 5 if USE_RANDOM_SCREEN: import random for _ in range(random.randint(0, len(bpy.data.screens))): bpy.ops.screen.delete() print("Scree IS", bpy.context.screen) def reset_file(): import random f = USE_FILES_LS[random.randint(0, len(USE_FILES_LS) - 1)] bpy.ops.wm.open_mainfile(filepath=f) if USE_ATTRSET: def build_property_typemap(skip_classes): property_typemap = {} for attr in dir(bpy.types): cls = getattr(bpy.types, attr) if issubclass(cls, skip_classes): continue # # to support skip-save we can't get all props # properties = cls.bl_rna.properties.keys() properties = [] for prop_id, prop in cls.bl_rna.properties.items(): if not prop.is_skip_save: properties.append(prop_id) properties.remove("rna_type") property_typemap[attr] = properties return property_typemap CLS_BLACKLIST = ( bpy.types.BrushTextureSlot, bpy.types.Brush, ) property_typemap = build_property_typemap(CLS_BLACKLIST) bpy_struct_type = bpy.types.Struct.__base__ def id_walk(value, parent): value_type = type(value) value_type_name = value_type.__name__ value_id = getattr(value, "id_data", Ellipsis) value_props = property_typemap.get(value_type_name, ()) for prop in value_props: subvalue = getattr(value, prop) if subvalue == parent: continue # grr, recursive! if prop == "point_caches": continue subvalue_type = type(subvalue) yield value, prop, subvalue_type subvalue_id = getattr(subvalue, "id_data", Ellipsis) if value_id == subvalue_id: if subvalue_type == float: pass elif subvalue_type == int: pass elif subvalue_type == bool: pass elif subvalue_type == str: pass elif hasattr(subvalue, "__len__"): for sub_item in subvalue[:]: if isinstance(sub_item, bpy_struct_type): subitem_id = getattr(sub_item, "id_data", Ellipsis) if subitem_id == subvalue_id: yield from id_walk(sub_item, value) if subvalue_type.__name__ in property_typemap: yield from id_walk(subvalue, value) # main function _random_values = ( None, object, type, 1, 0.1, -1, # float("nan"), "", "test", b"", b"test", (), [], {}, (10,), (10, 20), (0, 0, 0), {0: "", 1: "hello", 2: "test"}, {"": 0, "hello": 1, "test": 2}, set(), {"", "test", "."}, {None, ..., type}, range(10), (" " * i for i in range(10)), ) def attrset_data(): for attr in dir(bpy.data): if attr == "window_managers": continue seq = getattr(bpy.data, attr) if seq.__class__.__name__ == 'bpy_prop_collection': for id_data in seq: for val, prop, _tp in id_walk(id_data, bpy.data): # print(id_data) for val_rnd in _random_values: try: setattr(val, prop, val_rnd) except: pass def run_ops(operators, setup_func=None, reset=True): print("\ncontext:", setup_func.__name__) # first invoke for op_id, op in operators: if op.poll(): print(" operator: %4d, %s" % (STATE["counter"], op_id)) STATE["counter"] += 1 sys.stdout.flush() # in case of crash # disable will get blender in a bad state and crash easy! if reset: reset_test = True if USE_RANDOM: import random if random.random() < (1.0 - RANDOM_RESET): reset_test = False if reset_test: if USE_FILES: reset_file() else: reset_blend() del reset_test if USE_RANDOM: # we can't be sure it will work try: setup_func() except: pass else: setup_func() for mode in {'EXEC_DEFAULT', 'INVOKE_DEFAULT'}: try: op(mode) except: # import traceback # traceback.print_exc() pass if USE_ATTRSET: attrset_data() if not operators: # run test if reset: reset_blend() if USE_RANDOM: # we can't be sure it will work try: setup_func() except: pass else: setup_func() # contexts def ctx_clear_scene(): # copied from batch_import.py bpy.ops.wm.read_factory_settings(use_empty=True) def ctx_editmode_mesh(): bpy.ops.object.mode_set(mode='EDIT') def ctx_editmode_mesh_extra(): bpy.ops.object.vertex_group_add() bpy.ops.object.shape_key_add(from_mix=False) bpy.ops.object.shape_key_add(from_mix=True) bpy.ops.mesh.uv_texture_add() bpy.ops.object.material_slot_add() # editmode last! bpy.ops.object.mode_set(mode='EDIT') def ctx_editmode_mesh_empty(): bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.delete() def ctx_editmode_curves(): bpy.ops.curve.primitive_nurbs_circle_add() bpy.ops.object.mode_set(mode='EDIT') def ctx_editmode_curves_empty(): bpy.ops.curve.primitive_nurbs_circle_add() bpy.ops.object.mode_set(mode='EDIT') bpy.ops.curve.select_all(action='SELECT') bpy.ops.curve.delete(type='VERT') def ctx_editmode_surface(): bpy.ops.surface.primitive_nurbs_surface_torus_add() bpy.ops.object.mode_set(mode='EDIT') def ctx_editmode_mball(): bpy.ops.object.metaball_add() bpy.ops.object.mode_set(mode='EDIT') def ctx_editmode_text(): bpy.ops.object.text_add() bpy.ops.object.mode_set(mode='EDIT') def ctx_editmode_armature(): bpy.ops.object.armature_add() bpy.ops.object.mode_set(mode='EDIT') def ctx_editmode_armature_empty(): bpy.ops.object.armature_add() bpy.ops.object.mode_set(mode='EDIT') bpy.ops.armature.select_all(action='SELECT') bpy.ops.armature.delete() def ctx_editmode_lattice(): bpy.ops.object.add(type='LATTICE') bpy.ops.object.mode_set(mode='EDIT') # bpy.ops.object.vertex_group_add() def ctx_object_empty(): bpy.ops.object.add(type='EMPTY') def ctx_object_pose(): bpy.ops.object.armature_add() bpy.ops.object.mode_set(mode='POSE') bpy.ops.pose.select_all(action='SELECT') def ctx_object_volume(): bpy.ops.object.add(type='VOLUME') def ctx_object_paint_weight(): bpy.ops.object.mode_set(mode='WEIGHT_PAINT') def ctx_object_paint_vertex(): bpy.ops.object.mode_set(mode='VERTEX_PAINT') def ctx_object_paint_sculpt(): bpy.ops.object.mode_set(mode='SCULPT') def ctx_object_paint_texture(): bpy.ops.object.mode_set(mode='TEXTURE_PAINT') def bpy_check_type_duplicates(): # non essential sanity check bl_types = dir(bpy.types) bl_types_unique = set(bl_types) if len(bl_types) != len(bl_types_unique): print("Error, found duplicates in 'bpy.types'") for t in sorted(bl_types_unique): tot = bl_types.count(t) if tot > 1: print(" '%s', %d" % (t, tot)) import sys sys.exit(1) def main(): bpy_check_type_duplicates() # reset_blend() import bpy operators = [] for mod_name in dir(bpy.ops): mod = getattr(bpy.ops, mod_name) for submod_name in dir(mod): op = getattr(mod, submod_name) operators.append(("%s.%s" % (mod_name, submod_name), op)) operators.sort(key=lambda op: op[0]) filter_op_list(operators) # for testing, mix the list up. # operators.reverse() if USE_RANDOM: import random random.seed(RANDOM_SEED[0]) operators = operators * RANDOM_MULTIPLY random.shuffle(operators) # 2 passes, first just run setup_func to make sure they are ok for operators_test in ((), operators): # Run the operator tests in different contexts run_ops(operators_test, setup_func=lambda: None) if USE_FILES: continue run_ops(operators_test, setup_func=ctx_clear_scene) # object modes run_ops(operators_test, setup_func=ctx_object_empty) run_ops(operators_test, setup_func=ctx_object_pose) run_ops(operators_test, setup_func=ctx_object_paint_weight) run_ops(operators_test, setup_func=ctx_object_paint_vertex) run_ops(operators_test, setup_func=ctx_object_paint_sculpt) run_ops(operators_test, setup_func=ctx_object_paint_texture) # mesh run_ops(operators_test, setup_func=ctx_editmode_mesh) run_ops(operators_test, setup_func=ctx_editmode_mesh_extra) run_ops(operators_test, setup_func=ctx_editmode_mesh_empty) # armature run_ops(operators_test, setup_func=ctx_editmode_armature) run_ops(operators_test, setup_func=ctx_editmode_armature_empty) # curves run_ops(operators_test, setup_func=ctx_editmode_curves) run_ops(operators_test, setup_func=ctx_editmode_curves_empty) run_ops(operators_test, setup_func=ctx_editmode_surface) # other run_ops(operators_test, setup_func=ctx_editmode_mball) run_ops(operators_test, setup_func=ctx_editmode_text) run_ops(operators_test, setup_func=ctx_editmode_lattice) run_ops(operators_test, setup_func=ctx_object_volume) if not operators_test: print("All setup functions run fine!") print("Finished %r" % __file__) if __name__ == "__main__": # for i in range(200): # RANDOM_SEED[0] += 1 # main() main()