[MERGE] xmo python unit tests
bzr revid: al@openerp.com-20110321135427-fb6w9c3lexsdautw
This commit is contained in:
commit
d40743d0e3
|
@ -118,29 +118,39 @@ class Menu(openerpweb.Controller):
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
def load(self, req):
|
def load(self, req):
|
||||||
m = req.session.model('ir.ui.menu')
|
return {'data': self.do_load(req)}
|
||||||
|
|
||||||
|
def do_load(self, req):
|
||||||
|
""" Loads all menu items (all applications and their sub-menus).
|
||||||
|
|
||||||
|
:param req: A request object, with an OpenERP session attribute
|
||||||
|
:type req: < session -> OpenERPSession >
|
||||||
|
:return: the menu root
|
||||||
|
:rtype: dict('children': menu_nodes)
|
||||||
|
"""
|
||||||
|
Menus = req.session.model('ir.ui.menu')
|
||||||
# menus are loaded fully unlike a regular tree view, cause there are
|
# menus are loaded fully unlike a regular tree view, cause there are
|
||||||
# less than 512 items
|
# less than 512 items
|
||||||
menu_ids = m.search([])
|
menu_ids = Menus.search([])
|
||||||
menu_items = m.read(menu_ids, ['name', 'sequence', 'parent_id'])
|
menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'])
|
||||||
menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
|
menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
|
||||||
menu_items.append(menu_root)
|
menu_items.append(menu_root)
|
||||||
|
|
||||||
# make a tree using parent_id
|
# make a tree using parent_id
|
||||||
for i in menu_items:
|
menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
|
||||||
i['children'] = []
|
for menu_item in menu_items:
|
||||||
d = dict([(i["id"], i) for i in menu_items])
|
if not menu_item['parent_id']: continue
|
||||||
for i in menu_items:
|
parent = menu_item['parent_id'][0]
|
||||||
if not i['parent_id']:
|
if parent in menu_items_map:
|
||||||
pid = False
|
menu_items_map[parent].setdefault(
|
||||||
else:
|
'children', []).append(menu_item)
|
||||||
pid = i['parent_id'][0]
|
|
||||||
if pid in d:
|
|
||||||
d[pid]['children'].append(i)
|
|
||||||
# sort by sequence a tree using parent_id
|
|
||||||
for i in menu_items:
|
|
||||||
i['children'].sort(key=lambda x:x["sequence"])
|
|
||||||
|
|
||||||
return {'data': menu_root}
|
# sort by sequence a tree using parent_id
|
||||||
|
for menu_item in menu_items:
|
||||||
|
menu_item.setdefault('children', []).sort(
|
||||||
|
key=lambda x:x["sequence"])
|
||||||
|
|
||||||
|
return menu_root
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
def action(self, req, menu_id):
|
def action(self, req, menu_id):
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
|
@ -0,0 +1,99 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import mock
|
||||||
|
import unittest2
|
||||||
|
import base.controllers.main
|
||||||
|
import openerpweb.openerpweb
|
||||||
|
|
||||||
|
class Placeholder(object):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
for k, v in kwargs.iteritems():
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
class LoadTest(unittest2.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.menu = base.controllers.main.Menu()
|
||||||
|
self.menus_mock = mock.Mock()
|
||||||
|
self.request = Placeholder(
|
||||||
|
session=openerpweb.openerpweb.OpenERPSession(
|
||||||
|
model_factory=lambda _session, _name: self.menus_mock))
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
del self.request
|
||||||
|
del self.menus_mock
|
||||||
|
del self.menu
|
||||||
|
|
||||||
|
def test_empty(self):
|
||||||
|
self.menus_mock.search = mock.Mock(return_value=[])
|
||||||
|
self.menus_mock.read = mock.Mock(return_value=[])
|
||||||
|
|
||||||
|
root = self.menu.do_load(self.request)
|
||||||
|
|
||||||
|
self.menus_mock.search.assert_called_with([])
|
||||||
|
self.menus_mock.read.assert_called_with(
|
||||||
|
[], ['name', 'sequence', 'parent_id'])
|
||||||
|
|
||||||
|
self.assertListEqual(
|
||||||
|
root['children'],
|
||||||
|
[])
|
||||||
|
|
||||||
|
def test_applications_sort(self):
|
||||||
|
self.menus_mock.search = mock.Mock(return_value=[1, 2, 3])
|
||||||
|
self.menus_mock.read = mock.Mock(return_value=[
|
||||||
|
{'id': 2, 'sequence': 3, 'parent_id': [False, '']},
|
||||||
|
{'id': 3, 'sequence': 2, 'parent_id': [False, '']},
|
||||||
|
{'id': 1, 'sequence': 1, 'parent_id': [False, '']},
|
||||||
|
])
|
||||||
|
|
||||||
|
root = self.menu.do_load(self.request)
|
||||||
|
|
||||||
|
self.menus_mock.read.assert_called_with(
|
||||||
|
[1, 2, 3], ['name', 'sequence', 'parent_id'])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
root['children'],
|
||||||
|
[{
|
||||||
|
'id': 1, 'sequence': 1,
|
||||||
|
'parent_id': [False, ''], 'children': []
|
||||||
|
}, {
|
||||||
|
'id': 3, 'sequence': 2,
|
||||||
|
'parent_id': [False, ''], 'children': []
|
||||||
|
}, {
|
||||||
|
'id': 2, 'sequence': 3,
|
||||||
|
'parent_id': [False, ''], 'children': []
|
||||||
|
}])
|
||||||
|
|
||||||
|
def test_deep(self):
|
||||||
|
self.menus_mock.search = mock.Mock(return_value=[1, 2, 3, 4])
|
||||||
|
self.menus_mock.read = mock.Mock(return_value=[
|
||||||
|
{'id': 1, 'sequence': 1, 'parent_id': [False, '']},
|
||||||
|
{'id': 2, 'sequence': 2, 'parent_id': [1, '']},
|
||||||
|
{'id': 3, 'sequence': 1, 'parent_id': [2, '']},
|
||||||
|
{'id': 4, 'sequence': 2, 'parent_id': [2, '']},
|
||||||
|
])
|
||||||
|
|
||||||
|
root = self.menu.do_load(self.request)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
root['children'],
|
||||||
|
[{
|
||||||
|
'id': 1,
|
||||||
|
'sequence': 1,
|
||||||
|
'parent_id': [False, ''],
|
||||||
|
'children': [{
|
||||||
|
'id': 2,
|
||||||
|
'sequence': 2,
|
||||||
|
'parent_id': [1, ''],
|
||||||
|
'children': [{
|
||||||
|
'id': 3,
|
||||||
|
'sequence': 1,
|
||||||
|
'parent_id': [2, ''],
|
||||||
|
'children': []
|
||||||
|
}, {
|
||||||
|
'id': 4,
|
||||||
|
'sequence': 2,
|
||||||
|
'parent_id': [2, ''],
|
||||||
|
'children': []
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
)
|
|
@ -0,0 +1,9 @@
|
||||||
|
<addon name>
|
||||||
|
+-- __openerp__.py
|
||||||
|
+-- controllers/
|
||||||
|
+-- static/
|
||||||
|
+-- openerp/
|
||||||
|
+-- css/
|
||||||
|
+-- img/
|
||||||
|
+-- js/
|
||||||
|
+-- tests/
|
|
@ -1,7 +1,91 @@
|
||||||
Developing OpenERP Web Addons
|
Developing OpenERP Web Addons
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
* Structure of an addon
|
Structure
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. literalinclude:: addon-structure.txt
|
||||||
|
|
||||||
|
``__openerp__.py``
|
||||||
|
The addon's descriptor, contains the following information:
|
||||||
|
|
||||||
|
``name: str``
|
||||||
|
The addon name, in plain, readable english
|
||||||
|
``version: str``
|
||||||
|
The addon version, following `Semantic Versioning`_ rules
|
||||||
|
``depends: [str]``
|
||||||
|
A list of addons this addon needs to work correctly. ``base`` is
|
||||||
|
an implied dependency if the list is empty.
|
||||||
|
``css: [str]``
|
||||||
|
An ordered list of CSS files this addon provides and needs. The
|
||||||
|
file paths are relative to the addon's root. Because the Web
|
||||||
|
Client *may* perform concatenations and other various
|
||||||
|
optimizations on CSS files, the order is important.
|
||||||
|
``js: [str]``
|
||||||
|
An ordered list of Javascript files this addon provides and needs
|
||||||
|
(including dependencies files). As with CSS files, the order is
|
||||||
|
important as the Web Client *may* perform contatenations and
|
||||||
|
minimizations of files.
|
||||||
|
``active: bool``
|
||||||
|
Whether this addon should be enabled by default any time it is
|
||||||
|
found, or whether it will be enabled through other means (on a
|
||||||
|
by-need or by-installation basis for instance).
|
||||||
|
|
||||||
|
``controllers/``
|
||||||
|
All of the Python controllers and JSON-RPC endpoints.
|
||||||
|
|
||||||
|
``static/``
|
||||||
|
The static files directory, may be served via a separate web server.
|
||||||
|
|
||||||
|
The third-party dependencies should be bundled in it (each in their
|
||||||
|
own directory).
|
||||||
|
|
||||||
|
``static/openerp/``
|
||||||
|
Sub-tree for all the addon's own static files.
|
||||||
|
|
||||||
|
``static/openerp/{css,js,img}``
|
||||||
|
Location for (respectively) the addon's static CSS files, its JS
|
||||||
|
files and its various image resources.
|
||||||
|
|
||||||
|
``tests/``
|
||||||
|
The directories in which all tests for the addon are located.
|
||||||
|
|
||||||
|
.. _addons-testing:
|
||||||
|
|
||||||
|
Testing
|
||||||
|
-------
|
||||||
|
|
||||||
|
Python
|
||||||
|
++++++
|
||||||
|
|
||||||
|
OpenERP Web uses unittest2_ for its testing needs. We selected
|
||||||
|
unittest2 rather than unittest_ for the following reasons:
|
||||||
|
|
||||||
|
* autodiscovery_ (similar to nose, via the ``unit2``
|
||||||
|
CLI utility) and `pluggable test discovery`_.
|
||||||
|
|
||||||
|
* `new and improved assertions`_ (with improvements in type-specific
|
||||||
|
inequality reportings) including `pluggable custom types equality
|
||||||
|
assertions`_
|
||||||
|
|
||||||
|
* neveral new APIs, most notably `assertRaises context manager`_,
|
||||||
|
`cleanup function registration`_, `test skipping`_ and `class- and
|
||||||
|
module-level setup and teardown`_
|
||||||
|
|
||||||
|
* finally, unittest2 is a backport of Python 3's unittest. We might as
|
||||||
|
well get used to it.
|
||||||
|
|
||||||
|
To run tests on addons (from the root directory of OpenERP Web) is as
|
||||||
|
simple as typing ``PYTHONPATH=. unit2 discover -s addons`` [#]_. To
|
||||||
|
test an addon which does not live in the ``addons`` directory, simply
|
||||||
|
replace ``addons`` by the directory in which your own addon lives.
|
||||||
|
|
||||||
|
.. note:: unittest2 is entirely compatible with nose_ (or the
|
||||||
|
other way around). If you want to use nose as your test
|
||||||
|
runner (due to its addons for instance) you can simply install it
|
||||||
|
and run ``nosetests addons`` instead of the ``unit2`` command,
|
||||||
|
the result should be exactly the same.
|
||||||
|
|
||||||
* Addons lifecycle (loading, execution, events, ...)
|
* Addons lifecycle (loading, execution, events, ...)
|
||||||
|
|
||||||
* Python-side
|
* Python-side
|
||||||
|
@ -16,3 +100,54 @@ Developing OpenERP Web Addons
|
||||||
* Javascript public APIs
|
* Javascript public APIs
|
||||||
* QWeb templates description?
|
* QWeb templates description?
|
||||||
* OpenERP Web modules (from OpenERP modules)
|
* OpenERP Web modules (from OpenERP modules)
|
||||||
|
|
||||||
|
.. [#] the ``-s`` parameter tells ``unit2`` to start trying to
|
||||||
|
find tests in the provided directory (here we're testing
|
||||||
|
addons). However a side-effect of that is to set the
|
||||||
|
``PYTHONPATH`` there as well, so it will fail to find (and
|
||||||
|
import) ``openerpweb``.
|
||||||
|
|
||||||
|
The ``-t`` parameter lets us set the ``PYTHONPATH``
|
||||||
|
independently, but it doesn't accept multiple values and here
|
||||||
|
we really want to have both ``.`` and ``addons`` on the
|
||||||
|
``PYTHONPATH``.
|
||||||
|
|
||||||
|
The solution is to set the ``PYTHONPATH`` to ``.`` on start,
|
||||||
|
and the ``start-directory`` to ``addons``. This results in a
|
||||||
|
correct ``PYTHONPATH`` within ``unit2``.
|
||||||
|
|
||||||
|
.. _unittest:
|
||||||
|
http://docs.python.org/library/unittest.html
|
||||||
|
|
||||||
|
.. _unittest2:
|
||||||
|
http://www.voidspace.org.uk/python/articles/unittest2.shtml
|
||||||
|
|
||||||
|
.. _autodiscovery:
|
||||||
|
http://www.voidspace.org.uk/python/articles/unittest2.shtml#test-discovery
|
||||||
|
|
||||||
|
.. _pluggable test discovery:
|
||||||
|
http://www.voidspace.org.uk/python/articles/unittest2.shtml#load-tests
|
||||||
|
|
||||||
|
.. _new and improved assertions:
|
||||||
|
http://www.voidspace.org.uk/python/articles/unittest2.shtml#new-assert-methods
|
||||||
|
|
||||||
|
.. _pluggable custom types equality assertions:
|
||||||
|
http://www.voidspace.org.uk/python/articles/unittest2.shtml#add-new-type-specific-functions
|
||||||
|
|
||||||
|
.. _assertRaises context manager:
|
||||||
|
http://www.voidspace.org.uk/python/articles/unittest2.shtml#assertraises
|
||||||
|
|
||||||
|
.. _cleanup function registration:
|
||||||
|
http://www.voidspace.org.uk/python/articles/unittest2.shtml#cleanup-functions-with-addcleanup
|
||||||
|
|
||||||
|
.. _test skipping:
|
||||||
|
http://www.voidspace.org.uk/python/articles/unittest2.shtml#test-skipping
|
||||||
|
|
||||||
|
.. _class- and module-level setup and teardown:
|
||||||
|
http://www.voidspace.org.uk/python/articles/unittest2.shtml#class-and-module-level-fixtures
|
||||||
|
|
||||||
|
.. _Semantic Versioning:
|
||||||
|
http://semver.org/
|
||||||
|
|
||||||
|
.. _nose:
|
||||||
|
http://somethingaboutorange.com/mrl/projects/nose/1.0.0/
|
||||||
|
|
|
@ -6,4 +6,25 @@ Contributing to OpenERP Web
|
||||||
* QWeb code documentation/description
|
* QWeb code documentation/description
|
||||||
* Documentation of the OpenERP APIs and choices taken based on that?
|
* Documentation of the OpenERP APIs and choices taken based on that?
|
||||||
* Style guide and coding conventions (PEP8? More)
|
* Style guide and coding conventions (PEP8? More)
|
||||||
* Test frameworks for Python and JS?
|
* Test frameworks in JS?
|
||||||
|
|
||||||
|
Testing
|
||||||
|
-------
|
||||||
|
|
||||||
|
Python
|
||||||
|
++++++
|
||||||
|
|
||||||
|
Testing for the OpenERP Web core is similar to :ref:`testing addons
|
||||||
|
<addons-testing>`: the tests live in ``openerpweb.tests``, unittest2_
|
||||||
|
is the testing framework and tests can be run via either unittest2
|
||||||
|
(``unit2 discover``) or via nose_ (``nosetests``).
|
||||||
|
|
||||||
|
Tests for the OpenERP Web core can also be run using ``setup.py
|
||||||
|
test``.
|
||||||
|
|
||||||
|
|
||||||
|
.. _unittest2:
|
||||||
|
http://www.voidspace.org.uk/python/articles/unittest2.shtml
|
||||||
|
|
||||||
|
.. _nose:
|
||||||
|
http://somethingaboutorange.com/mrl/projects/nose/1.0.0/
|
||||||
|
|
|
@ -39,13 +39,15 @@ class OpenERPModel(object):
|
||||||
|
|
||||||
|
|
||||||
class OpenERPSession(object):
|
class OpenERPSession(object):
|
||||||
def __init__(self, server='127.0.0.1', port=8069):
|
def __init__(self, server='127.0.0.1', port=8069,
|
||||||
|
model_factory=OpenERPModel):
|
||||||
self._server = server
|
self._server = server
|
||||||
self._port = port
|
self._port = port
|
||||||
self._db = False
|
self._db = False
|
||||||
self._uid = False
|
self._uid = False
|
||||||
self._login = False
|
self._login = False
|
||||||
self._password = False
|
self._password = False
|
||||||
|
self.model_factory = model_factory
|
||||||
|
|
||||||
def proxy(self, service):
|
def proxy(self, service):
|
||||||
s = xmlrpctimeout.TimeoutServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service), timeout=5)
|
s = xmlrpctimeout.TimeoutServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service), timeout=5)
|
||||||
|
@ -69,7 +71,7 @@ class OpenERPSession(object):
|
||||||
return r
|
return r
|
||||||
|
|
||||||
def model(self, model):
|
def model(self, model):
|
||||||
return OpenERPModel(self, model)
|
return self.model_factory(self, model)
|
||||||
|
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
# OpenERP Web RequestHandler
|
# OpenERP Web RequestHandler
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import mock
|
||||||
|
import unittest2
|
||||||
|
import openerpweb.openerpweb
|
||||||
|
|
||||||
|
class OpenERPModelTest(unittest2.TestCase):
|
||||||
|
def test_rpc_call(self):
|
||||||
|
session = mock.Mock(['execute'])
|
||||||
|
Model = openerpweb.openerpweb.OpenERPModel(
|
||||||
|
session, 'a.b')
|
||||||
|
|
||||||
|
Model.search([('field', 'op', 'value')], {'key': 'value'})
|
||||||
|
session.execute.assert_called_once_with(
|
||||||
|
'a.b', 'search', [('field', 'op', 'value')], {'key': 'value'})
|
||||||
|
|
||||||
|
session.execute.reset_mock()
|
||||||
|
|
||||||
|
Model.read([42])
|
||||||
|
session.execute.assert_called_once_with(
|
||||||
|
'a.b', 'read', [42])
|
5
setup.py
5
setup.py
|
@ -59,6 +59,11 @@ setup(
|
||||||
"simplejson >= 2.0.9",
|
"simplejson >= 2.0.9",
|
||||||
"python-dateutil >= 1.4.1",
|
"python-dateutil >= 1.4.1",
|
||||||
],
|
],
|
||||||
|
tests_require=[
|
||||||
|
'unittest2',
|
||||||
|
'mock',
|
||||||
|
],
|
||||||
|
test_suite = 'unittest2.collector',
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
packages=[
|
packages=[
|
||||||
'addons',
|
'addons',
|
||||||
|
|
Loading…
Reference in New Issue