Modules test: Don't print backtrace for files which are ignored
[blender.git] / tests / python / bl_load_py_modules.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 # simple script to enable all addons, and disable
22
23 """
24 ./blender.bin --background -noaudio --factory-startup --python tests/python/bl_load_py_modules.py
25 """
26
27 import bpy
28 import addon_utils
29
30 import sys
31 import os
32
33 BLACKLIST = {
34     "bl_i18n_utils",
35     "bl_previews_utils",
36     "cycles",
37     "io_export_dxf",  # TODO, check on why this fails
38     'io_import_dxf',  # Because of cydxfentity.so dependency
39
40     # The unpacked wheel is only loaded when actually used, not directly on import:
41     os.path.join("io_blend_utils", "blender_bam-unpacked.whl"),
42     }
43
44 # Some modules need to add to the `sys.path`.
45 MODULE_SYS_PATHS = {
46     # Runs in a Python subprocess, so its expected its basedir can be imported.
47     "io_blend_utils.blendfile_pack": ".",
48     }
49
50 if not bpy.app.build_options.freestyle:
51     BLACKLIST.add("render_freestyle_svg")
52
53 BLACKLIST_DIRS = (
54     os.path.join(bpy.utils.resource_path('USER'), "scripts"),
55     ) + tuple(addon_utils.paths()[1:])
56
57
58 def module_names_recursive(mod_dir, *, parent=None):
59     """
60     a version of bpy.path.module_names that includes non-packages
61     """
62
63     is_package = os.path.exists(os.path.join(mod_dir, "__init__.py"))
64
65     for n in os.listdir(mod_dir):
66         if not n.startswith((".", "_")):
67             submod_full = os.path.join(mod_dir, n)
68             if os.path.isdir(submod_full):
69                 if not parent:
70                     subparent = n
71                 else:
72                     subparent = parent + "." + n
73                 yield from module_names_recursive(submod_full, parent=subparent)
74             elif n.endswith(".py") and is_package is False:
75                 submod = n[:-3]
76                 if parent:
77                     submod = parent + "." + submod
78                 yield submod, submod_full
79
80
81 def module_names_all(mod_dir):
82     yield from bpy.path.module_names(mod_dir)
83     yield from module_names_recursive(mod_dir)
84
85
86 def addon_modules_sorted():
87     modules = addon_utils.modules({})
88     modules[:] = [mod for mod in modules if not mod.__file__.startswith(BLACKLIST_DIRS)]
89     modules.sort(key=lambda mod: mod.__name__)
90     return modules
91
92
93 def source_list(path, filename_check=None):
94     from os.path import join
95     for dirpath, dirnames, filenames in os.walk(path):
96         # skip '.svn'
97         if dirpath.startswith("."):
98             continue
99
100         for filename in filenames:
101             filepath = join(dirpath, filename)
102             if filename_check is None or filename_check(filepath):
103                 yield filepath
104
105
106 def load_addons():
107     modules = addon_modules_sorted()
108     addons = bpy.context.user_preferences.addons
109
110     # first disable all
111     for mod_name in list(addons.keys()):
112         addon_utils.disable(mod_name, default_set=True)
113
114     assert(bool(addons) is False)
115
116     for mod in modules:
117         mod_name = mod.__name__
118         if mod_name in BLACKLIST:
119             continue
120         addon_utils.enable(mod_name, default_set=True)
121         if not (mod_name in addons):
122             raise Exception("'addon_utils.enable(%r)' call failed" % mod_name)
123
124
125 def load_modules():
126     VERBOSE = os.environ.get("BLENDER_VERBOSE") is not None
127
128     modules = []
129     module_paths = []
130
131     # paths blender stores scripts in.
132     paths = bpy.utils.script_paths()
133
134     print("Paths:")
135     for script_path in paths:
136         print("\t'%s'" % script_path)
137
138     #
139     # find all sys.path we added
140     for script_path in paths:
141         for mod_dir in sys.path:
142             if mod_dir.startswith(script_path):
143                 if not mod_dir.startswith(BLACKLIST_DIRS):
144                     if mod_dir not in module_paths:
145                         if os.path.exists(mod_dir):
146                             module_paths.append(mod_dir)
147
148     #
149     # collect modules from our paths.
150     module_names = {}
151     for mod_dir in module_paths:
152         # print("mod_dir", mod_dir)
153         for mod, mod_full in bpy.path.module_names(mod_dir):
154             if mod in BLACKLIST:
155                 continue
156             if mod in module_names:
157                 mod_dir_prev, mod_full_prev = module_names[mod]
158                 raise Exception("Module found twice %r.\n    (%r -> %r, %r -> %r)" %
159                                 (mod, mod_dir, mod_full, mod_dir_prev, mod_full_prev))
160
161             modules.append(__import__(mod))
162
163             module_names[mod] = mod_dir, mod_full
164     del module_names
165
166     #
167     # test we tested all files except for presets and templates
168     ignore_paths = [
169         os.sep + "presets" + os.sep,
170         os.sep + "templates" + os.sep,
171     ] + ([(os.sep + f + os.sep) for f in BLACKLIST] +
172          [(os.sep + f + ".py")  for f in BLACKLIST])
173
174     #
175     # now submodules
176     for m in modules:
177         filepath = m.__file__
178         if os.path.basename(filepath).startswith("__init__."):
179             mod_dir = os.path.dirname(filepath)
180             for submod, submod_full in module_names_all(mod_dir):
181                 # fromlist is ignored, ugh.
182                 mod_name_full = m.__name__ + "." + submod
183
184                 sys_path_back = sys.path[:]
185
186                 sys.path.extend([
187                     os.path.normpath(os.path.join(mod_dir, f))
188                     for f in MODULE_SYS_PATHS.get(mod_name_full, ())
189                     ])
190
191                 try:
192                     __import__(mod_name_full)
193                     mod_imp = sys.modules[mod_name_full]
194
195                     sys.path[:] = sys_path_back
196
197                     # check we load what we ask for.
198                     assert(os.path.samefile(mod_imp.__file__, submod_full))
199
200                     modules.append(mod_imp)
201                 except Exception as e:
202                     import traceback
203                     # Module might fail to import, but we don't want whole test to fail here.
204                     # Reasoning:
205                     # - This module might be in ignored list (for example, preset or template),
206                     #   so failing here will cause false-positive test failure.
207                     # - If this is module which should not be ignored, it is not added to list
208                     #   of successfully loaded modules, meaning the test will catch this
209                     #   import failure.
210                     # - We want to catch all failures of this script instead of stopping on
211                     #   a first big failure.
212                     do_print = True
213                     if not VERBOSE:
214                         for ignore in ignore_paths:
215                             if ignore in submod_full:
216                                 do_print = False
217                                 break
218                     if do_print:
219                         traceback.print_exc()
220
221     #
222     # check which filepaths we didn't load
223     source_files = []
224     for mod_dir in module_paths:
225         source_files.extend(source_list(mod_dir, filename_check=lambda f: f.endswith(".py")))
226
227     source_files = list(set(source_files))
228     source_files.sort()
229
230     #
231     # remove loaded files
232     loaded_files = list({m.__file__ for m in modules})
233     loaded_files.sort()
234
235     for f in loaded_files:
236         source_files.remove(f)
237
238     for f in source_files:
239         for ignore in ignore_paths:
240             if ignore in f:
241                 break
242         else:
243             raise Exception("Source file %r not loaded in test" % f)
244
245     print("loaded %d modules" % len(loaded_files))
246
247
248 def main():
249     load_addons()
250     load_modules()
251
252 if __name__ == "__main__":
253     # So a python error exits(1)
254     try:
255         main()
256     except:
257         import traceback
258         traceback.print_exc()
259         sys.exit(1)