merge trunk

bzr revid: nicolas.vanhoren@openerp.com-20130114102410-m60y4kh8o69u00hc
This commit is contained in:
niv-openerp 2013-01-14 11:24:10 +01:00
commit d211ac7798
38 changed files with 2910 additions and 282 deletions

34
doc/adding-command.rst Normal file
View File

@ -0,0 +1,34 @@
.. _adding-command:
Adding a new command
====================
``oe`` uses the argparse_ library to implement commands. Each
command lives in its own ``openerpcommand/<command>.py`` file.
.. _argparse: http://docs.python.org/2.7/library/argparse.html
To create a new command, probably the most simple way to get started is to
copy/paste an existing command, say ``openerpcommand/initialize.py`` to
``openerpcommand/foo.py``. In the newly created file, the important bits
are the ``run(args)`` and ``add_parser(subparsers)`` functions.
``add_parser``'s responsability is to create a (sub-)parser for the command,
i.e. describe the different options and flags. The last thing it does is to set
``run`` as the function to call when the command is invoked.
.. code-block:: python
> def add_parser(subparsers):
> parser = subparsers.add_parser('<command-name>',
> description='...')
> parser.add_argument(...)
> ...
> parser.set_defaults(run=run)
``run(args)`` actually implements the command. It should be kept as simple as
possible and delegate most of its work to small functions (probably placed at
the top of the new file). In other words, its responsability is mainly to
deal with the presence/absence/pre-processing of ``argparse``'s arguments.
Finally, the module must be added to ``openerpcommand/__init__.py``.

256
doc/conf.py Normal file
View File

@ -0,0 +1,256 @@
# -*- coding: utf-8 -*-
#
# OpenERP Technical Documentation configuration file, created by
# sphinx-quickstart on Fri Feb 17 16:14:06 2012.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
sys.path.append(os.path.abspath('_themes'))
sys.path.append(os.path.abspath('..'))
sys.path.append(os.path.abspath('../openerp'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'OpenERP Server Developers Documentation'
copyright = u'2012, OpenERP s.a.'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '7.0'
# The full version, including alpha/beta/rc tags.
release = '7.0b'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'flask'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = ['_themes']
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
html_sidebars = {
'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'],
'**': ['sidebarlogo.html', 'localtoc.html', 'relations.html',
'sourcelink.html', 'searchbox.html']
}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'openerp-server-doc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'openerp-server-doc.tex', u'OpenERP Server Developers Documentation',
u'OpenERP s.a.', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'openerp-server-doc', u'OpenERP Server Developers Documentation',
[u'OpenERP s.a.'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'OpenERPServerDocumentation', u'OpenERP Server Developers Documentation',
u'OpenERP s.a.', 'OpenERPServerDocumentation', 'Developers documentation for the openobject-server project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
'python': ('http://docs.python.org/', None),
'openerpweb': ('http://doc.openerp.com/trunk/developers/web', None),
}

View File

@ -18,13 +18,21 @@ OpenERP Server
06_misc
09_deployment
OpenERP Command
'''''''''''''''
.. toctree::
:maxdepth: 1
openerp-command.rst
adding-command.rst
OpenERP Server API
''''''''''''''''''
.. toctree::
:maxdepth: 1
api_core.rst
api_models.rst
Concepts

60
doc/openerp-command.rst Normal file
View File

@ -0,0 +1,60 @@
.. _openerp-command:
OpenERP Command
===============
The ``oe`` script provides a set of command-line tools around the OpenERP
framework.
Using OpenERP Command
---------------------
In contrast to the previous ``openerp-server`` script, ``oe`` defines a few
sub-commands, each with its own set of flags and options. You can get some
information for any of them with
::
> oe <sub-command> --help
For instance::
> oe run-tests --help
Some ``oe`` options can be provided via environment variables. For instance::
> export OPENERP_DATABASE=trunk
> export OPENERP_HOST=127.0.0.1
> export OPENERP_PORT=8069
Depending on your needs, you can group all of the above in one single script;
for instance here is a, say, ``test-trunk-view-validation.sh`` file::
COMMAND_REPO=/home/thu/repos/command/trunk/
SERVER_REPO=/home/thu/repos/server/trunk
export PYTHONPATH=$SERVER_REPO:$COMMAND_REPO
export PATH=$SERVER_REPO:$COMMAND_REPO:$PATH
export OPENERP_DATABASE=trunk
export OPENERP_HOST=127.0.0.1
export OPENERP_PORT=8069
# The -d ignored is actually needed by `oe` even though `test_view_validation`
# itself does not need it.
oe run-tests -d ignored -m openerp.test_view_validation
Adding new commands
-------------------
See the :doc:`adding-command` page.
Bash completion
---------------
A preliminary ``oe-bash-completion`` file is provided. After sourcing it,
::
> . oe-bash-completion
completion (using the TAB character) in Bash should be working.

5
oe Executable file
View File

@ -0,0 +1,5 @@
#! /usr/bin/env python2
if __name__ == '__main__':
import openerpcommand.main
openerpcommand.main.run()

89
oe-bash-completion Normal file
View File

@ -0,0 +1,89 @@
_oe()
{
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
cmd="${COMP_WORDS[0]}"
subcmd=""
if [[ ${COMP_CWORD} > 0 ]] ; then
subcmd="${COMP_WORDS[1]}"
fi
# oe
opts="initialize model read run-tests scaffold update \
call open show consume-nothing consume-memory leak-memory \
consume-cpu bench-read bench-fields-view-get bench-dummy bench-login \
bench-sale-mrp --help"
if [[ ${prev} == oe && ${cur} != -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# oe call
opts="--database --user --password --host --port --help"
if [[ ${subcmd} == call ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# oe initialize
opts="--database --addons --all-modules --exclude --no-create --help"
if [[ ${subcmd} == initialize ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# oe model
opts="--database --model --field --verbose --help"
if [[ ${subcmd} == model ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# oe read
opts="--database --model --id --field --verbose --short --help"
if [[ ${subcmd} == read ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# oe run-tests
opts="--database --addons --module --dry-run --help"
if [[ ${subcmd} == run-tests ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# oe scaffold
opts="--help"
if [[ ${subcmd} == scaffold ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# fallback for unimplemented completion
opts="--help"
if [[ true ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
}
complete -F _oe oe

File diff suppressed because it is too large Load Diff

View File

@ -136,12 +136,13 @@ class ir_cron(osv.osv):
except Exception, e:
self._handle_callback_exception(cr, uid, model_name, method_name, args, job_id, e)
def _process_job(self, cr, job):
def _process_job(self, job_cr, job, cron_cr):
""" Run a given job taking care of the repetition.
The cursor has a lock on the job (aquired by _acquire_job()).
:param job_cr: cursor to use to execute the job, safe to commit/rollback
:param job: job to be run (as a dictionary).
:param cron_cr: cursor holding lock on the cron job row, to use to update the next exec date,
must not be committed/rolled back!
"""
try:
now = datetime.now()
@ -153,19 +154,19 @@ class ir_cron(osv.osv):
if numbercall > 0:
numbercall -= 1
if not ok or job['doall']:
self._callback(cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
self._callback(job_cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
if numbercall:
nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
ok = True
addsql = ''
if not numbercall:
addsql = ', active=False'
cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s",
cron_cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s",
(nextcall.strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id']))
finally:
cr.commit()
cr.close()
job_cr.commit()
cron_cr.commit()
@classmethod
def _acquire_job(cls, db_name):
@ -181,44 +182,14 @@ class ir_cron(osv.osv):
"""
db = openerp.sql_db.db_connect(db_name)
cr = db.cursor()
jobs = []
try:
# Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1.
cr.execute("""SELECT * FROM ir_cron
WHERE numbercall != 0
AND active AND nextcall <= (now() at time zone 'UTC')
ORDER BY priority""")
for job in cr.dictfetchall():
task_cr = db.cursor()
try:
# Try to grab an exclusive lock on the job row from within the task transaction
acquired_lock = False
task_cr.execute("""SELECT *
FROM ir_cron
WHERE id=%s
FOR UPDATE NOWAIT""",
(job['id'],), log_exceptions=False)
acquired_lock = True
except psycopg2.OperationalError, e:
if e.pgcode == '55P03':
# Class 55: Object not in prerequisite state; 55P03: lock_not_available
_logger.debug('Another process/thread is already busy executing job `%s`, skipping it.', job['name'])
continue
else:
# Unexpected OperationalError
raise
finally:
if not acquired_lock:
# we're exiting due to an exception while acquiring the lot
task_cr.close()
# Got the lock on the job row, run its code
_logger.debug('Starting job `%s`.', job['name'])
openerp.modules.registry.RegistryManager.check_registry_signaling(db_name)
registry = openerp.pooler.get_pool(db_name)
registry[cls._name]._process_job(task_cr, job)
openerp.modules.registry.RegistryManager.signal_caches_change(db_name)
return True
jobs = cr.dictfetchall()
except psycopg2.ProgrammingError, e:
if e.pgcode == '42P01':
# Class 42 — Syntax Error or Access Rule Violation; 42P01: undefined_table
@ -228,12 +199,43 @@ class ir_cron(osv.osv):
raise
except Exception:
_logger.warning('Exception in cron:', exc_info=True)
finally:
cr.commit()
cr.close()
return False
for job in jobs:
lock_cr = db.cursor()
try:
# Try to grab an exclusive lock on the job row from within the task transaction
lock_cr.execute("""SELECT *
FROM ir_cron
WHERE id=%s
FOR UPDATE NOWAIT""",
(job['id'],), log_exceptions=False)
# Got the lock on the job row, run its code
_logger.debug('Starting job `%s`.', job['name'])
job_cr = db.cursor()
try:
openerp.modules.registry.RegistryManager.check_registry_signaling(db_name)
registry = openerp.pooler.get_pool(db_name)
registry[cls._name]._process_job(job_cr, job, lock_cr)
openerp.modules.registry.RegistryManager.signal_caches_change(db_name)
except Exception:
_logger.exception('Unexpected exception while processing cron job %r', job)
finally:
job_cr.close()
except psycopg2.OperationalError, e:
if e.pgcode == '55P03':
# Class 55: Object not in prerequisite state; 55P03: lock_not_available
_logger.debug('Another process/thread is already busy executing job `%s`, skipping it.', job['name'])
continue
else:
# Unexpected OperationalError
raise
finally:
# we're exiting due to an exception while acquiring the lock
lock_cr.close()
def _try_lock(self, cr, uid, ids, context=None):
"""Try to grab a dummy exclusive write-lock to the rows with the given ids,

View File

@ -199,7 +199,7 @@ class ir_model(osv.osv):
def instanciate(self, cr, user, model, context=None):
class x_custom_model(osv.osv):
pass
_custom = True
x_custom_model._name = model
x_custom_model._module = False
a = x_custom_model.create_instance(self.pool, cr)

View File

@ -177,7 +177,6 @@
<field name="fax"/>
<field name="email" widget="email"/>
<field name="title" domain="[('domain', '=', 'contact')]"
groups="base.group_no_one"
options='{"no_open": True}' attrs="{'invisible': [('is_company','=', True)]}" />
</group>
</group>

View File

@ -36,6 +36,11 @@ import time
import types
from pprint import pformat
try:
import psutil
except ImportError:
psutil = None
# TODO modules that import netsvc only for things from loglevels must be changed to use loglevels.
from loglevels import *
import tools
@ -273,6 +278,9 @@ def dispatch_rpc(service_name, method, params):
rpc_response_flag = rpc_response.isEnabledFor(logging.DEBUG)
if rpc_request_flag or rpc_response_flag:
start_time = time.time()
start_rss, start_vms = 0, 0
if psutil:
start_rss, start_vms = psutil.Process(os.getpid()).get_memory_info()
if rpc_request and rpc_response_flag:
log(rpc_request,logging.DEBUG,'%s.%s'%(service_name,method), replace_request_password(params))
@ -282,10 +290,14 @@ def dispatch_rpc(service_name, method, params):
if rpc_request_flag or rpc_response_flag:
end_time = time.time()
end_rss, end_vms = 0, 0
if psutil:
end_rss, end_vms = psutil.Process(os.getpid()).get_memory_info()
logline = '%s.%s time:%.3fs mem: %sk -> %sk (diff: %sk)' % (service_name, method, end_time - start_time, start_vms / 1024, end_vms / 1024, (end_vms - start_vms)/1024)
if rpc_response_flag:
log(rpc_response,logging.DEBUG,'%s.%s time:%.3fs '%(service_name,method,end_time - start_time), result)
log(rpc_response,logging.DEBUG, logline, result)
else:
log(rpc_request,logging.DEBUG,'%s.%s time:%.3fs '%(service_name,method,end_time - start_time), replace_request_password(params), depth=1)
log(rpc_request,logging.DEBUG, logline, replace_request_password(params), depth=1)
return result
except openerp.exceptions.AccessError:

View File

@ -629,7 +629,8 @@ class MetaModel(type):
self._module = module_name
# Remember which models to instanciate for this module.
self.module_to_models.setdefault(self._module, []).append(self)
if not self._custom:
self.module_to_models.setdefault(self._module, []).append(self)
# Definition of log access columns, automatically added to models if
@ -666,6 +667,7 @@ class BaseModel(object):
_name = None
_columns = {}
_constraints = []
_custom = False
_defaults = {}
_rec_name = None
_parent_name = 'parent_id'
@ -942,7 +944,8 @@ class BaseModel(object):
# managed by the metaclass.
module_model_list = MetaModel.module_to_models.setdefault(cls._module, [])
if cls not in module_model_list:
module_model_list.append(cls)
if not cls._custom:
module_model_list.append(cls)
# Since we don't return an instance here, the __init__
# method won't be called.

View File

@ -30,7 +30,7 @@ RELEASE_LEVELS_DISPLAY = {ALPHA: ALPHA,
# properly comparable using normal operarors, for example:
# (6,1,0,'beta',0) < (6,1,0,'candidate',1) < (6,1,0,'candidate',2)
# (6,1,0,'candidate',2) < (6,1,0,'final',0) < (6,1,2,'final',0)
version_info = (7, 0, 0, ALPHA, 0)
version_info = (7, 0, 0, FINAL, 0)
version = '.'.join(map(str, version_info[:2])) + RELEASE_LEVELS_DISPLAY[version_info[3]] + str(version_info[4] or '')
serie = major_version = '.'.join(map(str, version_info[:2]))

View File

@ -617,9 +617,7 @@ class report_sxw(report_rml, preprocess.report):
create_doc = self.generators[mime_type]
odt = etree.tostring(create_doc(rml_dom, rml_parser.localcontext),
encoding='utf-8', xml_declaration=True)
sxw_z = zipfile.ZipFile(sxw_io, mode='a')
sxw_z.writestr('content.xml', odt)
sxw_z.writestr('meta.xml', meta)
sxw_contents = {'content.xml':odt, 'meta.xml':meta}
if report_xml.header:
#Add corporate header/footer
@ -638,12 +636,25 @@ class report_sxw(report_rml, preprocess.report):
rml_parser._add_header(odt)
odt = etree.tostring(odt, encoding='utf-8',
xml_declaration=True)
sxw_z.writestr('styles.xml', odt)
sxw_contents['styles.xml'] = odt
finally:
rml_file.close()
sxw_z.close()
final_op = sxw_io.getvalue()
#created empty zip writing sxw contents to avoid duplication
sxw_out = StringIO.StringIO()
sxw_out_zip = zipfile.ZipFile(sxw_out, mode='w')
sxw_template_zip = zipfile.ZipFile (sxw_io, 'r')
for item in sxw_template_zip.infolist():
if item.filename not in sxw_contents:
buffer = sxw_template_zip.read(item.filename)
sxw_out_zip.writestr(item.filename, buffer)
for item_filename, buffer in sxw_contents.iteritems():
sxw_out_zip.writestr(item_filename, buffer)
sxw_template_zip.close()
sxw_out_zip.close()
final_op = sxw_out.getvalue()
sxw_io.close()
sxw_out.close()
return final_op, mime_type
def create_single_html2html(self, cr, uid, ids, data, report_xml, context=None):

View File

@ -379,12 +379,17 @@ class WorkerCron(Worker):
time.sleep(interval)
def process_work(self):
rpc_request = logging.getLogger('openerp.netsvc.rpc.request')
rpc_request_flag = rpc_request.isEnabledFor(logging.DEBUG)
_logger.debug("WorkerCron (%s) polling for jobs", self.pid)
if config['db_name']:
db_names = config['db_name'].split(',')
else:
db_names = openerp.netsvc.ExportService._services['db'].exp_list(True)
for db_name in db_names:
if rpc_request_flag:
start_time = time.time()
start_rss, start_vms = psutil.Process(os.getpid()).get_memory_info()
while True:
# acquired = openerp.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name)
# TODO why isnt openerp.addons.base defined ?
@ -395,7 +400,12 @@ class WorkerCron(Worker):
# dont keep cursors in multi database mode
if len(db_names) > 1:
openerp.sql_db.close_db(db_name)
# TODO Each job should be considered as one request instead of each db
if rpc_request_flag:
end_time = time.time()
end_rss, end_vms = psutil.Process(os.getpid()).get_memory_info()
logline = '%s time:%.3fs mem: %sk -> %sk (diff: %sk)' % (db_name, end_time - start_time, start_vms / 1024, end_vms / 1024, (end_vms - start_vms)/1024)
_logger.debug("WorkerCron (%s) %s", self.pid, logline)
# TODO Each job should be considered as one request instead of each run
self.request_count += 1
def start(self):

View File

@ -43,6 +43,47 @@ test12</font></div><div><font color="#1f1f1f" face="monospace" size="2"><br></fo
<a href="javascript:alert('malicious code')">test link</a>
"""
EDI_LIKE_HTML_SOURCE = """<div style="font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: #FFF; ">
<p>Hello ${object.partner_id.name},</p>
<p>A new invoice is available for you: </p>
<p style="border-left: 1px solid #8e0000; margin-left: 30px;">
&nbsp;&nbsp;<strong>REFERENCES</strong><br />
&nbsp;&nbsp;Invoice number: <strong>${object.number}</strong><br />
&nbsp;&nbsp;Invoice total: <strong>${object.amount_total} ${object.currency_id.name}</strong><br />
&nbsp;&nbsp;Invoice date: ${object.date_invoice}<br />
&nbsp;&nbsp;Order reference: ${object.origin}<br />
&nbsp;&nbsp;Your contact: <a href="mailto:${object.user_id.email or ''}?subject=Invoice%20${object.number}">${object.user_id.name}</a>
</p>
<br/>
<p>It is also possible to directly pay with Paypal:</p>
<a style="margin-left: 120px;" href="${object.paypal_url}">
<img class="oe_edi_paypal_button" src="https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif"/>
</a>
<br/>
<p>If you have any question, do not hesitate to contact us.</p>
<p>Thank you for choosing ${object.company_id.name or 'us'}!</p>
<br/>
<br/>
<div style="width: 375px; margin: 0px; padding: 0px; background-color: #8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; background-repeat: repeat no-repeat;">
<h3 style="margin: 0px; padding: 2px 14px; font-size: 12px; color: #DDD;">
<strong style="text-transform:uppercase;">${object.company_id.name}</strong></h3>
</div>
<div style="width: 347px; margin: 0px; padding: 5px 14px; line-height: 16px; background-color: #F2F2F2;">
<span style="color: #222; margin-bottom: 5px; display: block; ">
${object.company_id.street}<br/>
${object.company_id.street2}<br/>
${object.company_id.zip} ${object.company_id.city}<br/>
${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''}<br/>
</span>
<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
Phone:&nbsp; ${object.company_id.phone}
</div>
<div>
Web :&nbsp;<a href="${object.company_id.website}">${object.company_id.website}</a>
</div>
</div>
</div></body></html>"""
TEXT_MAIL1 = """I contact you about our meeting for tomorrow. Here is the schedule I propose:
9 AM: brainstorming about our new amazing business app</span></li>
9.45 AM: summary
@ -126,23 +167,85 @@ bert.tartopoils@miam.miam
class TestSanitizer(unittest2.TestCase):
""" Test the html sanitizer that filters html to remove unwanted attributes """
def test_simple(self):
x = "yop"
self.assertEqual(x, html_sanitize(x))
def test_basic_sanitizer(self):
cases = [
("yop", "<p>yop</p>"), # simple
("lala<p>yop</p>xxx", "<div><p>lala</p><p>yop</p>xxx</div>"), # trailing text
("Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci",
u"<p>Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci</p>"), # unicode
]
for content, expected in cases:
html = html_sanitize(content)
self.assertEqual(html, expected, 'html_sanitize is broken')
def test_trailing_text(self):
x = 'lala<p>yop</p>xxx'
self.assertEqual(x, html_sanitize(x))
def test_evil_malicious_code(self):
# taken from https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Tests
cases = [
("<IMG SRC=javascript:alert('XSS')>"), # no quotes and semicolons
("<IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>"), # UTF-8 Unicode encoding
("<IMG SRC=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>"), # hex encoding
("<IMG SRC=\"jav&#x0D;ascript:alert('XSS');\">"), # embedded carriage return
("<IMG SRC=\"jav&#x0A;ascript:alert('XSS');\">"), # embedded newline
("<IMG SRC=\"jav ascript:alert('XSS');\">"), # embedded tab
("<IMG SRC=\"jav&#x09;ascript:alert('XSS');\">"), # embedded encoded tab
("<IMG SRC=\" &#14; javascript:alert('XSS');\">"), # spaces and meta-characters
("<IMG SRC=\"javascript:alert('XSS')\""), # half-open html
("<IMG \"\"\"><SCRIPT>alert(\"XSS\")</SCRIPT>\">"), # malformed tag
("<SCRIPT/XSS SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT>"), # non-alpha-non-digits
("<SCRIPT/SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT>"), # non-alpha-non-digits
("<<SCRIPT>alert(\"XSS\");//<</SCRIPT>"), # extraneous open brackets
("<SCRIPT SRC=http://ha.ckers.org/xss.js?< B >"), # non-closing script tags
("<INPUT TYPE=\"IMAGE\" SRC=\"javascript:alert('XSS');\">"), # input image
("<BODY BACKGROUND=\"javascript:alert('XSS')\">"), # body image
("<IMG DYNSRC=\"javascript:alert('XSS')\">"), # img dynsrc
("<IMG LOWSRC=\"javascript:alert('XSS')\">"), # img lowsrc
("<TABLE BACKGROUND=\"javascript:alert('XSS')\">"), # table
("<TABLE><TD BACKGROUND=\"javascript:alert('XSS')\">"), # td
("<DIV STYLE=\"background-image: url(javascript:alert('XSS'))\">"), # div background
("<DIV STYLE=\"background-image:\0075\0072\006C\0028'\006a\0061\0076\0061\0073\0063\0072\0069\0070\0074\003a\0061\006c\0065\0072\0074\0028.1027\0058.1053\0053\0027\0029'\0029\">"), # div background with unicoded exploit
("<DIV STYLE=\"background-image: url(&#1;javascript:alert('XSS'))\">"), # div background + extra characters
("<IMG SRC='vbscript:msgbox(\"XSS\")'>"), # VBscrip in an image
("<BODY ONLOAD=alert('XSS')>"), # event handler
("<BR SIZE=\"&{alert('XSS')}\>"), # & javascript includes
("<LINK REL=\"stylesheet\" HREF=\"javascript:alert('XSS');\">"), # style sheet
("<LINK REL=\"stylesheet\" HREF=\"http://ha.ckers.org/xss.css\">"), # remote style sheet
("<STYLE>@import'http://ha.ckers.org/xss.css';</STYLE>"), # remote style sheet 2
("<META HTTP-EQUIV=\"Link\" Content=\"<http://ha.ckers.org/xss.css>; REL=stylesheet\">"), # remote style sheet 3
("<STYLE>BODY{-moz-binding:url(\"http://ha.ckers.org/xssmoz.xml#xss\")}</STYLE>"), # remote style sheet 4
("<IMG STYLE=\"xss:expr/*XSS*/ession(alert('XSS'))\">"), # style attribute using a comment to break up expression
("""<!--[if gte IE 4]>
<SCRIPT>alert('XSS');</SCRIPT>
<![endif]-->"""), # down-level hidden block
]
for content in cases:
html = html_sanitize(content)
self.assertNotIn('javascript', html, 'html_sanitize did not remove a malicious javascript')
self.assertTrue('ha.ckers.org' not in html or 'http://ha.ckers.org/xss.css' in html, 'html_sanitize did not remove a malicious code in %s (%s)' % (content, html))
def test_html(self):
sanitized_html = html_sanitize(HTML_SOURCE)
for tag in ['<font>', '<div>', '<b>', '<i>', '<u>', '<strike>', '<li>', '<blockquote>', '<a href']:
for tag in ['<div', '<b', '<i', '<u', '<strike', '<li', '<blockquote', '<a href']:
self.assertIn(tag, sanitized_html, 'html_sanitize stripped too much of original html')
for attr in ['style', 'javascript']:
for attr in ['javascript']:
self.assertNotIn(attr, sanitized_html, 'html_sanitize did not remove enough unwanted attributes')
def test_unicode(self):
html_sanitize("Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci")
emails =[("Charles <charles.bidule@truc.fr>", "Charles &lt;charles.bidule@truc.fr&gt;"),
("Dupuis <'tr/-: ${dupuis#$'@truc.baz.fr>", "Dupuis &lt;'tr/-: ${dupuis#$'@truc.baz.fr&gt;"),
("Technical <service/technical+2@open.com>", "Technical &lt;service/technical+2@open.com&gt;"),
("Div nico <div-nico@open.com>", "Div nico &lt;div-nico@open.com&gt;")]
for email in emails:
self.assertIn(email[1], html_sanitize(email[0]), 'html_sanitize stripped emails of original html')
def test_edi_source(self):
html = html_sanitize(EDI_LIKE_HTML_SOURCE)
self.assertIn('div style="font-family: \'Lucica Grande\', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: #FFF;', html,
'html_sanitize removed valid style attribute')
self.assertIn('<span style="color: #222; margin-bottom: 5px; display: block; ">', html,
'html_sanitize removed valid style attribute')
self.assertIn('img class="oe_edi_paypal_button" src="https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif"', html,
'html_sanitize removed valid img')
self.assertNotIn('</body></html>', html, 'html_sanitize did not remove extra closing tags')
class TestCleaner(unittest2.TestCase):
@ -181,6 +284,7 @@ class TestCleaner(unittest2.TestCase):
new_html = html_email_clean(u'<?xml version="1.0" encoding="iso-8859-1"?>\n<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"\n "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">\n <head>\n <title>404 - Not Found</title>\n </head>\n <body>\n <h1>404 - Not Found</h1>\n </body>\n</html>\n')
self.assertNotIn('encoding', new_html, 'html_email_cleaner did not remove correctly encoding attributes')
class TestHtmlTools(unittest2.TestCase):
""" Test some of our generic utility functions about html """

View File

@ -284,10 +284,10 @@ class configmanager(object):
help="Specify the number of workers, 0 disable prefork mode.",
type="int")
group.add_option("--limit-memory-soft", dest="limit_memory_soft", my_default=640 * 1024 * 1024,
help="Maximum allowed virtual memory per worker, when reached the worker be reset after the current request (default 640M).",
help="Maximum allowed virtual memory per worker, when reached the worker be reset after the current request (default 671088640 aka 640MB).",
type="int")
group.add_option("--limit-memory-hard", dest="limit_memory_hard", my_default=768 * 1024 * 1024,
help="Maximum allowed virtual memory per worker, when reached, any memory allocation will fail (default 768M).",
help="Maximum allowed virtual memory per worker, when reached, any memory allocation will fail (default 805306368 aka 768MB).",
type="int")
group.add_option("--limit-time-cpu", dest="limit_time_cpu", my_default=60,
help="Maximum allowed CPU time per request (default 60).",

View File

@ -23,8 +23,8 @@ from lxml import etree
import cgi
import logging
import lxml.html
import lxml.html.clean as clean
import openerp.pooler as pooler
import operator
import random
import re
import socket
@ -40,71 +40,32 @@ _logger = logging.getLogger(__name__)
# HTML Sanitizer
#----------------------------------------------------------
tags_to_kill = ["script", "head", "meta", "title", "link", "style", "frame", "iframe", "base", "object", "embed"]
tags_to_remove = ['html', 'body', 'font']
def html_sanitize(src):
if not src:
return src
src = ustr(src, errors='replace')
root = lxml.html.fromstring(u"<div>%s</div>" % src)
result = handle_element(root)
res = []
for element in children(result[0]):
if isinstance(element, basestring):
res.append(element)
else:
element.tail = ""
res.append(lxml.html.tostring(element))
return ''.join(res)
# FIXME: shouldn't this be a whitelist rather than a blacklist?!
to_remove = set(["script", "head", "meta", "title", "link", "img"])
to_unwrap = set(["html", "body"])
javascript_regex = re.compile(r"^\s*javascript\s*:.*$", re.IGNORECASE)
def handle_a(el, new):
href = el.get("href", "#")
if javascript_regex.search(href):
href = "#"
new.set("href", href)
special = {
"a": handle_a,
}
def handle_element(element):
if isinstance(element, basestring):
return [element]
if element.tag in to_remove:
return []
if element.tag in to_unwrap:
return reduce(operator.add, [handle_element(x) for x in children(element)])
result = lxml.html.fromstring("<%s />" % element.tag)
for c in children(element):
append_to(handle_element(c), result)
if element.tag in special:
special[element.tag](element, result)
return [result]
def children(node):
res = []
if node.text is not None:
res.append(node.text)
for child_node in node.getchildren():
res.append(child_node)
if child_node.tail is not None:
res.append(child_node.tail)
return res
def append_to(elements, dest_node):
for element in elements:
if isinstance(element, basestring):
children = dest_node.getchildren()
if len(children) == 0:
dest_node.text = element
else:
children[-1].tail = element
else:
dest_node.append(element)
# html encode email tags
part = re.compile(r"(<[^<>]+@[^<>]+>)", re.IGNORECASE | re.DOTALL)
src = part.sub(lambda m: cgi.escape(m.group(1)), src)
# some corner cases make the parser crash (such as <SCRIPT/XSS SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT> in test_mail)
try:
cleaner = clean.Cleaner(page_structure=True, style=False, safe_attrs_only=False, forms=False, kill_tags=tags_to_kill, remove_tags=tags_to_remove)
cleaned = cleaner.clean_html(src)
except TypeError, e:
# lxml.clean version < 2.3.1 does not have a kill_tags attribute
# to remove in 2014
cleaner = clean.Cleaner(page_structure=True, style=False, safe_attrs_only=False, forms=False, remove_tags=tags_to_kill+tags_to_remove)
cleaned = cleaner.clean_html(src)
except:
_logger.warning('html_sanitize failed to parse %s' % (src))
cleaned = '<p>Impossible to parse</p>'
return cleaned
#----------------------------------------------------------

View File

@ -0,0 +1,61 @@
import argparse
import textwrap
from .call import Call
from .client import Open, Show, ConsumeNothing, ConsumeMemory, LeakMemory, ConsumeCPU
from .benchmarks import Bench, BenchRead, BenchFieldsViewGet, BenchDummy, BenchLogin
from .bench_sale_mrp import BenchSaleMrp
from . import common
from . import conf # Not really server-side (in the `for` below).
from . import drop
from . import initialize
from . import model
from . import module
from . import read
from . import run_tests
from . import scaffold
from . import uninstall
from . import update
command_list_server = (conf, drop, initialize, model, module, read, run_tests,
scaffold, uninstall, update, )
command_list_client = (Call, Open, Show, ConsumeNothing, ConsumeMemory,
LeakMemory, ConsumeCPU, Bench, BenchRead,
BenchFieldsViewGet, BenchDummy, BenchLogin,
BenchSaleMrp, )
def main_parser():
parser = argparse.ArgumentParser(
usage=argparse.SUPPRESS,
description=textwrap.fill(textwrap.dedent("""\
OpenERP Command provides a set of command-line tools around
the OpenERP framework: openobject-server. All the tools are
sub-commands of a single oe executable.""")),
epilog="""Use <command> --help to get information about the command.""",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
description = []
for x in command_list_server:
description.append(x.__name__[len(__package__)+1:])
if x.__doc__:
description.extend([
":\n",
textwrap.fill(str(x.__doc__).strip(),
subsequent_indent=' ',
initial_indent=' '),
])
description.append("\n\n")
subparsers = parser.add_subparsers(
title="Available commands",
help=argparse.SUPPRESS,
description="".join(description[:-1]),
)
# Server-side commands.
for x in command_list_server:
x.add_parser(subparsers)
# Client-side commands. TODO one per .py file.
for x in command_list_client:
x(subparsers)
return parser

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
# Nothing here, the module provides only data.
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
{
'name': 'bench_sale_mrp',
'version': '0.1',
'category': 'Benchmarks',
'description': """Prepare some data to run a benchmark.""",
'author': 'OpenERP SA',
'maintainer': 'OpenERP SA',
'website': 'http://www.openerp.com',
'depends': ['base', 'sale_mrp'],
'data': ['data.yml'],
'installable': True,
'auto_install': False,
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,41 @@
-
This is a subset of `sale_mrp/test/sale_mrp.yml`.
-
I define a product category `Mobile Products Sellable`.
-
!record {model: product.category, id: my_product_category_0}:
name: Mobile Products Sellable
-
I define a product `Slider Mobile`
-
!record {model: product.product, id: my_slider_mobile_0}:
categ_id: my_product_category_0
cost_method: standard
list_price: 200.0
mes_type: fixed
name: Slider Mobile
procure_method: make_to_order
seller_delay: '1'
seller_ids:
- delay: 1
name: base.res_partner_agrolait
min_qty: 2.0
qty: 5.0
standard_price: 189.0
supply_method: produce
type: product
uom_id: product.product_uom_unit
uom_po_id: product.product_uom_unit
-
I create a Bill of Material for the `Slider Mobile` product.
-
!record {model: mrp.bom, id: mrp_bom_slidermobile0}:
company_id: base.main_company
name: Slider Mobile
product_efficiency: 1.0
product_id: my_slider_mobile_0
product_qty: 1.0
product_uom: product.product_uom_unit
product_uos_qty: 0.0
sequence: 0.0
type: normal

View File

@ -0,0 +1,68 @@
"""
Benchmark based on the `sale_mrp` addons (in `sale_mrp/test/sale_mrp.yml`).
"""
import time
from .benchmarks import Bench
class BenchSaleMrp(Bench):
"""\
Similar to `sale_mrp/test/sale_mrp.yml`.
This benchmarks the OpenERP server `sale_mrp` module by creating and
confirming a sale order. As it creates data in the server, it is necessary
to ensure unique names for the newly created data. You can use the --seed
argument to give a lower bound to those names. (The number of generated
names is --jobs * --samples.)
"""
command_name = 'bench-sale-mrp'
bench_name = '`sale_mrp/test/sale_mrp.yml`'
def measure_once(self, i):
if self.worker >= 0:
i = int(self.args.seed) + i + (self.worker * int(self.args.samples))
else:
i = int(self.args.seed) + i
# Resolve a few external-ids (this has little impact on the running
# time of the whole method).
product_uom_unit = self.execute('ir.model.data', 'get_object_reference', 'product', 'product_uom_unit')[1]
my_slider_mobile_0 = self.execute('ir.model.data', 'get_object_reference', 'bench_sale_mrp', 'my_slider_mobile_0')[1]
res_partner_4 = self.execute('ir.model.data', 'get_object_reference', 'base', 'res_partner_4')[1]
res_partner_address_7 = self.execute('ir.model.data', 'get_object_reference', 'base', 'res_partner_address_7')[1]
list0 = self.execute('ir.model.data', 'get_object_reference', 'product', 'list0')[1]
shop = self.execute('ir.model.data', 'get_object_reference', 'sale', 'shop')[1]
# Create a sale order for the product `Slider Mobile`.
data = {
'client_order_ref': 'ref_xxx_' + str(i).rjust(6, '0'),
'date_order': time.strftime('%Y-%m-%d'),
'invoice_quantity': 'order',
'name': 'sale_order_ref_xxx_' + str(i).rjust(6, '0'),
'order_line': [(0, 0, {
'name': 'Slider Mobile',
'price_unit': 2,
'product_uom': product_uom_unit,
'product_uom_qty': 5.0,
'state': 'draft',
'delay': 7.0,
'product_id': my_slider_mobile_0,
'product_uos_qty': 5,
'type': 'make_to_order',
})],
'order_policy': 'manual',
'partner_id': res_partner_4,
'partner_invoice_id': res_partner_address_7,
'partner_order_id': res_partner_address_7,
'partner_shipping_id': res_partner_address_7,
'picking_policy': 'direct',
'pricelist_id': list0,
'shop_id': shop,
}
sale_order_id = self.execute('sale.order', 'create', data, {})
# Confirm the sale order.
self.object_proxy.exec_workflow(self.database, self.uid, self.password, 'sale.order', 'order_confirm', sale_order_id, {})

View File

@ -0,0 +1,166 @@
"""
Define a base class for client-side benchmarking.
"""
import hashlib
import multiprocessing
import sys
import time
from .client import Client
class Bench(Client):
"""
Base class for concurrent benchmarks. The measure_once() method must be
overriden.
Each sub-benchmark will be run in its own process then a report is done
with all the results (shared with the main process using a
`multiprocessing.Array`).
"""
def __init__(self, subparsers=None):
super(Bench, self).__init__(subparsers)
self.parser.add_argument('-n', '--samples', metavar='INT',
default=100, help='number of measurements to take')
# TODO if -n <int>s is given (instead of -n <int>), run the
# benchmark for <int> seconds and return the number of iterations.
self.parser.add_argument('-o', '--output', metavar='PATH',
required=True, help='path to save the generated report')
self.parser.add_argument('--append', action='store_true',
default=False, help='append the report to an existing file')
self.parser.add_argument('-j', '--jobs', metavar='JOBS',
default=1, help='number of concurrent workers')
self.parser.add_argument('--seed', metavar='SEED',
default=0, help='a value to ensure different runs can create unique data')
self.worker = -1
def work(self, iarr=None):
if iarr:
# If an array is given, it means we are a worker process...
self.work_slave(iarr)
else:
# ... else we are the main process and we will spawn workers,
# passing them an array.
self.work_master()
def work_master(self):
N = int(self.args.samples)
self.arrs = [(i, multiprocessing.Array('f', range(N)))
for i in xrange(int(self.args.jobs))]
ps = [multiprocessing.Process(target=self.run, args=(arr,))
for arr in self.arrs]
[p.start() for p in ps]
[p.join() for p in ps]
self.report_html()
def work_slave(self, iarr):
j, arr = iarr
self.worker = j
N = int(self.args.samples)
total_t0 = time.time()
for i in xrange(N):
t0 = time.time()
self.measure_once(i)
t1 = time.time()
arr[i] = t1 - t0
print >> sys.stdout, '\r%s' % ('|' * (i * 60 / N)),
print >> sys.stdout, '%s %s%%' % \
(' ' * (60 - (i * 60 / N)), int(float(i+1)/N*100)),
sys.stdout.flush()
total_t1 = time.time()
print '\nDone in %ss.' % (total_t1 - total_t0)
def report_html(self):
series = []
for arr in self.arrs:
serie = """{
data: %s,
points: { show: true }
}""" % ([[x, i] for i, x in enumerate(arr)],)
series.append(serie)
chart_id = hashlib.md5(" ".join(sys.argv)).hexdigest()
HEADER = """<!doctype html>
<title>Benchmarks</title>
<meta charset=utf-8>
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="js/jquery.flot.js"></script>
"""
CONTENT = """<h1>%s</h1>
%s
<div id='chart_%s' style='width:400px;height:300px;'>...</div>
<script type="text/javascript">
$.plot($("#chart_%s"), [%s],
{yaxis: { ticks: false }});
</script>""" % (self.bench_name, ' '.join(sys.argv), chart_id, chart_id,
','.join(series))
if self.args.append:
with open(self.args.output, 'a') as f:
f.write(CONTENT,)
else:
with open(self.args.output, 'w') as f:
f.write(HEADER + CONTENT,)
def measure_once(self, i):
"""
The `measure_once` method is called --jobs times. A `i` argument is
supplied to allow to create unique values for each execution (e.g. to
supply fresh identifiers to a `create` method.
"""
pass
class BenchRead(Bench):
"""Read a record repeatedly."""
command_name = 'bench-read'
bench_name = 'res.users.read(1)'
def __init__(self, subparsers=None):
super(BenchRead, self).__init__(subparsers)
self.parser.add_argument('-m', '--model', metavar='MODEL',
required=True, help='the model')
self.parser.add_argument('-i', '--id', metavar='RECORDID',
required=True, help='the record id')
def measure_once(self, i):
self.execute(self.args.model, 'read', [self.args.id], [])
class BenchFieldsViewGet(Bench):
"""Read a record's fields and view architecture repeatedly."""
command_name = 'bench-view'
bench_name = 'res.users.fields_view_get(1)'
def __init__(self, subparsers=None):
super(BenchFieldsViewGet, self).__init__(subparsers)
self.parser.add_argument('-m', '--model', metavar='MODEL',
required=True, help='the model')
self.parser.add_argument('-i', '--id', metavar='RECORDID',
required=True, help='the record id')
def measure_once(self, i):
self.execute(self.args.model, 'fields_view_get', self.args.id)
class BenchDummy(Bench):
"""Dummy (call test.limits.model.consume_nothing())."""
command_name = 'bench-dummy'
bench_name = 'test.limits.model.consume_nothing()'
def __init__(self, subparsers=None):
super(BenchDummy, self).__init__(subparsers)
self.parser.add_argument('-a', '--args', metavar='ARGS',
default='', help='some arguments to serialize')
def measure_once(self, i):
self.execute('test.limits.model', 'consume_nothing')
class BenchLogin(Bench):
"""Login (update res_users.date)."""
command_name = 'bench-login'
bench_name = 'res.users.login(1)'
def measure_once(self, i):
self.common_proxy.login(self.database, self.user, self.password)

44
openerpcommand/call.py Normal file
View File

@ -0,0 +1,44 @@
"""
Call an arbitrary model's method.
"""
import ast
import os
import pprint
import sys
import time
import xmlrpclib
import client
class Call(client.Client):
"""\
Call an arbitrary model's method.
Example:
> oe call res.users.read '[1, 3]' '[]' -u 1 -p admin
"""
# TODO The above docstring is completely borked in the
# --help message.
command_name = 'call'
def __init__(self, subparsers=None):
super(Call, self).__init__(subparsers)
self.parser.add_argument('call', metavar='MODEL.METHOD',
help='the model and the method to call, using the '
'<model>.<method> format.')
self.parser.add_argument('args', metavar='ARGUMENT',
nargs='+',
help='the argument for the method call, must be '
'`ast.literal_eval` compatible. Can be repeated.')
def work(self):
try:
model, method = self.args.call.rsplit('.', 1)
except:
print "Invalid syntax `%s` must have the form <model>.<method>."
sys.exit(1)
args = tuple(map(ast.literal_eval, self.args.args)) if self.args.args else ()
x = self.execute(model, method, *args)
pprint.pprint(x, indent=4)

137
openerpcommand/client.py Normal file
View File

@ -0,0 +1,137 @@
"""
Define a few common arguments for client-side command-line tools.
"""
import os
import sys
import time
import xmlrpclib
import common
class Client(common.Command):
"""
Base class for XML-RPC command-line clients. It must be inherited and the
work() method overriden.
"""
def __init__(self, subparsers=None):
super(Client, self).__init__(subparsers)
required_or_default = common.required_or_default
self.parser.add_argument('-H', '--host', metavar='HOST',
**required_or_default('HOST', 'the server host'))
self.parser.add_argument('-P', '--port', metavar='PORT',
**required_or_default('PORT', 'the server port'))
def execute(self, *args):
return self.object_proxy.execute(self.database, self.uid, self.password, *args)
def initialize(self):
self.host = self.args.host
self.port = int(self.args.port)
self.database = self.args.database
self.user = self.args.user
self.password = self.args.password
self.url = 'http://%s:%d/xmlrpc/' % (self.host, self.port)
self.common_proxy = xmlrpclib.ServerProxy(self.url + 'common')
self.object_proxy = xmlrpclib.ServerProxy(self.url + 'object')
try:
self.uid = int(self.user)
except ValueError, e:
self.uid = self.common_proxy.login(self.database, self.user, self.password)
def run(self, *args):
self.initialize()
self.work(*args)
def work(self, *args):
pass
class Open(Client):
"""Get the web client's URL to view a specific model."""
command_name = 'open'
def __init__(self, subparsers=None):
super(Open, self).__init__(subparsers)
self.parser.add_argument('-m', '--model', metavar='MODEL',
required=True, help='the view type')
self.parser.add_argument('-v', '--view-mode', metavar='VIEWMODE',
default='tree', help='the view mode')
def work(self):
ids = self.execute('ir.actions.act_window', 'search', [
('res_model', '=', self.args.model),
('view_mode', 'like', self.args.view_mode),
])
xs = self.execute('ir.actions.act_window', 'read', ids, [])
for x in xs:
print x['id'], x['name']
d = {}
d['host'] = self.host
d['port'] = self.port
d['action_id'] = x['id']
print " http://%(host)s:%(port)s/web/webclient/home#action_id=%(action_id)s" % d
class Show(Client):
"""Display a record."""
command_name = 'show'
def __init__(self, subparsers=None):
super(Show, self).__init__(subparsers)
self.parser.add_argument('-m', '--model', metavar='MODEL',
required=True, help='the model')
self.parser.add_argument('-i', '--id', metavar='RECORDID',
required=True, help='the record id')
def work(self):
xs = self.execute(self.args.model, 'read', [self.args.id], [])
if xs:
x = xs[0]
print x['name']
else:
print "Record not found."
class ConsumeNothing(Client):
"""Call test.limits.model.consume_nothing()."""
command_name = 'consume-nothing'
def work(self):
xs = self.execute('test.limits.model', 'consume_nothing')
class ConsumeMemory(Client):
"""Call test.limits.model.consume_memory()."""
command_name = 'consume-memory'
def __init__(self, subparsers=None):
super(ConsumeMemory, self).__init__(subparsers)
self.parser.add_argument('--size', metavar='SIZE',
required=True, help='size of the list to allocate')
def work(self):
xs = self.execute('test.limits.model', 'consume_memory', int(self.args.size))
class LeakMemory(ConsumeMemory):
"""Call test.limits.model.leak_memory()."""
command_name = 'leak-memory'
def work(self):
xs = self.execute('test.limits.model', 'leak_memory', int(self.args.size))
class ConsumeCPU(Client):
"""Call test.limits.model.consume_cpu_time()."""
command_name = 'consume-cpu'
def __init__(self, subparsers=None):
super(ConsumeCPU, self).__init__(subparsers)
self.parser.add_argument('--seconds', metavar='INT',
required=True, help='how much CPU time to consume')
def work(self):
xs = self.execute('test.limits.model', 'consume_cpu_time', int(self.args.seconds))

88
openerpcommand/common.py Normal file
View File

@ -0,0 +1,88 @@
"""
Define a few common arguments for server-side command-line tools.
"""
import argparse
import os
import sys
def add_addons_argument(parser):
"""
Add a common --addons argument to a parser.
"""
parser.add_argument('--addons', metavar='ADDONS',
**required_or_default('ADDONS',
'colon-separated list of paths to addons'))
def get_addons_from_paths(paths, exclude):
"""
Build a list of available modules from a list of addons paths.
"""
exclude = exclude or []
module_names = []
for p in paths:
if os.path.exists(p):
names = list(set(os.listdir(p)))
names = filter(lambda a: not (a.startswith('.') or a in exclude), names)
module_names.extend(names)
else:
print "The addons path `%s` doesn't exist." % p
sys.exit(1)
return module_names
def required_or_default(name, h):
"""
Helper to define `argparse` arguments. If the name is the environment,
the argument is optional and draw its value from the environment if not
supplied on the command-line. If it is not in the environment, make it
a mandatory argument.
"""
if os.environ.get('OPENERP_' + name.upper()):
d = {'default': os.environ['OPENERP_' + name.upper()]}
else:
d = {'required': True}
d['help'] = h + '. The environment variable OPENERP_' + \
name.upper() + ' can be used instead.'
return d
class Command(object):
"""
Base class to create command-line tools. It must be inherited and the
run() method overriden.
"""
command_name = 'stand-alone'
def __init__(self, subparsers=None):
if subparsers:
self.parser = parser = subparsers.add_parser(self.command_name,
description=self.__class__.__doc__)
else:
self.parser = parser = argparse.ArgumentParser(
description=self.__class__.__doc__)
parser.add_argument('-d', '--database', metavar='DATABASE',
**required_or_default('DATABASE', 'the database to connect to'))
parser.add_argument('-u', '--user', metavar='USER',
**required_or_default('USER', 'the user login or ID. When using '
'RPC, providing an ID avoid the login() step'))
parser.add_argument('-p', '--password', metavar='PASSWORD',
**required_or_default('PASSWORD', 'the user password')) # TODO read it from the command line or from file.
parser.set_defaults(run=self.run_with_args)
def run_with_args(self, args):
self.args = args
self.run()
def run(self):
print 'Stub Command.run().'
@classmethod
def stand_alone(cls):
"""
A single Command object is a complete command-line program. See
`openerp-command/stand-alone` for an example.
"""
command = cls()
args = command.parser.parse_args()
args.run(args)

25
openerpcommand/conf.py Normal file
View File

@ -0,0 +1,25 @@
"""
Display the currently used configuration. The configuration for any
sub-command is normally given by options. But some options can be specified
using environment variables. This sub-command shows those variables.
A `set` sub-command should be provided when the configuration is in a real
configuration file instead of environment variables.
"""
import os
import sys
import textwrap
def run(args):
for x in ('database', 'addons', 'host', 'port'):
x_ = ('openerp_' + x).upper()
if x_ in os.environ:
print '%s: %s' % (x, os.environ[x_])
else:
print '%s: <not set>' % (x, )
os.environ['OPENERP_DATABASE'] = 'yeah'
def add_parser(subparsers):
parser = subparsers.add_parser('conf',
description='Display the currently used configuration.')
parser.set_defaults(run=run)

45
openerpcommand/drop.py Normal file
View File

@ -0,0 +1,45 @@
"""
Drop a database.
"""
import common
# TODO turn template1 in a parameter
# This should be exposed from openerp (currently in
# openerp/service/web_services.py).
def drop_database(database_name):
import openerp
db = openerp.sql_db.db_connect('template1')
cr = db.cursor()
cr.autocommit(True) # avoid transaction block
try:
# TODO option for doing this.
# Try to terminate all other connections that might prevent
# dropping the database
try:
cr.execute("""SELECT pg_terminate_backend(procpid)
FROM pg_stat_activity
WHERE datname = %s AND
procpid != pg_backend_pid()""",
(database_name,))
except Exception:
pass
try:
cr.execute('DROP DATABASE "%s"' % database_name)
except Exception, e:
print "Can't drop %s" % (database_name,)
finally:
cr.close()
def run(args):
assert args.database
drop_database(args.database)
def add_parser(subparsers):
parser = subparsers.add_parser('drop',
description='Drop a database.')
parser.add_argument('-d', '--database', metavar='DATABASE',
**common.required_or_default('DATABASE', 'the database to create'))
parser.set_defaults(run=run)

View File

@ -0,0 +1,103 @@
"""
Install OpenERP on a new (by default) database.
"""
import os
import sys
import common
def install_openerp(database_name, create_database_flag, module_names, install_demo_data):
import openerp
config = openerp.tools.config
if create_database_flag:
create_database(database_name)
config['init'] = dict.fromkeys(module_names, 1)
# Install the import hook, to import openerp.addons.<module>.
openerp.modules.module.initialize_sys_path()
if hasattr(openerp.modules.loading, 'open_openerp_namespace'):
openerp.modules.loading.open_openerp_namespace()
registry = openerp.modules.registry.RegistryManager.get(
database_name, update_module=True, force_demo=install_demo_data)
return registry
# TODO turn template1 in a parameter
# This should be exposed from openerp (currently in
# openerp/service/web_services.py).
def create_database(database_name):
import openerp
db = openerp.sql_db.db_connect('template1')
cr = db.cursor() # TODO `with db as cr:`
try:
cr.autocommit(True)
cr.execute("""CREATE DATABASE "%s"
ENCODING 'unicode' TEMPLATE "template1" """ \
% (database_name,))
finally:
cr.close()
def run(args):
assert args.database
assert not (args.module and args.all_modules)
import openerp
config = openerp.tools.config
if args.tests:
config['log_handler'] = [':TEST']
config['test_enable'] = True
config['without_demo'] = False
else:
config['log_handler'] = [':CRITICAL']
config['test_enable'] = False
config['without_demo'] = True
if args.addons:
args.addons = args.addons.split(':')
else:
args.addons = []
config['addons_path'] = ','.join(args.addons)
if args.all_modules:
module_names = common.get_addons_from_paths(args.addons, args.exclude)
elif args.module:
module_names = args.module
else:
module_names = ['base']
openerp.netsvc.init_logger()
registry = install_openerp(args.database, not args.no_create, module_names, not config['without_demo'])
# The `_assertion_report` attribute was added on the registry during the
# OpenERP 7.0 development.
if hasattr(registry, '_assertion_report'):
sys.exit(1 if registry._assertion_report.failures else 0)
def add_parser(subparsers):
parser = subparsers.add_parser('initialize',
description='Create and initialize a new OpenERP database.')
parser.add_argument('-d', '--database', metavar='DATABASE',
**common.required_or_default('DATABASE', 'the database to create'))
common.add_addons_argument(parser)
parser.add_argument('--module', metavar='MODULE', action='append',
help='specify a module to install'
' (this option can be repeated)')
parser.add_argument('--all-modules', action='store_true',
help='install all visible modules (not compatible with --module)')
parser.add_argument('--no-create', action='store_true',
help='do not create the database, only initialize it')
parser.add_argument('--exclude', metavar='MODULE', action='append',
help='exclude a module from installation'
' (this option can be repeated)')
parser.add_argument('--tests', action='store_true',
help='run the tests as modules are installed'
' (use the `run-tests` command to choose specific'
' tests to run against an existing database).'
' Demo data are installed.')
parser.set_defaults(run=run)

7
openerpcommand/main.py Normal file
View File

@ -0,0 +1,7 @@
import openerpcommand
def run():
""" Main entry point for the openerp-command tool."""
parser = openerpcommand.main_parser()
args = parser.parse_args()
args.run(args)

61
openerpcommand/model.py Normal file
View File

@ -0,0 +1,61 @@
"""
Display information about a given model.
"""
import os
import sys
import textwrap
def run(args):
assert args.database
assert args.model
import openerp
openerp.tools.config['log_level'] = 100
openerp.netsvc.init_logger()
registry = openerp.modules.registry.RegistryManager.get(
args.database, update_module=False)
model = registry.get(args.model)
longest_k = 1
longest_string = 1
columns = model._columns
if args.field and args.field not in columns:
print "No such field."
sys.exit(1)
if args.field:
columns = { args.field: columns[args.field] }
else:
print "Fields (model `%s`, database `%s`):" % (args.model, args.database)
for k, v in columns.items():
longest_k = len(k) if longest_k < len(k) else longest_k
longest_string = len(v.string) \
if longest_string < len(v.string) else longest_string
for k, v in sorted(columns.items()):
attr = []
if v.required:
attr.append("Required")
if v.readonly:
attr.append("Read-only")
attr = '/'.join(attr)
attr = '(' + attr + ')' if attr else attr
if args.verbose:
print v.string, '-- ' + k + ', ' + v._type, attr
else:
print k.ljust(longest_k + 2), v._type, attr
if args.verbose and v.help:
print textwrap.fill(v.help, initial_indent=' ', subsequent_indent=' ')
def add_parser(subparsers):
parser = subparsers.add_parser('model',
description='Display information about a given model for an existing database.')
parser.add_argument('-d', '--database', metavar='DATABASE', required=True,
help='the database to connect to')
parser.add_argument('-m', '--model', metavar='MODEL', required=True,
help='the model for which information should be displayed')
parser.add_argument('-v', '--verbose', action='store_true',
help='display more information')
parser.add_argument('-f', '--field', metavar='FIELD',
help='display information only for this particular field')
parser.set_defaults(run=run)

68
openerpcommand/module.py Normal file
View File

@ -0,0 +1,68 @@
"""
Show module information for a given database or from the file-system.
"""
import os
import sys
import textwrap
from . import common
# TODO provide a --rpc flag to use XML-RPC (with a specific username) instead
# of server-side library.
def run(args):
assert args.database
import openerp
config = openerp.tools.config
config['log_handler'] = [':CRITICAL']
if args.addons:
args.addons = args.addons.split(':')
else:
args.addons = []
config['addons_path'] = ','.join(args.addons)
openerp.netsvc.init_logger()
if args.filesystem:
module_names = common.get_addons_from_paths(args.addons, [])
print "Modules (addons path %s):" % (', '.join(args.addons),)
for x in sorted(module_names):
print x
else:
registry = openerp.modules.registry.RegistryManager.get(
args.database, update_module=False)
xs = []
ir_module_module = registry.get('ir.module.module')
cr = registry.db.cursor() # TODO context manager
try:
ids = ir_module_module.search(cr, openerp.SUPERUSER_ID, [], {})
xs = ir_module_module.read(cr, openerp.SUPERUSER_ID, ids, [], {})
finally:
cr.close()
if xs:
print "Modules (database `%s`):" % (args.database,)
for x in xs:
if args.short:
print '%3d %s' % (x['id'], x['name'])
else:
print '%3d %s %s' % (x['id'], x['name'], {'installed': '(installed)'}.get(x['state'], ''))
else:
print "No module found (database `%s`)." % (args.database,)
def add_parser(subparsers):
parser = subparsers.add_parser('module',
description='Display modules known from a given database or on file-system.')
parser.add_argument('-d', '--database', metavar='DATABASE',
**common.required_or_default('DATABASE', 'the database to modify'))
common.add_addons_argument(parser)
parser.add_argument('-m', '--module', metavar='MODULE', required=False,
help='the module for which information should be shown')
parser.add_argument('-v', '--verbose', action='store_true',
help='display more information')
parser.add_argument('--short', action='store_true',
help='display less information')
parser.add_argument('-f', '--filesystem', action='store_true',
help='display module in the addons path, not in db')
parser.set_defaults(run=run)

60
openerpcommand/read.py Normal file
View File

@ -0,0 +1,60 @@
"""
Read a record.
"""
import os
import sys
import textwrap
# TODO provide a --rpc flag to use XML-RPC (with a specific username) instead
# of server-side library.
def run(args):
assert args.database
assert args.model
import openerp
config = openerp.tools.config
config['log_handler'] = [':CRITICAL']
openerp.netsvc.init_logger()
registry = openerp.modules.registry.RegistryManager.get(
args.database, update_module=False)
model = registry.get(args.model)
cr = registry.db.cursor() # TODO context manager
field_names = [args.field] if args.field else []
if args.short:
# ignore --field
field_names = ['name']
try:
xs = model.read(cr, 1, args.id, field_names, {})
finally:
cr.close()
if xs:
print "Records (model `%s`, database `%s`):" % (args.model, args.database)
x = xs[0]
if args.short:
print str(x['id']) + '.', x['name']
else:
longest_k = 1
for k, v in x.items():
longest_k = len(k) if longest_k < len(k) else longest_k
for k, v in sorted(x.items()):
print (k + ':').ljust(longest_k + 2), v
else:
print "Record not found."
def add_parser(subparsers):
parser = subparsers.add_parser('read',
description='Display a record.')
parser.add_argument('-d', '--database', metavar='DATABASE', required=True,
help='the database to connect to')
parser.add_argument('-m', '--model', metavar='MODEL', required=True,
help='the model for which a record should be read')
parser.add_argument('-i', '--id', metavar='RECORDID', required=True,
help='the record id')
parser.add_argument('-v', '--verbose', action='store_true',
help='display more information')
parser.add_argument('--short', action='store_true',
help='display less information')
parser.add_argument('-f', '--field', metavar='FIELD',
help='display information only for this particular field')
parser.set_defaults(run=run)

202
openerpcommand/run_tests.py Normal file
View File

@ -0,0 +1,202 @@
"""
Execute the unittest2 tests available in OpenERP addons.
"""
import os
import sys
import types
import common
def get_test_modules(module, submodule, explode):
"""
Return a list of submodules containing tests.
`submodule` can be:
- None
- the name of a submodule
- '__fast_suite__'
- '__sanity_checks__'
"""
# Turn command-line module, submodule into importable names.
if module is None:
pass
elif module == 'openerp':
module = 'openerp.tests'
else:
module = 'openerp.addons.' + module + '.tests'
# Try to import the module
try:
__import__(module)
except Exception, e:
if explode:
print 'Can not `import %s`.' % module
import logging
logging.exception('')
sys.exit(1)
else:
if str(e) == 'No module named tests':
# It seems the module has no `tests` sub-module, no problem.
pass
else:
print 'Can not `import %s`.' % module
return []
# Discover available test sub-modules.
m = sys.modules[module]
submodule_names = sorted([x for x in dir(m) \
if x.startswith('test_') and \
isinstance(getattr(m, x), types.ModuleType)])
submodules = [getattr(m, x) for x in submodule_names]
def show_submodules_and_exit():
if submodule_names:
print 'Available submodules are:'
for x in submodule_names:
print ' ', x
sys.exit(1)
if submodule is None:
# Use auto-discovered sub-modules.
ms = submodules
elif submodule == '__fast_suite__':
# Obtain the explicit test sub-modules list.
ms = getattr(sys.modules[module], 'fast_suite', None)
# `suite` was used before the 6.1 release instead of `fast_suite`.
ms = ms if ms else getattr(sys.modules[module], 'suite', None)
if ms is None:
if explode:
print 'The module `%s` has no defined test suite.' % (module,)
show_submodules_and_exit()
else:
ms = []
elif submodule == '__sanity_checks__':
ms = getattr(sys.modules[module], 'checks', None)
if ms is None:
if explode:
print 'The module `%s` has no defined sanity checks.' % (module,)
show_submodules_and_exit()
else:
ms = []
else:
# Pick the command-line-specified test sub-module.
m = getattr(sys.modules[module], submodule, None)
ms = [m]
if m is None:
if explode:
print 'The module `%s` has no submodule named `%s`.' % \
(module, submodule)
show_submodules_and_exit()
else:
ms = []
return ms
def run(args):
import unittest2
import openerp
config = openerp.tools.config
config['db_name'] = args.database
if args.port:
config['xmlrpc_port'] = int(args.port)
config['admin_passwd'] = 'admin'
config['db_password'] = 'a2aevl8w' # TODO from .openerpserverrc
config['addons_path'] = args.addons.replace(':',',')
if args.addons:
args.addons = args.addons.split(':')
else:
args.addons = []
if args.sanity_checks and args.fast_suite:
print 'Only at most one of `--sanity-checks` and `--fast-suite` ' \
'can be specified.'
sys.exit(1)
import logging
openerp.netsvc.init_alternative_logger()
logging.getLogger('openerp').setLevel(logging.CRITICAL)
# Install the import hook, to import openerp.addons.<module>.
openerp.modules.module.initialize_sys_path()
openerp.modules.loading.open_openerp_namespace()
# Extract module, submodule from the command-line args.
if args.module is None:
module, submodule = None, None
else:
splitted = args.module.split('.')
if len(splitted) == 1:
module, submodule = splitted[0], None
elif len(splitted) == 2:
module, submodule = splitted
else:
print 'The `module` argument must have the form ' \
'`module[.submodule]`.'
sys.exit(1)
# Import the necessary modules and get the corresponding suite.
if module is None:
# TODO
modules = common.get_addons_from_paths(args.addons, []) # TODO openerp.addons.base is not included ?
test_modules = []
for module in ['openerp'] + modules:
if args.fast_suite:
submodule = '__fast_suite__'
if args.sanity_checks:
submodule = '__sanity_checks__'
test_modules.extend(get_test_modules(module,
submodule, explode=False))
else:
if submodule and args.fast_suite:
print "Submodule name `%s` given, ignoring `--fast-suite`." % (submodule,)
if submodule and args.sanity_checks:
print "Submodule name `%s` given, ignoring `--sanity-checks`." % (submodule,)
if not submodule and args.fast_suite:
submodule = '__fast_suite__'
if not submodule and args.sanity_checks:
submodule = '__sanity_checks__'
test_modules = get_test_modules(module,
submodule, explode=True)
# Run the test suite.
if not args.dry_run:
suite = unittest2.TestSuite()
for test_module in test_modules:
suite.addTests(unittest2.TestLoader().loadTestsFromModule(test_module))
r = unittest2.TextTestRunner(verbosity=2).run(suite)
if r.errors or r.failures:
sys.exit(1)
else:
print 'Test modules:'
for test_module in test_modules:
print ' ', test_module.__name__
def add_parser(subparsers):
parser = subparsers.add_parser('run-tests',
description='Run the OpenERP server and/or addons tests.')
parser.add_argument('-d', '--database', metavar='DATABASE', required=True,
help='the database to test. Depending on the test suites, the '
'database must already exist or not.')
parser.add_argument('-p', '--port', metavar='PORT',
help='the port used for WML-RPC tests')
common.add_addons_argument(parser)
parser.add_argument('-m', '--module', metavar='MODULE',
default=None,
help='the module to test in `module[.submodule]` notation. '
'Use `openerp` for the core OpenERP tests. '
'Leave empty to run every declared tests. '
'Give a module but no submodule to run all the module\'s declared '
'tests. If both the module and the submodule are given, '
'the sub-module can be run even if it is not declared in the module.')
parser.add_argument('--fast-suite', action='store_true',
help='run only the tests explicitely declared in the fast suite (this '
'makes sense only with the bare `module` notation or no module at '
'all).')
parser.add_argument('--sanity-checks', action='store_true',
help='run only the sanity check tests')
parser.add_argument('--dry-run', action='store_true',
help='do not run the tests')
parser.set_defaults(run=run)

View File

@ -0,0 +1,77 @@
"""
Generate an OpenERP module skeleton.
"""
import os
import sys
def run(args):
assert args.module
module = args.module
if os.path.exists(module):
print "The path `%s` already exists."
sys.exit(1)
os.mkdir(module)
os.mkdir(os.path.join(module, 'models'))
with open(os.path.join(module, '__openerp__.py'), 'w') as h:
h.write(MANIFEST)
with open(os.path.join(module, '__init__.py'), 'w') as h:
h.write(INIT_PY)
with open(os.path.join(module, 'models', '__init__.py'), 'w') as h:
h.write(MODELS_PY % (module,))
def add_parser(subparsers):
parser = subparsers.add_parser('scaffold',
description='Generate an OpenERP module skeleton.')
parser.add_argument('module', metavar='MODULE',
help='the name of the generated module')
parser.set_defaults(run=run)
MANIFEST = """\
# -*- coding: utf-8 -*-
{
'name': '<Module name>',
'version': '0.0',
'category': '<Category>',
'description': '''
<Long description>
''',
'author': '<author>',
'maintainer': '<maintainer>',
'website': 'http://<website>',
# Add any module that are necessary for this module to correctly work in
# the `depends` list.
'depends': ['base'],
'data': [
],
'test': [
],
# Set to False if you want to prevent the module to be known by OpenERP
# (and thus appearing in the list of modules).
'installable': True,
# Set to True if you want the module to be automatically whenever all its
# dependencies are installed.
'auto_install': False,
}
"""
INIT_PY = """\
# -*- coding: utf-8 -*-
import models
"""
MODELS_PY = """\
# -*- coding: utf-8 -*-
import openerp
# Define a new model.
class my_model(openerp.osv.osv.Model):
_name = '%s.my_model'
_columns = {
}
"""

View File

@ -0,0 +1,67 @@
"""
Install OpenERP on a new (by default) database.
"""
import os
import sys
import common
# TODO turn template1 in a parameter
# This should be exposed from openerp (currently in
# openerp/service/web_services.py).
def create_database(database_name):
import openerp
db = openerp.sql_db.db_connect('template1')
cr = db.cursor() # TODO `with db as cr:`
try:
cr.autocommit(True)
cr.execute("""CREATE DATABASE "%s"
ENCODING 'unicode' TEMPLATE "template1" """ \
% (database_name,))
finally:
cr.close()
def run(args):
assert args.database
assert args.module
import openerp
config = openerp.tools.config
config['log_handler'] = [':CRITICAL']
if args.addons:
args.addons = args.addons.split(':')
else:
args.addons = []
config['addons_path'] = ','.join(args.addons)
openerp.netsvc.init_logger()
# Install the import hook, to import openerp.addons.<module>.
openerp.modules.module.initialize_sys_path()
openerp.modules.loading.open_openerp_namespace()
registry = openerp.modules.registry.RegistryManager.get(
args.database, update_module=False)
ir_module_module = registry.get('ir.module.module')
cr = registry.db.cursor() # TODO context manager
try:
ids = ir_module_module.search(cr, openerp.SUPERUSER_ID, [('name', 'in', args.module), ('state', '=', 'installed')], {})
if len(ids) == len(args.module):
ir_module_module.button_immediate_uninstall(cr, openerp.SUPERUSER_ID, ids, {})
else:
print "At least one module not found (database `%s`)." % (args.database,)
finally:
cr.close()
def add_parser(subparsers):
parser = subparsers.add_parser('uninstall',
description='Uninstall some modules from an OpenERP database.')
parser.add_argument('-d', '--database', metavar='DATABASE',
**common.required_or_default('DATABASE', 'the database to modify'))
common.add_addons_argument(parser)
parser.add_argument('--module', metavar='MODULE', action='append',
help='specify a module to uninstall'
' (this option can be repeated)')
parser.set_defaults(run=run)

19
openerpcommand/update.py Normal file
View File

@ -0,0 +1,19 @@
"""
Update an existing OpenERP database.
"""
def run(args):
assert args.database
import openerp
config = openerp.tools.config
config['update']['all'] = 1
openerp.modules.registry.RegistryManager.get(
args.database, update_module=True)
def add_parser(subparsers):
parser = subparsers.add_parser('update',
description='Update an existing OpenERP database.')
parser.add_argument('-d', '--database', metavar='DATABASE', required=True,
help='the database to update')
parser.set_defaults(run=run)