2011-05-11 17:24:48 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
|
|
#
|
|
|
|
# OpenERP, Open Source Management Solution
|
|
|
|
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
2014-01-15 18:03:13 +00:00
|
|
|
# Copyright (C) 2010-2014 OpenERP s.a. (<http://openerp.com>).
|
2011-05-11 17:24:48 +00:00
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as
|
|
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
|
|
# License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
#
|
|
|
|
##############################################################################
|
|
|
|
|
2014-02-18 09:54:52 +00:00
|
|
|
import functools
|
2012-03-01 12:16:11 +00:00
|
|
|
import imp
|
2015-01-15 12:28:25 +00:00
|
|
|
import importlib
|
|
|
|
import inspect
|
2011-05-11 17:24:48 +00:00
|
|
|
import itertools
|
2014-02-09 00:40:05 +00:00
|
|
|
import logging
|
2012-03-01 12:16:11 +00:00
|
|
|
import os
|
2014-02-09 00:40:05 +00:00
|
|
|
import re
|
2012-03-01 12:16:11 +00:00
|
|
|
import sys
|
2014-06-29 12:42:27 +00:00
|
|
|
import time
|
2014-02-17 15:58:30 +00:00
|
|
|
import unittest
|
2015-01-16 10:57:23 +00:00
|
|
|
import threading
|
2014-02-09 00:40:05 +00:00
|
|
|
from os.path import join as opj
|
2011-05-11 17:24:48 +00:00
|
|
|
|
2014-02-09 00:40:05 +00:00
|
|
|
import unittest2
|
2011-05-11 17:24:48 +00:00
|
|
|
|
2014-02-09 00:40:05 +00:00
|
|
|
import openerp
|
|
|
|
import openerp.tools as tools
|
2011-05-11 17:24:48 +00:00
|
|
|
import openerp.release as release
|
2014-02-09 00:40:05 +00:00
|
|
|
from openerp.tools.safe_eval import safe_eval as eval
|
2011-05-11 17:24:48 +00:00
|
|
|
|
2014-05-29 16:23:48 +00:00
|
|
|
MANIFEST = '__openerp__.py'
|
2014-08-13 08:57:53 +00:00
|
|
|
README = ['README.rst', 'README.md', 'README.txt']
|
2014-05-29 16:23:48 +00:00
|
|
|
|
2012-01-30 11:31:16 +00:00
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
2014-02-09 00:40:05 +00:00
|
|
|
# addons path as a list
|
2014-07-07 13:32:25 +00:00
|
|
|
ad_paths = []
|
2014-05-30 16:23:15 +00:00
|
|
|
hooked = False
|
2011-05-11 17:24:48 +00:00
|
|
|
|
|
|
|
# Modules already loaded
|
|
|
|
loaded = []
|
|
|
|
|
2012-01-09 12:52:43 +00:00
|
|
|
class AddonsImportHook(object):
|
|
|
|
"""
|
|
|
|
Import hook to load OpenERP addons from multiple paths.
|
|
|
|
|
|
|
|
OpenERP implements its own import-hook to load its addons. OpenERP
|
|
|
|
addons are Python modules. Originally, they were each living in their
|
|
|
|
own top-level namespace, e.g. the sale module, or the hr module. For
|
|
|
|
backward compatibility, `import <module>` is still supported. Now they
|
|
|
|
are living in `openerp.addons`. The good way to import such modules is
|
|
|
|
thus `import openerp.addons.module`.
|
|
|
|
"""
|
|
|
|
|
2016-01-29 09:20:08 +00:00
|
|
|
def find_module(self, module_name, package_path=None):
|
2012-01-09 12:52:43 +00:00
|
|
|
module_parts = module_name.split('.')
|
|
|
|
if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
|
|
|
|
return self # We act as a loader too.
|
|
|
|
|
|
|
|
def load_module(self, module_name):
|
2014-02-14 14:35:49 +00:00
|
|
|
if module_name in sys.modules:
|
|
|
|
return sys.modules[module_name]
|
2012-01-09 12:52:43 +00:00
|
|
|
|
2014-02-14 14:35:49 +00:00
|
|
|
_1, _2, module_part = module_name.split('.')
|
2012-01-09 12:52:43 +00:00
|
|
|
# Note: we don't support circular import.
|
|
|
|
f, path, descr = imp.find_module(module_part, ad_paths)
|
2012-01-24 10:20:24 +00:00
|
|
|
mod = imp.load_module('openerp.addons.' + module_part, f, path, descr)
|
2012-01-09 12:52:43 +00:00
|
|
|
sys.modules['openerp.addons.' + module_part] = mod
|
|
|
|
return mod
|
|
|
|
|
2011-05-11 17:24:48 +00:00
|
|
|
def initialize_sys_path():
|
2012-01-10 09:27:30 +00:00
|
|
|
"""
|
|
|
|
Setup an import-hook to be able to import OpenERP addons from the different
|
|
|
|
addons paths.
|
2011-09-02 13:31:36 +00:00
|
|
|
|
2012-01-10 09:27:30 +00:00
|
|
|
This ensures something like ``import crm`` (or even
|
|
|
|
``import openerp.addons.crm``) works even if the addons are not in the
|
|
|
|
PYTHONPATH.
|
2011-09-02 13:31:36 +00:00
|
|
|
"""
|
2011-05-11 17:24:48 +00:00
|
|
|
global ad_paths
|
2014-05-30 16:23:15 +00:00
|
|
|
global hooked
|
2011-05-11 17:24:48 +00:00
|
|
|
|
2014-07-07 13:32:25 +00:00
|
|
|
dd = tools.config.addons_data_dir
|
[FIX] module: allow disabling 1-click install
As discussed on issue #15225, it should be possible for system administrators
to disable the 1-click installation system.
The plan is to disable the feature by default, but make it relatively easy
to turn on when it is explicitly desired.
1. At the moment we cannot guarantee that all Apps published on the Odoo Apps
Store are safe. And it is a security risk to let end-users deploy Python
code on their Odoo servers without requiring any review/deployment by a
competent system administrator.
We will work on improving the validation process of the Store, but this
will require time, and won't probably be a 100% safe process in any case.
2. The one-click install feature is however really useful to help
non-technical users install Apps, as long as the feature has been
explicitly allowed by the system administrator. This is a common feature
in other software suites as well. So we'd like to keep it as an opt-in
feature.
3. Administrators of multi-tenant servers, cloud hosting services, etc.
understandably expect to be able to turn off the feature for
security/control reasons.
4. By turning off the feature by default, but still exposing it in the UI,
we keep it *discoverable* for users. The error message should be
helpful to direct users to their sysadmins.
5. By using the permissions of the download folder as a flag for turning
off the feature, we avoid introducing an extra server parameter.
The folder is still created (read-only) by default, for the sole purpose
of making it easier to locate.
Fixes #15225
2017-01-27 01:29:11 +00:00
|
|
|
if os.access(dd, os.R_OK) and dd not in ad_paths:
|
2014-07-07 13:32:25 +00:00
|
|
|
ad_paths.append(dd)
|
|
|
|
|
2014-05-30 16:23:15 +00:00
|
|
|
for ad in tools.config['addons_path'].split(','):
|
|
|
|
ad = os.path.abspath(tools.ustr(ad.strip()))
|
|
|
|
if ad not in ad_paths:
|
|
|
|
ad_paths.append(ad)
|
2014-01-15 18:03:13 +00:00
|
|
|
|
|
|
|
# add base module path
|
|
|
|
base_path = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons'))
|
2014-05-30 16:23:15 +00:00
|
|
|
if base_path not in ad_paths:
|
|
|
|
ad_paths.append(base_path)
|
2014-01-15 18:03:13 +00:00
|
|
|
|
2014-05-30 16:23:15 +00:00
|
|
|
if not hooked:
|
|
|
|
sys.meta_path.append(AddonsImportHook())
|
|
|
|
hooked = True
|
2011-05-11 17:24:48 +00:00
|
|
|
|
2012-01-09 10:16:47 +00:00
|
|
|
def get_module_path(module, downloaded=False, display_warning=True):
|
2011-05-11 17:24:48 +00:00
|
|
|
"""Return the path of the given module.
|
|
|
|
|
|
|
|
Search the addons paths and return the first path where the given
|
|
|
|
module is found. If downloaded is True, return the default addons
|
|
|
|
path if nothing else is found.
|
|
|
|
|
|
|
|
"""
|
|
|
|
initialize_sys_path()
|
|
|
|
for adp in ad_paths:
|
2016-09-13 13:04:58 +00:00
|
|
|
if os.path.exists(opj(adp, module, MANIFEST)) or os.path.exists(opj(adp, '%s.zip' % module)):
|
2011-05-11 17:24:48 +00:00
|
|
|
return opj(adp, module)
|
|
|
|
|
|
|
|
if downloaded:
|
2014-01-15 18:03:13 +00:00
|
|
|
return opj(tools.config.addons_data_dir, module)
|
2012-01-09 10:16:47 +00:00
|
|
|
if display_warning:
|
2012-01-24 12:42:52 +00:00
|
|
|
_logger.warning('module %s: module not found', module)
|
2011-05-11 17:24:48 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
def get_module_filetree(module, dir='.'):
|
|
|
|
path = get_module_path(module)
|
|
|
|
if not path:
|
|
|
|
return False
|
|
|
|
|
|
|
|
dir = os.path.normpath(dir)
|
|
|
|
if dir == '.':
|
|
|
|
dir = ''
|
|
|
|
if dir.startswith('..') or (dir and dir[0] == '/'):
|
|
|
|
raise Exception('Cannot access file outside the module')
|
|
|
|
|
2014-02-09 00:40:05 +00:00
|
|
|
files = openerp.tools.osutil.listdir(path, True)
|
2011-05-11 17:24:48 +00:00
|
|
|
|
|
|
|
tree = {}
|
|
|
|
for f in files:
|
|
|
|
if not f.startswith(dir):
|
|
|
|
continue
|
|
|
|
|
|
|
|
if dir:
|
|
|
|
f = f[len(dir)+int(not dir.endswith('/')):]
|
|
|
|
lst = f.split(os.sep)
|
|
|
|
current = tree
|
|
|
|
while len(lst) != 1:
|
|
|
|
current = current.setdefault(lst.pop(0), {})
|
|
|
|
current[lst.pop(0)] = None
|
|
|
|
|
|
|
|
return tree
|
|
|
|
|
|
|
|
def get_module_resource(module, *args):
|
|
|
|
"""Return the full path of a resource of the given module.
|
|
|
|
|
2012-02-08 17:39:32 +00:00
|
|
|
:param module: module name
|
|
|
|
:param list(str) args: resource path components within module
|
2011-05-11 17:24:48 +00:00
|
|
|
|
2012-02-08 17:39:32 +00:00
|
|
|
:rtype: str
|
|
|
|
:return: absolute path to the resource
|
2011-05-11 17:24:48 +00:00
|
|
|
|
|
|
|
TODO name it get_resource_path
|
|
|
|
TODO make it available inside on osv object (self.get_resource_path)
|
|
|
|
"""
|
2012-02-08 17:39:32 +00:00
|
|
|
mod_path = get_module_path(module)
|
|
|
|
if not mod_path: return False
|
|
|
|
resource_path = opj(mod_path, *args)
|
|
|
|
if os.path.isdir(mod_path):
|
|
|
|
# the module is a directory - ignore zip behavior
|
|
|
|
if os.path.exists(resource_path):
|
|
|
|
return resource_path
|
2011-05-11 17:24:48 +00:00
|
|
|
return False
|
|
|
|
|
2011-12-09 12:28:03 +00:00
|
|
|
def get_module_icon(module):
|
2013-06-28 15:07:55 +00:00
|
|
|
iconpath = ['static', 'description', 'icon.png']
|
2011-12-09 12:28:03 +00:00
|
|
|
if get_module_resource(module, *iconpath):
|
|
|
|
return ('/' + module + '/') + '/'.join(iconpath)
|
|
|
|
return '/base/' + '/'.join(iconpath)
|
2011-05-11 17:24:48 +00:00
|
|
|
|
2014-05-29 16:23:48 +00:00
|
|
|
def get_module_root(path):
|
|
|
|
"""
|
|
|
|
Get closest module's root begining from path
|
|
|
|
|
|
|
|
# Given:
|
|
|
|
# /foo/bar/module_dir/static/src/...
|
|
|
|
|
|
|
|
get_module_root('/foo/bar/module_dir/static/')
|
|
|
|
# returns '/foo/bar/module_dir'
|
|
|
|
|
|
|
|
get_module_root('/foo/bar/module_dir/')
|
|
|
|
# returns '/foo/bar/module_dir'
|
|
|
|
|
|
|
|
get_module_root('/foo/bar')
|
|
|
|
# returns None
|
|
|
|
|
|
|
|
@param path: Path from which the lookup should start
|
|
|
|
|
|
|
|
@return: Module root path or None if not found
|
|
|
|
"""
|
|
|
|
while not os.path.exists(os.path.join(path, MANIFEST)):
|
|
|
|
new_path = os.path.abspath(os.path.join(path, os.pardir))
|
|
|
|
if path == new_path:
|
|
|
|
return None
|
|
|
|
path = new_path
|
|
|
|
return path
|
|
|
|
|
2014-03-19 08:57:08 +00:00
|
|
|
def load_information_from_description_file(module, mod_path=None):
|
2011-05-11 17:24:48 +00:00
|
|
|
"""
|
|
|
|
:param module: The name of the module (sale, purchase, ...)
|
2014-03-19 08:57:08 +00:00
|
|
|
:param mod_path: Physical path of module, if not providedThe name of the module (sale, purchase, ...)
|
2011-05-11 17:24:48 +00:00
|
|
|
"""
|
|
|
|
|
2014-03-19 08:57:08 +00:00
|
|
|
if not mod_path:
|
|
|
|
mod_path = get_module_path(module)
|
2014-05-29 16:23:48 +00:00
|
|
|
terp_file = mod_path and opj(mod_path, MANIFEST) or False
|
2011-05-11 17:24:48 +00:00
|
|
|
if terp_file:
|
|
|
|
info = {}
|
2014-02-09 00:40:05 +00:00
|
|
|
if os.path.isfile(terp_file):
|
2011-12-09 11:53:16 +00:00
|
|
|
# default values for descriptor
|
|
|
|
info = {
|
|
|
|
'application': False,
|
|
|
|
'author': '',
|
2012-01-30 21:04:29 +00:00
|
|
|
'auto_install': False,
|
2011-12-09 11:53:16 +00:00
|
|
|
'category': 'Uncategorized',
|
|
|
|
'depends': [],
|
|
|
|
'description': '',
|
2011-12-09 12:28:03 +00:00
|
|
|
'icon': get_module_icon(module),
|
2011-12-09 11:53:16 +00:00
|
|
|
'installable': True,
|
|
|
|
'license': 'AGPL-3',
|
|
|
|
'post_load': None,
|
2012-08-24 14:23:23 +00:00
|
|
|
'version': '1.0',
|
2011-12-09 11:53:16 +00:00
|
|
|
'web': False,
|
|
|
|
'website': '',
|
2012-01-07 04:17:45 +00:00
|
|
|
'sequence': 100,
|
2012-07-17 08:50:28 +00:00
|
|
|
'summary': '',
|
2011-12-09 11:53:16 +00:00
|
|
|
}
|
|
|
|
info.update(itertools.izip(
|
|
|
|
'depends data demo test init_xml update_xml demo_xml'.split(),
|
|
|
|
iter(list, None)))
|
|
|
|
|
2012-02-02 15:45:32 +00:00
|
|
|
f = tools.file_open(terp_file)
|
|
|
|
try:
|
|
|
|
info.update(eval(f.read()))
|
|
|
|
finally:
|
|
|
|
f.close()
|
2011-12-09 12:02:07 +00:00
|
|
|
|
2014-08-13 08:57:53 +00:00
|
|
|
if not info.get('description'):
|
|
|
|
readme_path = [opj(mod_path, x) for x in README
|
|
|
|
if os.path.isfile(opj(mod_path, x))]
|
|
|
|
if readme_path:
|
|
|
|
readme_text = tools.file_open(readme_path[0]).read()
|
|
|
|
info['description'] = readme_text
|
|
|
|
|
2012-01-30 11:31:16 +00:00
|
|
|
if 'active' in info:
|
2012-01-30 21:04:29 +00:00
|
|
|
# 'active' has been renamed 'auto_install'
|
2012-01-30 11:31:16 +00:00
|
|
|
info['auto_install'] = info['active']
|
|
|
|
|
2012-08-24 14:23:23 +00:00
|
|
|
info['version'] = adapt_version(info['version'])
|
2011-05-11 17:24:48 +00:00
|
|
|
return info
|
|
|
|
|
|
|
|
#TODO: refactor the logger in this file to follow the logging guidelines
|
|
|
|
# for 6.0
|
2014-05-29 16:23:48 +00:00
|
|
|
_logger.debug('module %s: no %s file found.', module, MANIFEST)
|
2011-05-11 17:24:48 +00:00
|
|
|
return {}
|
|
|
|
|
|
|
|
def init_module_models(cr, module_name, obj_list):
|
|
|
|
""" Initialize a list of models.
|
|
|
|
|
|
|
|
Call _auto_init and init on each model to create or update the
|
|
|
|
database tables supporting the models.
|
|
|
|
|
|
|
|
TODO better explanation of _auto_init and init.
|
|
|
|
|
|
|
|
"""
|
2012-01-24 12:42:52 +00:00
|
|
|
_logger.info('module %s: creating or updating database tables', module_name)
|
2011-05-11 17:24:48 +00:00
|
|
|
todo = []
|
|
|
|
for obj in obj_list:
|
2011-06-10 14:05:21 +00:00
|
|
|
result = obj._auto_init(cr, {'module': module_name})
|
2011-05-11 17:24:48 +00:00
|
|
|
if result:
|
|
|
|
todo += result
|
|
|
|
if hasattr(obj, 'init'):
|
|
|
|
obj.init(cr)
|
|
|
|
cr.commit()
|
2011-06-10 14:05:21 +00:00
|
|
|
for obj in obj_list:
|
|
|
|
obj._auto_end(cr, {'module': module_name})
|
|
|
|
cr.commit()
|
2014-07-06 14:44:26 +00:00
|
|
|
todo.sort(key=lambda x: x[0])
|
2011-05-11 17:24:48 +00:00
|
|
|
for t in todo:
|
|
|
|
t[1](cr, *t[2])
|
|
|
|
cr.commit()
|
|
|
|
|
2012-02-09 15:27:32 +00:00
|
|
|
def load_openerp_module(module_name):
|
|
|
|
""" Load an OpenERP module, if not already loaded.
|
2011-05-11 17:24:48 +00:00
|
|
|
|
2011-09-02 13:31:36 +00:00
|
|
|
This loads the module and register all of its models, thanks to either
|
|
|
|
the MetaModel metaclass, or the explicit instantiation of the model.
|
2012-02-09 15:27:32 +00:00
|
|
|
This is also used to load server-wide module (i.e. it is also used
|
|
|
|
when there is no model to register).
|
2011-05-11 17:24:48 +00:00
|
|
|
"""
|
|
|
|
global loaded
|
2012-02-09 15:27:32 +00:00
|
|
|
if module_name in loaded:
|
2011-05-11 17:24:48 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
initialize_sys_path()
|
|
|
|
try:
|
2012-02-09 15:27:32 +00:00
|
|
|
mod_path = get_module_path(module_name)
|
2014-02-09 00:40:05 +00:00
|
|
|
__import__('openerp.addons.' + module_name)
|
2012-02-09 15:27:32 +00:00
|
|
|
|
|
|
|
# Call the module's post-load hook. This can done before any model or
|
|
|
|
# data has been initialized. This is ok as the post-load hook is for
|
|
|
|
# server-wide (instead of registry-specific) functionalities.
|
|
|
|
info = load_information_from_description_file(module_name)
|
|
|
|
if info['post_load']:
|
|
|
|
getattr(sys.modules['openerp.addons.' + module_name], info['post_load'])()
|
|
|
|
|
2011-05-11 17:24:48 +00:00
|
|
|
except Exception, e:
|
2014-02-09 00:40:05 +00:00
|
|
|
msg = "Couldn't load module %s" % (module_name)
|
2012-02-09 15:27:32 +00:00
|
|
|
_logger.critical(msg)
|
|
|
|
_logger.critical(e)
|
2011-05-11 17:24:48 +00:00
|
|
|
raise
|
|
|
|
else:
|
2012-02-09 15:27:32 +00:00
|
|
|
loaded.append(module_name)
|
2011-05-11 17:24:48 +00:00
|
|
|
|
|
|
|
def get_modules():
|
|
|
|
"""Returns the list of module names
|
|
|
|
"""
|
|
|
|
def listdir(dir):
|
|
|
|
def clean(name):
|
|
|
|
name = os.path.basename(name)
|
|
|
|
if name[-4:] == '.zip':
|
|
|
|
name = name[:-4]
|
|
|
|
return name
|
|
|
|
|
|
|
|
def is_really_module(name):
|
2014-05-29 16:23:48 +00:00
|
|
|
manifest_name = opj(dir, name, MANIFEST)
|
2012-12-09 02:46:18 +00:00
|
|
|
zipfile_name = opj(dir, name)
|
2014-02-09 00:40:05 +00:00
|
|
|
return os.path.isfile(manifest_name)
|
2011-05-11 17:24:48 +00:00
|
|
|
return map(clean, filter(is_really_module, os.listdir(dir)))
|
|
|
|
|
|
|
|
plist = []
|
|
|
|
initialize_sys_path()
|
|
|
|
for ad in ad_paths:
|
|
|
|
plist.extend(listdir(ad))
|
|
|
|
return list(set(plist))
|
|
|
|
|
|
|
|
def get_modules_with_version():
|
|
|
|
modules = get_modules()
|
2012-08-24 14:23:23 +00:00
|
|
|
res = dict.fromkeys(modules, adapt_version('1.0'))
|
2011-05-11 17:24:48 +00:00
|
|
|
for module in modules:
|
|
|
|
try:
|
|
|
|
info = load_information_from_description_file(module)
|
2012-08-24 14:23:23 +00:00
|
|
|
res[module] = info['version']
|
|
|
|
except Exception:
|
2011-05-11 17:24:48 +00:00
|
|
|
continue
|
|
|
|
return res
|
|
|
|
|
2012-08-24 14:23:23 +00:00
|
|
|
def adapt_version(version):
|
|
|
|
serie = release.major_version
|
|
|
|
if version == serie or not version.startswith(serie + '.'):
|
|
|
|
version = '%s.%s' % (serie, version)
|
|
|
|
return version
|
|
|
|
|
2014-02-09 00:40:05 +00:00
|
|
|
def get_test_modules(module):
|
2015-01-15 12:28:25 +00:00
|
|
|
""" Return a list of module for the addons potentially containing tests to
|
2014-02-09 14:19:12 +00:00
|
|
|
feed unittest2.TestLoader.loadTestsFromModule() """
|
2012-03-01 12:16:11 +00:00
|
|
|
# Try to import the module
|
2015-01-15 12:28:25 +00:00
|
|
|
modpath = 'openerp.addons.' + module
|
2012-03-01 12:16:11 +00:00
|
|
|
try:
|
2015-01-15 12:28:25 +00:00
|
|
|
mod = importlib.import_module('.tests', modpath)
|
2012-03-01 12:16:11 +00:00
|
|
|
except Exception, e:
|
2014-02-09 14:19:12 +00:00
|
|
|
# If module has no `tests` sub-module, no problem.
|
|
|
|
if str(e) != 'No module named tests':
|
|
|
|
_logger.exception('Can not `import %s`.', module)
|
|
|
|
return []
|
|
|
|
|
2015-01-15 12:28:25 +00:00
|
|
|
if hasattr(mod, 'fast_suite') or hasattr(mod, 'checks'):
|
|
|
|
_logger.warn(
|
|
|
|
"Found deprecated fast_suite or checks attribute in test module "
|
|
|
|
"%s. These have no effect in or after version 8.0.",
|
|
|
|
mod.__name__)
|
|
|
|
|
|
|
|
result = [mod_obj for name, mod_obj in inspect.getmembers(mod, inspect.ismodule)
|
|
|
|
if name.startswith('test_')]
|
2014-02-09 14:19:12 +00:00
|
|
|
return result
|
2014-02-09 00:40:05 +00:00
|
|
|
|
|
|
|
# Use a custom stream object to log the test executions.
|
|
|
|
class TestStream(object):
|
2014-03-17 02:13:17 +00:00
|
|
|
def __init__(self, logger_name='openerp.tests'):
|
|
|
|
self.logger = logging.getLogger(logger_name)
|
2014-02-09 00:40:05 +00:00
|
|
|
self.r = re.compile(r'^-*$|^ *... *$|^ok$')
|
|
|
|
def flush(self):
|
|
|
|
pass
|
|
|
|
def write(self, s):
|
|
|
|
if self.r.match(s):
|
|
|
|
return
|
|
|
|
first = True
|
2014-06-22 09:31:07 +00:00
|
|
|
level = logging.ERROR if s.startswith(('ERROR', 'FAIL', 'Traceback')) else logging.INFO
|
|
|
|
for c in s.splitlines():
|
2014-02-09 00:40:05 +00:00
|
|
|
if not first:
|
|
|
|
c = '` ' + c
|
|
|
|
first = False
|
2014-06-22 09:31:07 +00:00
|
|
|
self.logger.log(level, c)
|
2014-02-09 00:40:05 +00:00
|
|
|
|
2014-02-17 00:51:37 +00:00
|
|
|
current_test = None
|
|
|
|
|
2014-03-18 12:22:50 +00:00
|
|
|
def runs_at(test, hook, default):
|
|
|
|
# by default, tests do not run post install
|
|
|
|
test_runs = getattr(test, hook, default)
|
|
|
|
|
|
|
|
# for a test suite, we're done
|
|
|
|
if not isinstance(test, unittest.TestCase):
|
|
|
|
return test_runs
|
|
|
|
|
|
|
|
# otherwise check the current test method to see it's been set to a
|
|
|
|
# different state
|
|
|
|
method = getattr(test, test._testMethodName)
|
|
|
|
return getattr(method, hook, test_runs)
|
|
|
|
|
|
|
|
runs_at_install = functools.partial(runs_at, hook='at_install', default=True)
|
|
|
|
runs_post_install = functools.partial(runs_at, hook='post_install', default=False)
|
|
|
|
|
|
|
|
def run_unit_tests(module_name, dbname, position=runs_at_install):
|
2012-03-02 11:02:27 +00:00
|
|
|
"""
|
2014-02-18 09:56:32 +00:00
|
|
|
:returns: ``True`` if all of ``module_name``'s tests succeeded, ``False``
|
|
|
|
if any of them failed.
|
|
|
|
:rtype: bool
|
2012-03-02 11:02:27 +00:00
|
|
|
"""
|
2014-02-17 00:51:37 +00:00
|
|
|
global current_test
|
|
|
|
current_test = module_name
|
2014-02-09 00:40:05 +00:00
|
|
|
mods = get_test_modules(module_name)
|
2015-01-16 10:57:23 +00:00
|
|
|
threading.currentThread().testing = True
|
2014-02-09 00:40:05 +00:00
|
|
|
r = True
|
|
|
|
for m in mods:
|
2014-02-17 15:58:30 +00:00
|
|
|
tests = unwrap_suite(unittest2.TestLoader().loadTestsFromModule(m))
|
2014-03-18 12:22:50 +00:00
|
|
|
suite = unittest2.TestSuite(itertools.ifilter(position, tests))
|
2014-02-14 14:52:57 +00:00
|
|
|
|
2014-06-29 12:42:27 +00:00
|
|
|
if suite.countTestCases():
|
2014-07-01 19:57:55 +00:00
|
|
|
t0 = time.time()
|
|
|
|
t0_sql = openerp.sql_db.sql_counter
|
2014-06-29 12:42:27 +00:00
|
|
|
_logger.info('%s running tests.', m.__name__)
|
|
|
|
result = unittest2.TextTestRunner(verbosity=2, stream=TestStream(m.__name__)).run(suite)
|
2014-07-01 19:57:55 +00:00
|
|
|
if time.time() - t0 > 5:
|
|
|
|
_logger.log(25, "%s tested in %.2fs, %s queries", m.__name__, time.time() - t0, openerp.sql_db.sql_counter - t0_sql)
|
2014-06-29 12:42:27 +00:00
|
|
|
if not result.wasSuccessful():
|
|
|
|
r = False
|
|
|
|
_logger.error("Module %s: %d failures, %d errors", module_name, len(result.failures), len(result.errors))
|
2014-02-18 10:18:47 +00:00
|
|
|
|
2014-02-17 00:51:37 +00:00
|
|
|
current_test = None
|
2015-01-16 10:57:23 +00:00
|
|
|
threading.currentThread().testing = False
|
2014-02-09 00:40:05 +00:00
|
|
|
return r
|
2011-05-11 17:24:48 +00:00
|
|
|
|
2014-02-17 15:58:30 +00:00
|
|
|
def unwrap_suite(test):
|
2012-03-02 11:02:27 +00:00
|
|
|
"""
|
2014-02-18 09:54:52 +00:00
|
|
|
Attempts to unpack testsuites (holding suites or cases) in order to
|
|
|
|
generate a single stream of terminals (either test cases or customized
|
|
|
|
test suites). These can then be checked for run/skip attributes
|
|
|
|
individually.
|
|
|
|
|
|
|
|
An alternative would be to use a variant of @unittest2.skipIf with a state
|
|
|
|
flag of some sort e.g. @unittest2.skipIf(common.runstate != 'at_install'),
|
|
|
|
but then things become weird with post_install as tests should *not* run
|
|
|
|
by default there
|
|
|
|
"""
|
2014-02-17 15:58:30 +00:00
|
|
|
if isinstance(test, unittest.TestCase):
|
|
|
|
yield test
|
|
|
|
return
|
|
|
|
|
|
|
|
subtests = list(test)
|
|
|
|
# custom test suite (no test cases)
|
|
|
|
if not len(subtests):
|
|
|
|
yield test
|
|
|
|
return
|
|
|
|
|
|
|
|
for item in itertools.chain.from_iterable(
|
|
|
|
itertools.imap(unwrap_suite, subtests)):
|
|
|
|
yield item
|
2011-05-11 17:24:48 +00:00
|
|
|
|
|
|
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|