Merge branch 'master' into blender2.8
[blender.git] / tests / python / bl_run_operators.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8 compliant>
20
21 # semi-useful script, runs all operators in a number of different
22 # contexts, cheap way to find misc small bugs but is in no way a complete test.
23 #
24 # only error checked for here is a segfault.
25
26 import bpy
27 import sys
28
29 USE_ATTRSET = False
30 USE_FILES = ""  # "/mango/"
31 USE_RANDOM = False
32 USE_RANDOM_SCREEN = False
33 RANDOM_SEED = [1]  # so we can redo crashes
34 RANDOM_RESET = 0.1  # 10% chance of resetting on each new operator
35 RANDOM_MULTIPLY = 10
36
37 STATE = {
38     "counter": 0,
39 }
40
41
42 op_blacklist = (
43     "script.reload",
44     "export*.*",
45     "import*.*",
46     "*.save_*",
47     "*.read_*",
48     "*.open_*",
49     "*.link_append",
50     "render.render",
51     "render.play_rendered_anim",
52     "sound.bake_animation",    # OK but slow
53     "sound.mixdown",           # OK but slow
54     "object.bake_image",       # OK but slow
55     "object.paths_calculate",  # OK but slow
56     "object.paths_update",     # OK but slow
57     "ptcache.bake_all",        # OK but slow
58     "nla.bake",                # OK but slow
59     "*.*_export",
60     "*.*_import",
61     "ed.undo",
62     "ed.undo_push",
63     "script.autoexec_warn_clear",
64     "screen.delete",           # already used for random screens
65     "wm.blenderplayer_start",
66     "wm.recover_auto_save",
67     "wm.quit_blender",
68     "wm.window_close",
69     "wm.url_open",
70     "wm.doc_view",
71     "wm.doc_edit",
72     "wm.doc_view_manual",
73     "wm.path_open",
74     "wm.copy_prev_settings",
75     "wm.theme_install",
76     "wm.context_*",
77     "wm.properties_add",
78     "wm.properties_remove",
79     "wm.properties_edit",
80     "wm.properties_context_change",
81     "wm.operator_cheat_sheet",
82     "wm.interface_theme_*",
83     "wm.previews_ensure",       # slow - but harmless
84     "wm.keyitem_add",           # just annoying - but harmless
85     "wm.keyconfig_activate",    # just annoying - but harmless
86     "wm.keyconfig_preset_add",  # just annoying - but harmless
87     "wm.keyconfig_test",        # just annoying - but harmless
88     "wm.memory_statistics",     # another annoying one
89     "wm.dependency_relations",  # another annoying one
90     "wm.keymap_restore",        # another annoying one
91     "wm.addon_*",               # harmless, but dont change state
92     "console.*",                # just annoying - but harmless
93 )
94
95
96 def blend_list(mainpath):
97     import os
98     from os.path import join, splitext
99
100     def file_list(path, filename_check=None):
101         for dirpath, dirnames, filenames in os.walk(path):
102             # skip '.git'
103             dirnames[:] = [d for d in dirnames if not d.startswith(".")]
104
105             for filename in filenames:
106                 filepath = join(dirpath, filename)
107                 if filename_check is None or filename_check(filepath):
108                     yield filepath
109
110     def is_blend(filename):
111         ext = splitext(filename)[1]
112         return (ext in {".blend", })
113
114     return list(sorted(file_list(mainpath, is_blend)))
115
116
117 if USE_FILES:
118     USE_FILES_LS = blend_list(USE_FILES)
119     # print(USE_FILES_LS)
120
121
122 def filter_op_list(operators):
123     from fnmatch import fnmatchcase
124
125     def is_op_ok(op):
126         for op_match in op_blacklist:
127             if fnmatchcase(op, op_match):
128                 print("    skipping: %s (%s)" % (op, op_match))
129                 return False
130         return True
131
132     operators[:] = [op for op in operators if is_op_ok(op[0])]
133
134
135 def reset_blend():
136     bpy.ops.wm.read_factory_settings()
137     for scene in bpy.data.scenes:
138         # reduce range so any bake action doesn't take too long
139         scene.frame_start = 1
140         scene.frame_end = 5
141
142     if USE_RANDOM_SCREEN:
143         import random
144         for _ in range(random.randint(0, len(bpy.data.screens))):
145             bpy.ops.screen.delete()
146         print("Scree IS", bpy.context.screen)
147
148
149 def reset_file():
150     import random
151     f = USE_FILES_LS[random.randint(0, len(USE_FILES_LS) - 1)]
152     bpy.ops.wm.open_mainfile(filepath=f)
153
154
155 if USE_ATTRSET:
156     def build_property_typemap(skip_classes):
157
158         property_typemap = {}
159
160         for attr in dir(bpy.types):
161             cls = getattr(bpy.types, attr)
162             if issubclass(cls, skip_classes):
163                 continue
164
165             # # to support skip-save we cant get all props
166             # properties = cls.bl_rna.properties.keys()
167             properties = []
168             for prop_id, prop in cls.bl_rna.properties.items():
169                 if not prop.is_skip_save:
170                     properties.append(prop_id)
171
172             properties.remove("rna_type")
173             property_typemap[attr] = properties
174
175         return property_typemap
176     CLS_BLACKLIST = (
177         bpy.types.BrushTextureSlot,
178         bpy.types.Brush,
179     )
180     property_typemap = build_property_typemap(CLS_BLACKLIST)
181     bpy_struct_type = bpy.types.Struct.__base__
182
183     def id_walk(value, parent):
184         value_type = type(value)
185         value_type_name = value_type.__name__
186
187         value_id = getattr(value, "id_data", Ellipsis)
188         value_props = property_typemap.get(value_type_name, ())
189
190         for prop in value_props:
191             subvalue = getattr(value, prop)
192
193             if subvalue == parent:
194                 continue
195             # grr, recursive!
196             if prop == "point_caches":
197                 continue
198             subvalue_type = type(subvalue)
199             yield value, prop, subvalue_type
200             subvalue_id = getattr(subvalue, "id_data", Ellipsis)
201
202             if value_id == subvalue_id:
203                 if subvalue_type == float:
204                     pass
205                 elif subvalue_type == int:
206                     pass
207                 elif subvalue_type == bool:
208                     pass
209                 elif subvalue_type == str:
210                     pass
211                 elif hasattr(subvalue, "__len__"):
212                     for sub_item in subvalue[:]:
213                         if isinstance(sub_item, bpy_struct_type):
214                             subitem_id = getattr(sub_item, "id_data", Ellipsis)
215                             if subitem_id == subvalue_id:
216                                 yield from id_walk(sub_item, value)
217
218                 if subvalue_type.__name__ in property_typemap:
219                     yield from id_walk(subvalue, value)
220
221     # main function
222     _random_values = (
223         None, object, type,
224         1, 0.1, -1,  # float("nan"),
225         "", "test", b"", b"test",
226         (), [], {},
227         (10,), (10, 20), (0, 0, 0),
228         {0: "", 1: "hello", 2: "test"}, {"": 0, "hello": 1, "test": 2},
229         set(), {"", "test", "."}, {None, ..., type},
230         range(10), (" " * i for i in range(10)),
231     )
232
233     def attrset_data():
234         for attr in dir(bpy.data):
235             if attr == "window_managers":
236                 continue
237             seq = getattr(bpy.data, attr)
238             if seq.__class__.__name__ == 'bpy_prop_collection':
239                 for id_data in seq:
240                     for val, prop, _tp in id_walk(id_data, bpy.data):
241                         # print(id_data)
242                         for val_rnd in _random_values:
243                             try:
244                                 setattr(val, prop, val_rnd)
245                             except:
246                                 pass
247
248
249 def run_ops(operators, setup_func=None, reset=True):
250     print("\ncontext:", setup_func.__name__)
251
252     # first invoke
253     for op_id, op in operators:
254         if op.poll():
255             print("    operator: %4d, %s" % (STATE["counter"], op_id))
256             STATE["counter"] += 1
257             sys.stdout.flush()  # in case of crash
258
259             # disable will get blender in a bad state and crash easy!
260             if reset:
261                 reset_test = True
262                 if USE_RANDOM:
263                     import random
264                     if random.random() < (1.0 - RANDOM_RESET):
265                         reset_test = False
266
267                 if reset_test:
268                     if USE_FILES:
269                         reset_file()
270                     else:
271                         reset_blend()
272                 del reset_test
273
274             if USE_RANDOM:
275                 # we can't be sure it will work
276                 try:
277                     setup_func()
278                 except:
279                     pass
280             else:
281                 setup_func()
282
283             for mode in {'EXEC_DEFAULT', 'INVOKE_DEFAULT'}:
284                 try:
285                     op(mode)
286                 except:
287                     # import traceback
288                     # traceback.print_exc()
289                     pass
290
291                 if USE_ATTRSET:
292                     attrset_data()
293
294     if not operators:
295         # run test
296         if reset:
297             reset_blend()
298         if USE_RANDOM:
299             # we can't be sure it will work
300             try:
301                 setup_func()
302             except:
303                 pass
304         else:
305             setup_func()
306
307
308 # contexts
309 def ctx_clear_scene():  # copied from batch_import.py
310     bpy.ops.wm.read_factory_settings(use_empty=True)
311
312
313 def ctx_editmode_mesh():
314     bpy.ops.object.mode_set(mode='EDIT')
315
316
317 def ctx_editmode_mesh_extra():
318     bpy.ops.object.vertex_group_add()
319     bpy.ops.object.shape_key_add(from_mix=False)
320     bpy.ops.object.shape_key_add(from_mix=True)
321     bpy.ops.mesh.uv_texture_add()
322     bpy.ops.mesh.vertex_color_add()
323     bpy.ops.object.material_slot_add()
324     # editmode last!
325     bpy.ops.object.mode_set(mode='EDIT')
326
327
328 def ctx_editmode_mesh_empty():
329     bpy.ops.object.mode_set(mode='EDIT')
330     bpy.ops.mesh.select_all(action='SELECT')
331     bpy.ops.mesh.delete()
332
333
334 def ctx_editmode_curves():
335     bpy.ops.curve.primitive_nurbs_circle_add()
336     bpy.ops.object.mode_set(mode='EDIT')
337
338
339 def ctx_editmode_curves_empty():
340     bpy.ops.curve.primitive_nurbs_circle_add()
341     bpy.ops.object.mode_set(mode='EDIT')
342     bpy.ops.curve.select_all(action='SELECT')
343     bpy.ops.curve.delete(type='VERT')
344
345
346 def ctx_editmode_surface():
347     bpy.ops.surface.primitive_nurbs_surface_torus_add()
348     bpy.ops.object.mode_set(mode='EDIT')
349
350
351 def ctx_editmode_mball():
352     bpy.ops.object.metaball_add()
353     bpy.ops.object.mode_set(mode='EDIT')
354
355
356 def ctx_editmode_text():
357     bpy.ops.object.text_add()
358     bpy.ops.object.mode_set(mode='EDIT')
359
360
361 def ctx_editmode_armature():
362     bpy.ops.object.armature_add()
363     bpy.ops.object.mode_set(mode='EDIT')
364
365
366 def ctx_editmode_armature_empty():
367     bpy.ops.object.armature_add()
368     bpy.ops.object.mode_set(mode='EDIT')
369     bpy.ops.armature.select_all(action='SELECT')
370     bpy.ops.armature.delete()
371
372
373 def ctx_editmode_lattice():
374     bpy.ops.object.add(type='LATTICE')
375     bpy.ops.object.mode_set(mode='EDIT')
376     # bpy.ops.object.vertex_group_add()
377
378
379 def ctx_object_empty():
380     bpy.ops.object.add(type='EMPTY')
381
382
383 def ctx_object_pose():
384     bpy.ops.object.armature_add()
385     bpy.ops.object.mode_set(mode='POSE')
386     bpy.ops.pose.select_all(action='SELECT')
387
388
389 def ctx_object_paint_weight():
390     bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
391
392
393 def ctx_object_paint_vertex():
394     bpy.ops.object.mode_set(mode='VERTEX_PAINT')
395
396
397 def ctx_object_paint_sculpt():
398     bpy.ops.object.mode_set(mode='SCULPT')
399
400
401 def ctx_object_paint_texture():
402     bpy.ops.object.mode_set(mode='TEXTURE_PAINT')
403
404
405 def bpy_check_type_duplicates():
406     # non essential sanity check
407     bl_types = dir(bpy.types)
408     bl_types_unique = set(bl_types)
409
410     if len(bl_types) != len(bl_types_unique):
411         print("Error, found duplicates in 'bpy.types'")
412         for t in sorted(bl_types_unique):
413             tot = bl_types.count(t)
414             if tot > 1:
415                 print("    '%s', %d" % (t, tot))
416         import sys
417         sys.exit(1)
418
419
420 def main():
421
422     bpy_check_type_duplicates()
423
424     # reset_blend()
425     import bpy
426     operators = []
427     for mod_name in dir(bpy.ops):
428         mod = getattr(bpy.ops, mod_name)
429         for submod_name in dir(mod):
430             op = getattr(mod, submod_name)
431             operators.append(("%s.%s" % (mod_name, submod_name), op))
432
433     operators.sort(key=lambda op: op[0])
434
435     filter_op_list(operators)
436
437     # for testing, mix the list up.
438     # operators.reverse()
439
440     if USE_RANDOM:
441         import random
442         random.seed(RANDOM_SEED[0])
443         operators = operators * RANDOM_MULTIPLY
444         random.shuffle(operators)
445
446     # 2 passes, first just run setup_func to make sure they are ok
447     for operators_test in ((), operators):
448         # Run the operator tests in different contexts
449         run_ops(operators_test, setup_func=lambda: None)
450
451         if USE_FILES:
452             continue
453
454         run_ops(operators_test, setup_func=ctx_clear_scene)
455         # object modes
456         run_ops(operators_test, setup_func=ctx_object_empty)
457         run_ops(operators_test, setup_func=ctx_object_pose)
458         run_ops(operators_test, setup_func=ctx_object_paint_weight)
459         run_ops(operators_test, setup_func=ctx_object_paint_vertex)
460         run_ops(operators_test, setup_func=ctx_object_paint_sculpt)
461         run_ops(operators_test, setup_func=ctx_object_paint_texture)
462         # mesh
463         run_ops(operators_test, setup_func=ctx_editmode_mesh)
464         run_ops(operators_test, setup_func=ctx_editmode_mesh_extra)
465         run_ops(operators_test, setup_func=ctx_editmode_mesh_empty)
466         # armature
467         run_ops(operators_test, setup_func=ctx_editmode_armature)
468         run_ops(operators_test, setup_func=ctx_editmode_armature_empty)
469         # curves
470         run_ops(operators_test, setup_func=ctx_editmode_curves)
471         run_ops(operators_test, setup_func=ctx_editmode_curves_empty)
472         run_ops(operators_test, setup_func=ctx_editmode_surface)
473         # other
474         run_ops(operators_test, setup_func=ctx_editmode_mball)
475         run_ops(operators_test, setup_func=ctx_editmode_text)
476         run_ops(operators_test, setup_func=ctx_editmode_lattice)
477
478         if not operators_test:
479             print("All setup functions run fine!")
480
481     print("Finished %r" % __file__)
482
483
484 if __name__ == "__main__":
485     # ~ for i in range(200):
486         # ~ RANDOM_SEED[0] += 1
487         #~ main()
488     main()