summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2018-10-18 14:26:26 +0200
committerChristophe Simonis <chs@odoo.com>2018-10-18 14:26:26 +0200
commitb66a37d2c55f6bf6fb4902b766a4ef259faab3dc (patch)
tree431346fd78965ae9c0b53253cf39a61fc61599b5
parente91b16b734b39ad7018d0d2acc5be8b6963c337e (diff)
parent8f8060faae8a26784baa288f7722f322200faf62 (diff)
[MERGE] Backport of chrome headless support
For unknown [1] reason, phantomjs starts to segfault when running qunit tests. Be radical with simply not using phantomjs anymore. [1] Any guidance on tracking the root cause may help...
-rw-r--r--addons/web_tour/static/src/js/tour_manager.js3
-rw-r--r--odoo/tests/common.py578
-rw-r--r--odoo/tests/phantomtest.js145
3 files changed, 433 insertions, 293 deletions
diff --git a/addons/web_tour/static/src/js/tour_manager.js b/addons/web_tour/static/src/js/tour_manager.js
index 599e6c6b2f5..7a63edfe342 100644
--- a/addons/web_tour/static/src/js/tour_manager.js
+++ b/addons/web_tour/static/src/js/tour_manager.js
@@ -437,7 +437,8 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
console.log(log);
});
console.log(document.body.outerHTML);
- console.log("error " + error); // phantomJS wait for message starting by error
+ console.error(error); // will be displayed as error info
+ console.log("error"); // phantomJS wait for message starting by error to stop
} else {
console.log(_.str.sprintf("Tour %s succeeded", tour_name));
console.log("ok"); // phantomJS wait for exact message "ok"
diff --git a/odoo/tests/common.py b/odoo/tests/common.py
index b2e0b449b43..9ee1def1aac 100644
--- a/odoo/tests/common.py
+++ b/odoo/tests/common.py
@@ -4,38 +4,49 @@ The module :mod:`odoo.tests.common` provides unittest test cases and a few
helpers and classes to write tests.
"""
+import base64
import collections
-import errno
-import glob
import importlib
+import inspect
import itertools
import json
import logging
import operator
import os
+import platform
import re
-import select
+import requests
+import shutil
+import signal
import subprocess
+import tempfile
import threading
import time
import unittest
+import werkzeug.urls
from contextlib import contextmanager
-from datetime import datetime, timedelta, date
-from pprint import pformat
+from datetime import datetime, date
-import requests
from decorator import decorator
from lxml import etree, html
from odoo.models import BaseModel
from odoo.osv.expression import normalize_domain
from odoo.tools import pycompat
+from odoo.tools.misc import find_in_path
from odoo.tools.safe_eval import safe_eval
try:
from itertools import zip_longest as izip_longest
except ImportError:
from itertools import izip_longest
+
+try:
+ import websocket
+except ImportError:
+ # chrome headless tests will be skipped
+ websocket = None
+
try:
from xmlrpc import client as xmlrpclib
except ImportError:
@@ -326,10 +337,350 @@ class SavepointCase(SingleTransactionCase):
super(SavepointCase, self).tearDown()
+class ChromeBrowser():
+ """ Helper object to control a Chrome headless process. """
+
+ def __init__(self, logger):
+ self._logger = logger
+ if websocket is None:
+ self._logger.warning("websocket-client module is not installed")
+ raise unittest.SkipTest("websocket-client module is not installed")
+ self.devtools_port = PORT + 2
+ self.ws_url = '' # WebSocketUrl
+ self.ws = None # websocket
+ self.request_id = 0
+ self.user_data_dir = tempfile.mkdtemp(suffix='_chrome_odoo')
+ self.chrome_process = None
+ self.screencast_frames = []
+ self._chrome_start()
+ self._find_websocket()
+ self._logger.info('Websocket url found: %s', self.ws_url)
+ self._open_websocket()
+ self._logger.info('Enable chrome headless console log notification')
+ self._websocket_send('Runtime.enable')
+ self._logger.info('Chrome headless enable page notifications')
+ self._websocket_send('Page.enable')
+ self.sigxcpu_handler = None
+ if os.name == 'posix':
+ self.sigxcpu_handler = signal.getsignal(signal.SIGXCPU)
+ signal.signal(signal.SIGXCPU, self.signal_handler)
+
+ def signal_handler(self, sig, frame):
+ if sig == signal.SIGXCPU:
+ _logger.info('CPU time limit reached, stopping Chrome and shutting down')
+ self.stop()
+ os._exit(0)
+
+ def stop(self):
+ if self.chrome_process is not None:
+ self._logger.info("Closing chrome headless with pid %s", self.chrome_process.pid)
+ self._websocket_send('Browser.close')
+ if self.chrome_process.poll() is None:
+ self._logger.info("Terminating chrome headless with pid %s", self.chrome_process.pid)
+ self.chrome_process.terminate()
+ self.chrome_process.wait()
+ if self.user_data_dir and os.path.isdir(self.user_data_dir) and self.user_data_dir != '/':
+ self._logger.info('Removing chrome user profile "%s"', self.user_data_dir)
+ shutil.rmtree(self.user_data_dir)
+ # Restore previous signal handler
+ if self.sigxcpu_handler and os.name == 'posix':
+ signal.signal(signal.SIGXCPU, self.sigxcpu_handler)
+
+ @property
+ def executable(self):
+ system = platform.system()
+ if system == 'Linux':
+ for bin_ in ['google-chrome', 'chromium', 'chromium-browser']:
+ try:
+ return find_in_path(bin_)
+ except IOError:
+ continue
+
+ elif system == 'Darwin':
+ bins = [
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
+ ]
+ for bin_ in bins:
+ if os.path.exists(bin_):
+ return bin_
+
+ elif system == 'Windows':
+ # TODO: handle windows platform: https://stackoverflow.com/a/40674915
+ pass
+
+ raise unittest.SkipTest("Chrome executable not found")
+
+ def _chrome_start(self):
+ if self.chrome_process is not None:
+ return
+ switches = {
+ '--headless': '',
+ '--enable-logging': 'stderr',
+ '--no-default-browser-check': '',
+ '--no-first-run': '',
+ '--disable-extensions': '',
+ '--user-data-dir': self.user_data_dir,
+ '--disable-translate': '',
+ '--window-size': '1366x768',
+ '--remote-debugging-port': str(self.devtools_port)
+ }
+ cmd = [self.executable]
+ cmd += ['%s=%s' % (k, v) if v else k for k, v in switches.items()]
+ url = 'about:blank'
+ cmd.append(url)
+ self._logger.info('chrome_run executing %s', ' '.join(cmd))
+ try:
+ self.chrome_process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ except OSError:
+ raise unittest.SkipTest("%s not found" % cmd[0])
+ self._logger.info('Chrome pid: %s', self.chrome_process.pid)
+
+ def _find_websocket(self):
+ version = self._json_command('version')
+ self._logger.info('Browser version: %s', version['Browser'])
+ try:
+ infos = self._json_command('')[0] # Infos about the first tab
+ except IndexError:
+ self._logger.warning('No tab found in Chrome')
+ self.stop()
+ raise unittest.SkipTest('No tab found in Chrome')
+ self.ws_url = infos['webSocketDebuggerUrl']
+ self._logger.info('Chrome headless temporary user profile dir: %s', self.user_data_dir)
+
+ def _json_command(self, command, timeout=3):
+ """
+ Inspect dev tools with get
+ Available commands:
+ '' : return list of tabs with their id
+ list (or json/): list tabs
+ new : open a new tab
+ activate/ + an id: activate a tab
+ close/ + and id: close a tab
+ version : get chrome and dev tools version
+ protocol : get the full protocol
+ """
+ self._logger.info("Issuing json command %s", command)
+ command = os.path.join('json', command).strip('/')
+ while timeout > 0:
+ try:
+ url = werkzeug.urls.url_join('http://127.0.0.1:%s/' % self.devtools_port, command)
+ self._logger.info('Url : %s', url)
+ r = requests.get(url, timeout=3)
+ if r.ok:
+ return r.json()
+ return {'status_code': r.status_code}
+ except requests.ConnectionError:
+ time.sleep(0.1)
+ timeout -= 0.1
+ except requests.exceptions.ReadTimeout:
+ break
+ self._logger.error('Could not connect to chrome debugger')
+ raise unittest.SkipTest("Cannot connect to chrome headless")
+
+ def _open_websocket(self):
+ self.ws = websocket.create_connection(self.ws_url)
+ if self.ws.getstatus() != 101:
+ raise unittest.SkipTest("Cannot connect to chrome dev tools")
+ self.ws.settimeout(0.01)
+
+ def _websocket_send(self, method, params=None):
+ """
+ send chrome devtools protocol commands through websocket
+ """
+ sent_id = self.request_id
+ payload = {
+ 'method': method,
+ 'id': sent_id,
+ }
+ if params:
+ payload.update({'params': params})
+ self.ws.send(json.dumps(payload))
+ self.request_id += 1
+ return sent_id
+
+ def _websocket_wait_id(self, awaited_id, timeout=10):
+ """
+ blocking wait for a certain id in a response
+ warning other messages are discarded
+ """
+ start_time = time.time()
+ while time.time() - start_time < timeout:
+ try:
+ res = json.loads(self.ws.recv())
+ except websocket.WebSocketTimeoutException:
+ res = None
+ if res and res.get('id') == awaited_id:
+ return res
+ self._logger.info('timeout exceeded while waiting for id : %d', awaited_id)
+ return {}
+
+ def _websocket_wait_event(self, method, params=None, timeout=10):
+ """
+ blocking wait for a particular event method and eventually a dict of params
+ """
+ start_time = time.time()
+ while time.time() - start_time < timeout:
+ try:
+ res = json.loads(self.ws.recv())
+ except websocket.WebSocketTimeoutException:
+ res = None
+ if res and res.get('method', '') == method:
+ if params:
+ if set(params).issubset(set(res.get('params', {}))):
+ return res
+ else:
+ return res
+ elif res:
+ self._logger.debug('chrome devtools protocol event: %s', res)
+ self._logger.info('timeout exceeded while waiting for : %s', method)
+
+ def _get_shotname(self, prefix, ext):
+ """ return a unique filename for screenshot or screencast"""
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
+ base_file = os.path.splitext(odoo.tools.config['logfile'])[0]
+ name = '%s_%s_%s.%s' % (base_file, prefix, timestamp, ext)
+ return name
+
+ def take_screenshot(self, prefix='failed'):
+ if not odoo.tools.config['logfile']:
+ self._logger.info('Screenshot disabled !')
+ return None
+ ss_id = self._websocket_send('Page.captureScreenshot')
+ self._logger.info('Asked for screenshot (id: %s)', ss_id)
+ res = self._websocket_wait_id(ss_id)
+ base_png = res.get('result', {}).get('data')
+ decoded = base64.decodebytes(bytes(base_png.encode('utf-8')))
+ outfile = self._get_shotname(prefix, 'png')
+ with open(outfile, 'wb') as f:
+ f.write(decoded)
+ self._logger.info('Screenshot in: %s', outfile)
+
+ def _save_screencast(self, prefix='failed'):
+ # could be encododed with something like that
+ # ffmpeg -framerate 3 -i frame_%05d.png output.mp4
+ if not odoo.tools.config['logfile']:
+ self._logger.info('Screencast disabled !')
+ return None
+ sdir = tempfile.mkdtemp(suffix='_chrome_odoo_screencast')
+ nb = 0
+ for frame in self.screencast_frames:
+ outfile = os.path.join(sdir, 'frame_%05d.png' % nb)
+ with open(outfile, 'wb') as f:
+ f.write(base64.decodebytes(bytes(frame.get('data').encode('utf-8'))))
+ nb += 1
+ framerate = int(nb / (self.screencast_frames[nb-1].get('metadata').get('timestamp') - self.screencast_frames[0].get('metadata').get('timestamp')))
+ outfile = self._get_shotname(prefix, 'mp4')
+ r = subprocess.run(['ffmpeg', '-framerate', str(framerate), '-i', '%s/frame_%%05d.png' % sdir, outfile])
+ shutil.rmtree(sdir)
+ if r.returncode == 0:
+ self._logger.info('Screencast in: %s', outfile)
+
+ def start_screencast(self):
+ self._websocket_send('Page.startScreencast', {'params': {'everyNthFrame': 5, 'maxWidth': 683, 'maxHeight': 384}})
+
+ def set_cookie(self, name, value, path, domain):
+ params = {'name': name, 'value': value, 'path': path, 'domain': domain}
+ self._websocket_send('Network.setCookie', params=params)
+
+ def _wait_ready(self, ready_code, timeout=60):
+ self._logger.info('Evaluate ready code "%s"', ready_code)
+ awaited_result = {'result': {'type': 'boolean', 'value': True}}
+ ready_id = self._websocket_send('Runtime.evaluate', params={'expression': ready_code})
+ last_bad_res = ''
+ start_time = time.time()
+ tdiff = time.time() - start_time
+ has_exceeded = False
+ while tdiff < timeout:
+ try:
+ res = json.loads(self.ws.recv())
+ except websocket.WebSocketTimeoutException:
+ res = None
+ if res and res.get('id') == ready_id:
+ if res.get('result') == awaited_result:
+ return True
+ else:
+ last_bad_res = res
+ ready_id = self._websocket_send('Runtime.evaluate', params={'expression': ready_code})
+ tdiff = time.time() - start_time
+ if tdiff >= 2 and not has_exceeded:
+ has_exceeded = True
+ self._logger.warning('The ready code takes too much time : %s', tdiff)
+
+ self.take_screenshot(prefix='failed_ready')
+ self._logger.info('Ready code last try result: %s', last_bad_res or res)
+ return False
+
+ def _wait_code_ok(self, code, timeout):
+ self._logger.info('Evaluate test code "%s"', code)
+ code_id = self._websocket_send('Runtime.evaluate', params={'expression': code})
+ start_time = time.time()
+ logged_error = False
+ while time.time() - start_time < timeout:
+ try:
+ res = json.loads(self.ws.recv())
+ except websocket.WebSocketTimeoutException:
+ res = None
+ if res and res.get('id', -1) == code_id:
+ self._logger.info('Code start result: %s', res)
+ if res.get('result', {}).get('result').get('subtype', '') == 'error':
+ self._logger.error("Running code returned an error")
+ return False
+ elif res and res.get('method') == 'Runtime.consoleAPICalled' and res.get('params', {}).get('type') in ('log', 'error'):
+ logs = res.get('params', {}).get('args')
+ log_type = res.get('params', {}).get('type')
+ content = " ".join([str(log.get('value', '')) for log in logs])
+ if log_type == 'error':
+ self._logger.error(content)
+ logged_error = True
+ else:
+ self._logger.info('console log: %s', content)
+ for log in logs:
+ if log.get('type', '') == 'string' and log.get('value', '').lower() == 'ok':
+ # it is possible that some tests returns ok while an error was shown in logs.
+ # since runbot should always be red in this case, better explicitly fail.
+ if logged_error:
+ return False
+ return True
+ elif log.get('type', '') == 'string' and log.get('value', '').lower().startswith('error'):
+ self.take_screenshot()
+ self._save_screencast()
+ return False
+ elif res and res.get('method') == 'Page.screencastFrame':
+ self.screencast_frames.append(res.get('params'))
+ elif res:
+ self._logger.debug('chrome devtools protocol event: %s', res)
+ self._logger.error('Script timeout exceeded : %s', (time.time() - start_time))
+ self.take_screenshot()
+ return False
+
+ def navigate_to(self, url, wait_stop=False):
+ self._logger.info('Navigating to: "%s"', url)
+ nav_id = self._websocket_send('Page.navigate', params={'url': url})
+ nav_result = self._websocket_wait_id(nav_id)
+ self._logger.info("Navigation result: %s", nav_result)
+ frame_id = nav_result.get('result', {}).get('frameId', '')
+ if wait_stop and frame_id:
+ self._logger.info('Waiting for frame "%s" to stop loading', frame_id)
+ self._websocket_wait_event('Page.frameStoppedLoading', params={'frameId': frame_id})
+
+ def clear(self):
+ self._websocket_send('Page.stopScreencast')
+ self.screencast_frames = []
+ self._websocket_send('Page.stopLoading')
+ self._logger.info('Deleting cookies and clearing local storage')
+ dc_id = self._websocket_send('Network.clearBrowserCookies')
+ self._websocket_wait_id(dc_id)
+ cl_id = self._websocket_send('Runtime.evaluate', params={'expression': 'localStorage.clear()'})
+ self._websocket_wait_id(cl_id)
+ self.navigate_to('about:blank', wait_stop=True)
+
+
class HttpCase(TransactionCase):
- """ Transactional HTTP TestCase with url_open and phantomjs helpers.
+ """ Transactional HTTP TestCase with url_open and Chrome headless helpers.
"""
registry_test_mode = True
+ browser = None
def __init__(self, methodName='runTest'):
super(HttpCase, self).__init__(methodName)
@@ -338,9 +689,25 @@ class HttpCase(TransactionCase):
self.xmlrpc_common = xmlrpclib.ServerProxy(url_8 + 'common')
self.xmlrpc_db = xmlrpclib.ServerProxy(url_8 + 'db')
self.xmlrpc_object = xmlrpclib.ServerProxy(url_8 + 'object')
+ cls = type(self)
+ self._logger = logging.getLogger('%s.%s' % (cls.__module__, cls.__name__))
+
+ @classmethod
+ def start_browser(cls, logger):
+ # start browser on demand
+ if cls.browser is None:
+ cls.browser = ChromeBrowser(logger)
+
+ @classmethod
+ def tearDownClass(cls):
+ if cls.browser:
+ cls.browser.stop()
+ cls.browser = None
+ super(HttpCase, cls).tearDownClass()
def setUp(self):
super(HttpCase, self).setUp()
+
if self.registry_test_mode:
self.registry.enter_test_mode(self.cr)
self.addCleanup(self.registry.leave_test_mode)
@@ -360,6 +727,27 @@ class HttpCase(TransactionCase):
return self.opener.post(url, data=data, timeout=timeout)
return self.opener.get(url, timeout=timeout)
+ def _wait_remaining_requests(self):
+ t0 = int(time.time())
+ for thread in threading.enumerate():
+ if thread.name.startswith('odoo.service.http.request.'):
+ join_retry_count = 10
+ while thread.isAlive():
+ # Need a busyloop here as thread.join() masks signals
+ # and would prevent the forced shutdown.
+ thread.join(0.05)
+ join_retry_count -= 1
+ if join_retry_count < 0:
+ self._logger.warning("Stop waiting for thread %s handling request for url %s",
+ thread.name, getattr(thread, 'url', '<UNKNOWN>'))
+ break
+ time.sleep(0.5)
+ t1 = int(time.time())
+ if t0 != t1:
+ self._logger.info('remaining requests')
+ odoo.tools.misc.dumpstacks()
+ t0 = t1
+
def authenticate(self, user, password):
# stay non-authenticated
if user is None:
@@ -383,130 +771,11 @@ class HttpCase(TransactionCase):
session._fix_lang(session.context)
odoo.http.root.session_store.save(session)
+ if self.browser:
+ self._logger.info('Setting session cookie in browser')
+ self.browser.set_cookie('session_id', self.session_id, '/', '127.0.0.1')
- def phantom_poll(self, phantom, timeout):
- """ Phantomjs Test protocol.
-
- Use console.log in phantomjs to output test results:
-
- - for a success: console.log("ok")
- - for an error: console.log("error")
-
- Other lines are relayed to the test log.
-
- """
- logger = _logger.getChild('phantomjs')
- t0 = datetime.now()
- td = timedelta(seconds=timeout)
- buf = bytearray()
- pid = phantom.stdout.fileno()
- while True:
- # timeout
- self.assertLess(datetime.now() - t0, td,
- "PhantomJS tests should take less than %s seconds" % timeout)
-
- # read a byte
- try:
- ready, _, _ = select.select([pid], [], [], 0.5)
- except select.error as e:
- # In Python 2, select.error has no relation to IOError or
- # OSError, and no errno/strerror/filename, only a pair of
- # unnamed arguments (matching errno and strerror)
- err, _ = e.args
- if err == errno.EINTR:
- continue
- raise
-
- if not ready:
- continue
-
- s = os.read(pid, 4096)
- if not s:
- self.fail("Ran out of data to read")
- buf.extend(s)
-
- # process lines
- while b'\n' in buf and (not buf.startswith(b'<phantomLog>') or b'</phantomLog>' in buf):
-
- if buf.startswith(b'<phantomLog>'):
- line, buf = buf[12:].split(b'</phantomLog>\n', 1)
- else:
- line, buf = buf.split(b'\n', 1)
- line = line.decode('utf-8')
-
- lline = line.lower()
- if lline.startswith(("error", "server application error")):
- try:
- # when errors occur the execution stack may be sent as a JSON
- prefix = lline.index('error') + 6
- self.fail(pformat(json.loads(line[prefix:])))
- except ValueError:
- self.fail(lline)
- elif lline.startswith("warning"):
- logger.warn(line)
- else:
- logger.info(line)
-
- if line == "ok":
- return True
-
- def phantom_run(self, cmd, timeout):
- _logger.info('phantom_run executing %s', ' '.join(cmd))
-
- ls_glob = os.path.expanduser('~/.qws/share/data/Ofi Labs/PhantomJS/http_%s_%s.*' % (HOST, PORT))
- ls_glob2 = os.path.expanduser('~/.local/share/Ofi Labs/PhantomJS/http_%s_%s.*' % (HOST, PORT))
- for i in (glob.glob(ls_glob) + glob.glob(ls_glob2)):
- _logger.info('phantomjs unlink localstorage %s', i)
- os.unlink(i)
- try:
- phantom = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=None)
- except OSError:
- raise unittest.SkipTest("PhantomJS not found")
- try:
- result = self.phantom_poll(phantom, timeout)
- self.assertTrue(
- result,
- "PhantomJS test completed without reporting success; "
- "the log may contain errors or hints.")
- finally:
- # kill phantomjs if phantom.exit() wasn't called in the test
- if phantom.poll() is None:
- _logger.info("Terminating phantomjs")
- phantom.terminate()
- phantom.wait()
- else:
- # if we had to terminate phantomjs its return code is
- # always -15 so we don't care
- # check PhantomJS health
- from signal import SIGSEGV
- _logger.info("Phantom JS return code: %d" % phantom.returncode)
- if phantom.returncode == -SIGSEGV:
- _logger.error("Phantom JS has crashed (segmentation fault) during testing; log may not be relevant")
-
- self._wait_remaining_requests()
-
- def _wait_remaining_requests(self):
- t0 = int(time.time())
- for thread in threading.enumerate():
- if thread.name.startswith('odoo.service.http.request.'):
- join_retry_count = 10
- while thread.isAlive():
- # Need a busyloop here as thread.join() masks signals
- # and would prevent the forced shutdown.
- thread.join(0.05)
- join_retry_count -= 1
- if join_retry_count < 0:
- _logger.warning("Stop waiting for thread %s handling request for url %s",
- thread.name, thread.url)
- break
- time.sleep(0.5)
- t1 = int(time.time())
- if t0 != t1:
- _logger.info('remaining requests')
- odoo.tools.misc.dumpstacks()
- t0 = t1
-
- def phantom_js(self, url_path, code, ready="window", login=None, timeout=60, **kw):
+ def browser_js(self, url_path, code, ready='', login=None, timeout=60, **kw):
""" Test js code running in the browser
- optionnally log as 'login'
- load page given by url_path
@@ -521,23 +790,37 @@ class HttpCase(TransactionCase):
If neither are done before timeout test fails.
"""
- options = {
- 'port': PORT,
- 'db': get_db_name(),
- 'url_path': url_path,
- 'code': code,
- 'ready': ready,
- 'timeout' : timeout,
- 'session_id': self.session_id,
- }
- options.update(kw)
+ # increase timeout if coverage is running
+ if any(f.filename.endswith('/coverage/execfile.py') for f in inspect.stack() if f.filename):
+ timeout = timeout * 1.5
+ self.start_browser(self._logger)
- self.authenticate(login, login)
-
- phantomtest = os.path.join(os.path.dirname(__file__), 'phantomtest.js')
- cmd = ['phantomjs', phantomtest, json.dumps(options)]
- self.phantom_run(cmd, timeout)
+ try:
+ self.authenticate(login, login)
+ url = "http://%s:%s%s" % (HOST, PORT, url_path or '/')
+ self._logger.info('Open "%s" in browser', url)
+
+ if odoo.tools.config['logfile']:
+ self._logger.info('Starting screen cast')
+ self.browser.start_screencast()
+ self.browser.navigate_to(url)
+
+ # Needed because tests like test01.js (qunit tests) are passing a ready
+ # code = ""
+ ready = ready or "document.readyState === 'complete'"
+ self.assertTrue(self.browser._wait_ready(ready), 'The ready "%s" code was always falsy' % ready)
+ if code:
+ message = 'The test code "%s" failed' % code
+ else:
+ message = "Some js test failed"
+ self.assertTrue(self.browser._wait_code_ok(code, timeout), message)
+ finally:
+ # clear browser to make it stop sending requests, in case we call
+ # the method several times in a test method
+ self.browser.clear()
+ self._wait_remaining_requests()
+ phantom_js = browser_js
def users(*logins):
""" Decorate a method to execute it once for each given user. """
@@ -1332,10 +1615,11 @@ class TagsSelector(object):
tags = getattr(arg, 'test_tags', set())
inter_no_test = self.exclude.intersection(tags)
if inter_no_test:
- _logger.debug("Test '%s' not selected because of following tag(s): '%s'", arg, inter_no_test)
+ _logger.debug("Test '%s' not selected because it is tagged with : %s (exclusions: %s)", arg, inter_no_test, self.exclude)
return False
inter_to_test = self.include.intersection(tags)
if not inter_to_test:
- _logger.debug("Test '%s' not selected because it was not tagged with '%s'", arg, self.include)
+ _logger.debug("Test '%s' not selected because it was not tagged with %s", arg, self.include)
return False
+ _logger.debug("Test '%s' selected: tagged with %s, exclusions: %s, inclusions: %s", arg, tags, self.exclude, self.include)
return True
diff --git a/odoo/tests/phantomtest.js b/odoo/tests/phantomtest.js
deleted file mode 100644
index b77cc3ffe9d..00000000000
--- a/odoo/tests/phantomtest.js
+++ /dev/null
@@ -1,145 +0,0 @@
-// Phantomjs odoo helper
-// jshint evil: true, loopfunc: true
-
-var system = require('system');
-var HOST = '127.0.0.1';
-
-function waitFor (condition, callback, timeout, timeoutMessageCallback) {
- timeout = timeout || 10000;
- var start = new Date();
-
- (function waitLoop() {
- if(new Date() - start > timeout) {
- console.log('error', timeoutMessageCallback ? timeoutMessageCallback() : "Timeout after "+timeout+" ms");
- phantom.exit(1);
- } else if (condition()) {
- callback();
- } else {
- setTimeout(waitLoop, 250);
- }
- }());
-}
-
-function PhantomTest() {
- var self = this;
- this.options = JSON.parse(system.args[system.args.length-1]);
- this.inject = this.options.inject || [];
- this.timeout = this.options.timeout ? Math.round(parseFloat(this.options.timeout)*1000 - 5000) : 10000;
- this.origin = 'http://' + HOST;
- this.origin += this.options.port ? ':' + this.options.port : '';
-
- // ----------------------------------------------------
- // configure phantom and page
- // ----------------------------------------------------
- phantom.addCookie({
- 'domain': HOST,
- 'name': 'session_id',
- 'value': this.options.session_id,
- });
- this.page = require('webpage').create();
- this.page.viewportSize = { width: 1366, height: 768 };
- this.page.onError = function(message, trace) {
- var msg = [message];
- if (trace && trace.length) {
- msg.push.apply(msg, trace.map(function (frame) {
- var result = [' at ', frame.file, ':', frame.line];
- if (frame.function) {
- result.push(' (in ', frame.function, ')');
- }
- return result.join('');
- }));
- msg.push('(leaf frame on top)');
- }
- console.log('error', JSON.stringify(msg.join('\n')));
- phantom.exit(1);
- };
- this.page.onAlert = function(message) {
- console.log('error', message);
- phantom.exit(1);
- };
- this.page.onConsoleMessage = function(message) {
- console.log('<phantomLog>'+message+'</phantomLog>');
- };
- this.page.onLoadFinished = function(status) {
- console.log('PhantomJS: onLoadFinished. Status=', status);
- if (status === "success") {
- for (var k in self.inject) {
- var found = false;
- var v = self.inject[k];
- var need = v;
- var src = v;
- if (v[0]) {
- need = v[0];
- src = v[1];
- found = self.page.evaluate(function(code) {
- try {
- return !!eval(code);
- } catch (e) {
- return false;
- }
- }, need);
- }
- if(!found) {
- console.log('Injecting', src, 'needed for', need);
- if(!self.page.injectJs(src)) {
- console.log('error', "Cannot inject " + src);
- phantom.exit(1);
- }
- }
- }
- }
- };
- setTimeout(function () {
- self.page.evaluate(function () {
- var message = ("Timeout\nhref: " + window.location.href +
- "\nreferrer: " + document.referrer +
- "\n\n" + (document.body && document.body.innerHTML)).replace(/[^a-z0-9\s~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, "*");
- console.log('<phantomLog>error ' + message + '</phantomLog>');
- });
- phantom.exit(1);
- }, self.timeout);
-
- // ----------------------------------------------------
- // run test
- // ----------------------------------------------------
- this.run = function(url_path, code, ready) {
- var url = self.origin + url_path;
- code = code || "true";
- ready = ready || "true";
- self.page.open(url, function(status) {
- if (status !== 'success') {
- console.log('error', "failed to load " + url);
- phantom.exit(1);
- } else {
- console.log('loaded', url, status);
- // clear localstorage leftovers
- self.page.evaluate(function () { localStorage.clear() });
- // process ready
- waitFor(function() {
- console.log("PhantomTest.run: wait for condition:", ready);
- return self.page.evaluate(function (ready) {
- var r = false;
- try {
- console.log("page.evaluate eval expr:", ready);
- r = !!eval(ready);
- } catch(ex) {
- }
- console.log("page.evaluate eval result:", r);
- return r;
- }, ready);
- // run test
- }, function() {
- console.log("PhantomTest.run: condition statified, executing: " + code);
- self.page.evaluate(function (code) { return eval(code); }, code);
- console.log("PhantomTest.run: execution launched, waiting for console.log('ok')...");
- });
- }
- });
- };
-}
-
-// js mode or jsfile mode
-if(system.args.length === 2) {
- pt = new PhantomTest();
- pt.run(pt.options.url_path, pt.options.code, pt.options.ready);
-}