[FIX] ir.http testing generate a router including the current module
[odoo/odoo.git] / openerp / modules / module.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #    Copyright (C) 2010-2012 OpenERP s.a. (<http://openerp.com>).
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU Affero General Public License as
10 #    published by the Free Software Foundation, either version 3 of the
11 #    License, or (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 import base64
24 import imp
25 import itertools
26 import logging
27 import os
28 import re
29 import sys
30 import types
31 from cStringIO import StringIO
32 from os.path import join as opj
33
34 import unittest2
35
36 import openerp
37 import openerp.tools as tools
38 import openerp.release as release
39 from openerp.tools.safe_eval import safe_eval as eval
40
41 _logger = logging.getLogger(__name__)
42 _test_logger = logging.getLogger('openerp.tests')
43
44 # addons path ','.joined
45 _ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base)
46
47 # addons path as a list
48 ad_paths = []
49
50 # Modules already loaded
51 loaded = []
52
53 class AddonsImportHook(object):
54     """
55     Import hook to load OpenERP addons from multiple paths.
56
57     OpenERP implements its own import-hook to load its addons. OpenERP
58     addons are Python modules. Originally, they were each living in their
59     own top-level namespace, e.g. the sale module, or the hr module. For
60     backward compatibility, `import <module>` is still supported. Now they
61     are living in `openerp.addons`. The good way to import such modules is
62     thus `import openerp.addons.module`.
63     """
64
65     def find_module(self, module_name, package_path):
66         module_parts = module_name.split('.')
67         if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
68             return self # We act as a loader too.
69
70     def load_module(self, module_name):
71         if module_name in sys.modules:
72             return sys.modules[module_name]
73
74         _1, _2, module_part = module_name.split('.')
75         # Note: we don't support circular import.
76         f, path, descr = imp.find_module(module_part, ad_paths)
77         mod = imp.load_module('openerp.addons.' + module_part, f, path, descr)
78         sys.modules['openerp.addons.' + module_part] = mod
79         return mod
80
81 def initialize_sys_path():
82     """
83     Setup an import-hook to be able to import OpenERP addons from the different
84     addons paths.
85
86     This ensures something like ``import crm`` (or even
87     ``import openerp.addons.crm``) works even if the addons are not in the
88     PYTHONPATH.
89     """
90     global ad_paths
91     if ad_paths:
92         return
93
94     ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(','))
95     ad_paths.append(os.path.abspath(_ad)) # for get_module_path
96     sys.meta_path.append(AddonsImportHook())
97
98 def get_module_path(module, downloaded=False, display_warning=True):
99     """Return the path of the given module.
100
101     Search the addons paths and return the first path where the given
102     module is found. If downloaded is True, return the default addons
103     path if nothing else is found.
104
105     """
106     initialize_sys_path()
107     for adp in ad_paths:
108         if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
109             return opj(adp, module)
110
111     if downloaded:
112         return opj(_ad, module)
113     if display_warning:
114         _logger.warning('module %s: module not found', module)
115     return False
116
117 def get_module_filetree(module, dir='.'):
118     path = get_module_path(module)
119     if not path:
120         return False
121
122     dir = os.path.normpath(dir)
123     if dir == '.':
124         dir = ''
125     if dir.startswith('..') or (dir and dir[0] == '/'):
126         raise Exception('Cannot access file outside the module')
127
128     files = openerp.tools.osutil.listdir(path, True)
129
130     tree = {}
131     for f in files:
132         if not f.startswith(dir):
133             continue
134
135         if dir:
136             f = f[len(dir)+int(not dir.endswith('/')):]
137         lst = f.split(os.sep)
138         current = tree
139         while len(lst) != 1:
140             current = current.setdefault(lst.pop(0), {})
141         current[lst.pop(0)] = None
142
143     return tree
144
145 def get_module_resource(module, *args):
146     """Return the full path of a resource of the given module.
147
148     :param module: module name
149     :param list(str) args: resource path components within module
150
151     :rtype: str
152     :return: absolute path to the resource
153
154     TODO name it get_resource_path
155     TODO make it available inside on osv object (self.get_resource_path)
156     """
157     mod_path = get_module_path(module)
158     if not mod_path: return False
159     resource_path = opj(mod_path, *args)
160     if os.path.isdir(mod_path):
161         # the module is a directory - ignore zip behavior
162         if os.path.exists(resource_path):
163             return resource_path
164     return False
165
166 def get_module_icon(module):
167     iconpath = ['static', 'description', 'icon.png']
168     if get_module_resource(module, *iconpath):
169         return ('/' + module + '/') + '/'.join(iconpath)
170     return '/base/'  + '/'.join(iconpath)
171
172 def load_information_from_description_file(module):
173     """
174     :param module: The name of the module (sale, purchase, ...)
175     """
176
177     terp_file = get_module_resource(module, '__openerp__.py')
178     mod_path = get_module_path(module)
179     if terp_file:
180         info = {}
181         if os.path.isfile(terp_file):
182             # default values for descriptor
183             info = {
184                 'application': False,
185                 'author': '',
186                 'auto_install': False,
187                 'category': 'Uncategorized',
188                 'depends': [],
189                 'description': '',
190                 'icon': get_module_icon(module),
191                 'installable': True,
192                 'license': 'AGPL-3',
193                 'name': False,
194                 'post_load': None,
195                 'version': '1.0',
196                 'web': False,
197                 'website': '',
198                 'sequence': 100,
199                 'summary': '',
200             }
201             info.update(itertools.izip(
202                 'depends data demo test init_xml update_xml demo_xml'.split(),
203                 iter(list, None)))
204
205             f = tools.file_open(terp_file)
206             try:
207                 info.update(eval(f.read()))
208             finally:
209                 f.close()
210
211             if 'active' in info:
212                 # 'active' has been renamed 'auto_install'
213                 info['auto_install'] = info['active']
214
215             info['version'] = adapt_version(info['version'])
216             return info
217
218     #TODO: refactor the logger in this file to follow the logging guidelines
219     #      for 6.0
220     _logger.debug('module %s: no __openerp__.py file found.', module)
221     return {}
222
223 def init_module_models(cr, module_name, obj_list):
224     """ Initialize a list of models.
225
226     Call _auto_init and init on each model to create or update the
227     database tables supporting the models.
228
229     TODO better explanation of _auto_init and init.
230
231     """
232     _logger.info('module %s: creating or updating database tables', module_name)
233     todo = []
234     for obj in obj_list:
235         result = obj._auto_init(cr, {'module': module_name})
236         if result:
237             todo += result
238         if hasattr(obj, 'init'):
239             obj.init(cr)
240         cr.commit()
241     for obj in obj_list:
242         obj._auto_end(cr, {'module': module_name})
243         cr.commit()
244     todo.sort()
245     for t in todo:
246         t[1](cr, *t[2])
247     cr.commit()
248
249 def load_openerp_module(module_name):
250     """ Load an OpenERP module, if not already loaded.
251
252     This loads the module and register all of its models, thanks to either
253     the MetaModel metaclass, or the explicit instantiation of the model.
254     This is also used to load server-wide module (i.e. it is also used
255     when there is no model to register).
256     """
257     global loaded
258     if module_name in loaded:
259         return
260
261     initialize_sys_path()
262     try:
263         mod_path = get_module_path(module_name)
264         __import__('openerp.addons.' + module_name)
265
266         # Call the module's post-load hook. This can done before any model or
267         # data has been initialized. This is ok as the post-load hook is for
268         # server-wide (instead of registry-specific) functionalities.
269         info = load_information_from_description_file(module_name)
270         if info['post_load']:
271             getattr(sys.modules['openerp.addons.' + module_name], info['post_load'])()
272
273     except Exception, e:
274         msg = "Couldn't load module %s" % (module_name)
275         _logger.critical(msg)
276         _logger.critical(e)
277         raise
278     else:
279         loaded.append(module_name)
280
281 def get_modules():
282     """Returns the list of module names
283     """
284     def listdir(dir):
285         def clean(name):
286             name = os.path.basename(name)
287             if name[-4:] == '.zip':
288                 name = name[:-4]
289             return name
290
291         def is_really_module(name):
292             manifest_name = opj(dir, name, '__openerp__.py')
293             zipfile_name = opj(dir, name)
294             return os.path.isfile(manifest_name)
295         return map(clean, filter(is_really_module, os.listdir(dir)))
296
297     plist = []
298     initialize_sys_path()
299     for ad in ad_paths:
300         plist.extend(listdir(ad))
301     return list(set(plist))
302
303 def get_modules_with_version():
304     modules = get_modules()
305     res = dict.fromkeys(modules, adapt_version('1.0'))
306     for module in modules:
307         try:
308             info = load_information_from_description_file(module)
309             res[module] = info['version']
310         except Exception:
311             continue
312     return res
313
314 def adapt_version(version):
315     serie = release.major_version
316     if version == serie or not version.startswith(serie + '.'):
317         version = '%s.%s' % (serie, version)
318     return version
319
320 def get_test_modules(module):
321     """ Return a list of module for the addons potentialy containing tests to
322     feed unittest2.TestLoader.loadTestsFromModule() """
323     # Try to import the module
324     module = 'openerp.addons.' + module + '.tests'
325     try:
326         __import__(module)
327     except Exception, e:
328         # If module has no `tests` sub-module, no problem.
329         if str(e) != 'No module named tests':
330             _logger.exception('Can not `import %s`.', module)
331         return []
332
333     # include submodules too
334     result = [mod_obj for name, mod_obj in sys.modules.iteritems()
335               if mod_obj # mod_obj can be None
336               if name.startswith(module)]
337     return result
338
339 # Use a custom stream object to log the test executions.
340 class TestStream(object):
341     def __init__(self):
342         self.r = re.compile(r'^-*$|^ *... *$|^ok$')
343     def flush(self):
344         pass
345     def write(self, s):
346         if self.r.match(s):
347             return
348         first = True
349         for c in s.split('\n'):
350             if not first:
351                 c = '` ' + c
352             first = False
353             _test_logger.info(c)
354
355 current_test = None
356
357 def run_unit_tests(module_name, dbname):
358     """
359     Return True or False if some tests were found and succeeded or failed.
360     Return None if no test was found.
361     """
362     global current_test
363     current_test = module_name
364     mods = get_test_modules(module_name)
365     r = True
366     for m in mods:
367         suite = unittest2.TestSuite()
368         suite.addTests(unittest2.TestLoader().loadTestsFromModule(m))
369         _logger.info('module %s: running test %s.', module_name, m.__name__)
370
371         result = unittest2.TextTestRunner(verbosity=2, stream=TestStream()).run(suite)
372         if not result.wasSuccessful():
373             r = False
374             _logger.error('module %s: at least one error occurred in a test', module_name)
375     current_test = None
376     return r
377
378 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: