1 # -*- coding: utf-8 -*-
3 The module :mod:`openerp.tests.common` provides unittest2 test cases and a few
4 helpers and classes to write tests.
19 from datetime import datetime, timedelta
24 from openerp import api
25 from openerp.modules.registry import RegistryManager
27 _logger = logging.getLogger(__name__)
29 # The openerp library is supposed already configured.
30 ADDONS_PATH = openerp.tools.config['addons_path']
32 PORT = openerp.tools.config['xmlrpc_port']
33 DB = openerp.tools.config['db_name']
34 # If the database name is not provided on the command-line,
35 # use the one on the thread (which means if it is provided on
36 # the command-line, this will break when installing another
37 # database from XML-RPC).
38 if not DB and hasattr(threading.current_thread(), 'dbname'):
39 DB = threading.current_thread().dbname
40 # Useless constant, tests are aware of the content of demo data
41 ADMIN_USER_ID = openerp.SUPERUSER_ID
44 """ Sets the at-install state of a test, the flag is a boolean specifying
45 whether the test should (``True``) or should not (``False``) run during
48 By default, tests are run right after installing the module, before
49 starting the installation of the next module.
56 def post_install(flag):
57 """ Sets the post-install state of a test. The flag is a boolean
58 specifying whether the test should or should not run after a set of
61 By default, tests are *not* run after installation of all modules in the
62 current installation set.
65 obj.post_install = flag
69 class BaseCase(unittest2.TestCase):
71 Subclass of TestCase for common OpenERP-specific code.
73 This class is abstract and expects self.registry, self.cr and self.uid to be
74 initialized by subclasses.
78 return self.registry.cursor()
81 """ Returns database ID for the provided :term:`external identifier`,
82 shortcut for ``get_object_reference``
84 :param xid: fully-qualified :term:`external identifier`, in the form
85 :samp:`{module}.{identifier}`
86 :raise: ValueError if not found
87 :returns: registered id
89 assert "." in xid, "this method requires a fully qualified parameter, in the following form: 'module.identifier'"
90 module, xid = xid.split('.')
91 _, id = self.registry('ir.model.data').get_object_reference(self.cr, self.uid, module, xid)
94 def browse_ref(self, xid):
95 """ Returns a record object for the provided
96 :term:`external identifier`
98 :param xid: fully-qualified :term:`external identifier`, in the form
99 :samp:`{module}.{identifier}`
100 :raise: ValueError if not found
101 :returns: :class:`~openerp.models.BaseModel`
103 assert "." in xid, "this method requires a fully qualified parameter, in the following form: 'module.identifier'"
104 module, xid = xid.split('.')
105 return self.registry('ir.model.data').get_object(self.cr, self.uid, module, xid)
108 class TransactionCase(BaseCase):
109 """ TestCase in which each test method is run in its own transaction,
110 and with its own cursor. The transaction is rolled back and the cursor
111 is closed after each test.
115 self.registry = RegistryManager.get(DB)
116 #: current transaction's cursor
117 self.cr = self.cursor()
118 self.uid = openerp.SUPERUSER_ID
119 #: :class:`~openerp.api.Environment` for the current test case
120 self.env = api.Environment(self.cr, self.uid, {})
127 class SingleTransactionCase(BaseCase):
128 """ TestCase in which all test methods are run in the same transaction,
129 the transaction is started with the first test method and rolled back at
135 cls.registry = RegistryManager.get(DB)
136 cls.cr = cls.registry.cursor()
137 cls.uid = openerp.SUPERUSER_ID
138 cls.env = api.Environment(cls.cr, cls.uid, {})
141 def tearDownClass(cls):
145 class RedirectHandler(urllib2.HTTPRedirectHandler):
147 HTTPRedirectHandler is predicated upon HTTPErrorProcessor being used and
148 works by intercepting 3xy "errors".
150 Inherit from it to handle 3xy non-error responses instead, as we're not
151 using the error processor
154 def http_response(self, request, response):
155 code, msg, hdrs = response.code, response.msg, response.info()
157 if 300 <= code < 400:
158 return self.parent.error(
159 'http', request, response, code, msg, hdrs)
163 https_response = http_response
165 class HttpCase(TransactionCase):
166 """ Transactional HTTP TestCase with url_open and phantomjs helpers.
169 def __init__(self, methodName='runTest'):
170 super(HttpCase, self).__init__(methodName)
171 # v8 api with correct xmlrpc exception handling.
172 self.xmlrpc_url = url_8 = 'http://%s:%d/xmlrpc/2/' % (HOST, PORT)
173 self.xmlrpc_common = xmlrpclib.ServerProxy(url_8 + 'common')
174 self.xmlrpc_db = xmlrpclib.ServerProxy(url_8 + 'db')
175 self.xmlrpc_object = xmlrpclib.ServerProxy(url_8 + 'object')
178 super(HttpCase, self).setUp()
179 self.registry.enter_test_mode()
180 # setup a magic session_id that will be rollbacked
181 self.session = openerp.http.root.session_store.new()
182 self.session_id = self.session.sid
184 openerp.http.root.session_store.save(self.session)
185 # setup an url opener helper
186 self.opener = urllib2.OpenerDirector()
187 self.opener.add_handler(urllib2.UnknownHandler())
188 self.opener.add_handler(urllib2.HTTPHandler())
189 self.opener.add_handler(urllib2.HTTPSHandler())
190 self.opener.add_handler(urllib2.HTTPCookieProcessor())
191 self.opener.add_handler(RedirectHandler())
192 self.opener.addheaders.append(('Cookie', 'session_id=%s' % self.session_id))
195 self.registry.leave_test_mode()
196 super(HttpCase, self).tearDown()
198 def url_open(self, url, data=None, timeout=10):
199 if url.startswith('/'):
200 url = "http://localhost:%s%s" % (PORT, url)
201 return self.opener.open(url, data, timeout)
203 def authenticate(self, user, password):
205 url = '/login?%s' % werkzeug.urls.url_encode({'db': DB,'login': user, 'key': password})
206 auth = self.url_open(url)
207 assert auth.getcode() < 400, "Auth failure %d" % auth.getcode()
209 def phantom_poll(self, phantom, timeout):
210 """ Phantomjs Test protocol.
212 Use console.log in phantomjs to output test results:
214 - for a success: console.log("ok")
215 - for an error: console.log("error")
217 Other lines are relayed to the test log.
221 td = timedelta(seconds=timeout)
225 self.assertLess(datetime.now() - t0, td,
226 "PhantomJS tests should take less than %s seconds" % timeout)
230 ready, _, _ = select.select([phantom.stdout], [], [], 0.5)
231 except select.error, e:
232 # In Python 2, select.error has no relation to IOError or
233 # OSError, and no errno/strerror/filename, only a pair of
234 # unnamed arguments (matching errno and strerror)
236 if err == errno.EINTR:
241 s = phantom.stdout.read(1)
248 line, buf = buf.split('\n', 1)
251 # relay everything from console.log, even 'ok' or 'error...' lines
252 _logger.info("phantomjs: %s", line)
256 if line.startswith("error"):
258 # when error occurs the execution stack may be sent as as JSON
260 line_ = json.loads(line_)
263 self.fail(line_ or "phantomjs test failed")
265 def phantom_run(self, cmd, timeout):
266 _logger.info('phantom_run executing %s', ' '.join(cmd))
268 ls_glob = os.path.expanduser('~/.qws/share/data/Ofi Labs/PhantomJS/http_localhost_%s.*'%PORT)
269 for i in glob.glob(ls_glob):
270 _logger.info('phantomjs unlink localstorage %s', i)
273 phantom = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
275 raise unittest2.SkipTest("PhantomJS not found")
277 self.phantom_poll(phantom, timeout)
279 # kill phantomjs if phantom.exit() wasn't called in the test
280 if phantom.poll() is None:
283 self._wait_remaining_requests()
284 _logger.info("phantom_run execution finished")
286 def _wait_remaining_requests(self):
287 t0 = int(time.time())
288 for thread in threading.enumerate():
289 if thread.name.startswith('openerp.service.http.request.'):
290 while thread.isAlive():
291 # Need a busyloop here as thread.join() masks signals
292 # and would prevent the forced shutdown.
295 t1 = int(time.time())
297 _logger.info('remaining requests')
298 openerp.tools.misc.dumpstacks()
301 def phantom_jsfile(self, jsfile, timeout=60, **kw):
306 'session_id': self.session_id,
309 phantomtest = os.path.join(os.path.dirname(__file__), 'phantomtest.js')
310 # phantom.args[0] == phantomtest path
311 # phantom.args[1] == options
314 jsfile, phantomtest, json.dumps(options)
316 self.phantom_run(cmd, timeout)
318 def phantom_js(self, url_path, code, ready="window", login=None, timeout=60, **kw):
319 """ Test js code running in the browser
320 - optionnally log as 'login'
321 - load page given by url_path
322 - wait for ready object to be available
323 - eval(code) inside the page
325 To signal success test do:
328 To signal failure do:
331 If neither are done before timeout test fails.
336 'url_path': url_path,
341 'session_id': self.session_id,
344 options.setdefault('password', options.get('login'))
345 phantomtest = os.path.join(os.path.dirname(__file__), 'phantomtest.js')
346 cmd = ['phantomjs', phantomtest, json.dumps(options)]
347 self.phantom_run(cmd, timeout)
350 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: