[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
|
||||
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
|
||||
# less than 512 items
|
||||
menu_ids = m.search([])
|
||||
menu_items = m.read(menu_ids, ['name', 'sequence', 'parent_id'])
|
||||
menu_ids = Menus.search([])
|
||||
menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'])
|
||||
menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
|
||||
menu_items.append(menu_root)
|
||||
# make a tree using parent_id
|
||||
for i in menu_items:
|
||||
i['children'] = []
|
||||
d = dict([(i["id"], i) for i in menu_items])
|
||||
for i in menu_items:
|
||||
if not i['parent_id']:
|
||||
pid = False
|
||||
else:
|
||||
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}
|
||||
# make a tree using parent_id
|
||||
menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
|
||||
for menu_item in menu_items:
|
||||
if not menu_item['parent_id']: continue
|
||||
parent = menu_item['parent_id'][0]
|
||||
if parent in menu_items_map:
|
||||
menu_items_map[parent].setdefault(
|
||||
'children', []).append(menu_item)
|
||||
|
||||
# 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
|
||||
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
|
||||
=============================
|
||||
|
||||
* 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, ...)
|
||||
|
||||
* Python-side
|
||||
|
@ -16,3 +100,54 @@ Developing OpenERP Web Addons
|
|||
* Javascript public APIs
|
||||
* QWeb templates description?
|
||||
* 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
|
||||
* Documentation of the OpenERP APIs and choices taken based on that?
|
||||
* 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):
|
||||
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._port = port
|
||||
self._db = False
|
||||
self._uid = False
|
||||
self._login = False
|
||||
self._password = False
|
||||
self.model_factory = model_factory
|
||||
|
||||
def proxy(self, service):
|
||||
s = xmlrpctimeout.TimeoutServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service), timeout=5)
|
||||
|
@ -69,7 +71,7 @@ class OpenERPSession(object):
|
|||
return r
|
||||
|
||||
def model(self, model):
|
||||
return OpenERPModel(self, model)
|
||||
return self.model_factory(self, model)
|
||||
|
||||
#----------------------------------------------------------
|
||||
# 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])
|
Loading…
Reference in New Issue