[IMP] tests: save screenshots and screencasts in temp
Actually, when a browser_js test is running, screencasts and screenshots
are saved only if a "--logfile" parameter is specified.
This is problematic with the runbot because '--logfile' arg is not
provided when running tests. Also, in most situations, developpers
prefer to have logs on stdout.
With this commit, two CLI args are added to specify screencasts and
screenshots directories. If no screenshots dir is provided, it defaults
to "{tmp_dir}/odoo_tests/{db_name}/screenshots".
It means that screenshots are always enabled during browser_js tests.
If screencasts arg is given, it enables screencasts and save them in
"DIR/{db_name}/screencasts". If the screencasts parameter is "1", DIR
will be the same as for screenshots.
The ffmpeg tools is used to encode frames into a video. If the ffmpeg
tool is not found on the system, the frames are kept as pngs files
instead.
closes odoo/odoo#34640
Signed-off-by: Xavier Dollé (xdo) <xdo@odoo.com>
This commit is contained in:
@@ -117,6 +117,20 @@ Running the server
|
||||
|
||||
select the tests to run by using tags.
|
||||
|
||||
.. option:: --screenshots
|
||||
|
||||
Specify directory where to write screenshots when an HttpCase.browser_js test
|
||||
fails. It defaults to :file:`/tmp/odoo_tests/{db_name}/screenshots`
|
||||
|
||||
.. option:: --screencasts
|
||||
|
||||
Enable screencasts and specify directory where to write screencasts files.
|
||||
The ``ffmpeg`` utility needs to be installed to encode frames into a video
|
||||
file. Otherwise frames will be kept instead of the video file.
|
||||
|
||||
``1``, ``t`` or ``true`` can be used to use the same directory as the above
|
||||
mentioned ``--screenshots`` option.
|
||||
|
||||
.. option:: --dev <feature,feature,...,feature>
|
||||
|
||||
* ``all``: all the features below are activated
|
||||
|
||||
@@ -513,6 +513,20 @@ business flow. It explains a sequence of steps that should be followed. The
|
||||
test runner will then create a phantom_js browser, point it to the proper url
|
||||
and simulate the click and inputs, according to the scenario.
|
||||
|
||||
Screenshots and screencasts during browser_js tests
|
||||
---------------------------------------------------
|
||||
|
||||
When running tests that use HttpCase.browser_js from the command line, the Chrome
|
||||
browser is used in headless mode. By default, if a test fails, a PNG screenshot is
|
||||
taken at the moment of the failure and written in
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
'/tmp/odoo_tests/{db_name}/screenshots/'
|
||||
|
||||
Two new command line arguments were added since Odoo 13.0 to control this behavior:
|
||||
:option:`--screenshots <odoo-bin --screenshots>` and :option:`--screencasts <odoo-bin --screencasts>`
|
||||
|
||||
|
||||
.. _qunit: http://qunitjs.com/
|
||||
.. _qunit_config.js: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/qunit_config.js#L49
|
||||
|
||||
+78
-40
@@ -462,8 +462,9 @@ class SavepointCase(SingleTransactionCase):
|
||||
class ChromeBrowser():
|
||||
""" Helper object to control a Chrome headless process. """
|
||||
|
||||
def __init__(self, logger, window_size):
|
||||
def __init__(self, logger, window_size, test_class):
|
||||
self._logger = logger
|
||||
self.test_class = test_class
|
||||
if websocket is None:
|
||||
self._logger.warning("websocket-client module is not installed")
|
||||
raise unittest.SkipTest("websocket-client module is not installed")
|
||||
@@ -473,7 +474,19 @@ class ChromeBrowser():
|
||||
self.request_id = 0
|
||||
self.user_data_dir = tempfile.mkdtemp(suffix='_chrome_odoo')
|
||||
self.chrome_process = None
|
||||
|
||||
otc = odoo.tools.config
|
||||
self.screenshots_dir = os.path.join(otc['screenshots'], get_db_name(), 'screenshots')
|
||||
self.screencasts_dir = None
|
||||
if otc['screencasts']:
|
||||
if otc['screencasts'] in ('1', 'true', 't'):
|
||||
self.screencasts_dir = os.path.join(otc['screenshots'], get_db_name(), 'screencasts')
|
||||
else:
|
||||
self.screencasts_dir =os.path.join(otc['screencasts'], get_db_name(), 'screencasts')
|
||||
|
||||
self.screencast_frames = []
|
||||
os.makedirs(self.screenshots_dir, exist_ok=True)
|
||||
|
||||
self.window_size = window_size
|
||||
self._chrome_start()
|
||||
self._find_websocket()
|
||||
@@ -660,49 +673,60 @@ class ChromeBrowser():
|
||||
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
|
||||
def take_screenshot(self, prefix='sc_', suffix=None):
|
||||
if suffix is None:
|
||||
suffix = '_%s' % self.test_class
|
||||
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:
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
|
||||
fname = '%s%s%s.png' % (prefix, timestamp,suffix)
|
||||
full_path = os.path.join(self.screenshots_dir, fname)
|
||||
with open(full_path, 'wb') as f:
|
||||
f.write(decoded)
|
||||
self._logger.info('Screenshot in: %s', outfile)
|
||||
self._logger.log(25, 'Screenshot in: %s', full_path)
|
||||
|
||||
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 !')
|
||||
if not self.screencast_frames:
|
||||
self._logger.debug('No screencast frames to encode')
|
||||
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)
|
||||
|
||||
for f in self.screencast_frames:
|
||||
with open(f['file_path'], 'rb') as b64_file:
|
||||
frame = base64.decodebytes(b64_file.read())
|
||||
os.unlink(f['file_path'])
|
||||
f['file_path'] = f['file_path'].replace('.b64', '.png')
|
||||
with open(f['file_path'], 'wb') as png_file:
|
||||
png_file.write(frame)
|
||||
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
|
||||
fname = '%s_screencast_%s.mp4' % (prefix, timestamp)
|
||||
outfile = os.path.join(self.screencasts_dir, fname)
|
||||
|
||||
try:
|
||||
ffmpeg_path = find_in_path('ffmpeg')
|
||||
except IOError:
|
||||
ffmpeg_path = None
|
||||
|
||||
if ffmpeg_path:
|
||||
framerate = int(len(self.screencast_frames) / (self.screencast_frames[-1].get('timestamp') - self.screencast_frames[0].get('timestamp')))
|
||||
r = subprocess.run([ffmpeg_path, '-framerate', str(framerate), '-i', '%s/frame_%%05d.png' % self.screencasts_dir, outfile])
|
||||
self._logger.log(25, 'Screencast in: %s', outfile)
|
||||
else:
|
||||
outfile = outfile.strip('.mp4')
|
||||
shutil.move(self.screencasts_frames_dir, outfile)
|
||||
self._logger.log(25, 'Screencast frames in: %s', outfile)
|
||||
|
||||
def start_screencast(self):
|
||||
self._websocket_send('Page.startScreencast', {'params': {'everyNthFrame': 5, 'maxWidth': 683, 'maxHeight': 384}})
|
||||
if self.screencasts_dir:
|
||||
os.makedirs(self.screencasts_dir, exist_ok=True)
|
||||
self.screencasts_frames_dir = os.path.join(self.screencasts_dir, 'frames')
|
||||
os.makedirs(self.screencasts_frames_dir, exist_ok=True)
|
||||
self._websocket_send('Page.startScreencast', params={'maxWidth': 1024, 'maxHeight': 576})
|
||||
|
||||
def set_cookie(self, name, value, path, domain):
|
||||
params = {'name': name, 'value': value, 'path': path, 'domain': domain}
|
||||
@@ -733,7 +757,7 @@ class ChromeBrowser():
|
||||
if tdiff >= 2 and not has_exceeded:
|
||||
has_exceeded = True
|
||||
|
||||
self.take_screenshot(prefix='failed_ready')
|
||||
self.take_screenshot(prefix='sc_failed_ready_')
|
||||
self._logger.info('Ready code last try result: %s', last_bad_res or res)
|
||||
return False
|
||||
|
||||
@@ -742,6 +766,7 @@ class ChromeBrowser():
|
||||
code_id = self._websocket_send('Runtime.evaluate', params={'expression': code})
|
||||
start_time = time.time()
|
||||
logged_error = False
|
||||
nb_frame = 0
|
||||
while time.time() - start_time < timeout:
|
||||
try:
|
||||
res = json.loads(self.ws.recv())
|
||||
@@ -758,7 +783,7 @@ class ChromeBrowser():
|
||||
self.take_screenshot()
|
||||
self._save_screencast()
|
||||
return False
|
||||
elif res and res.get('method') in 'Runtime.consoleAPICalled' and res.get('params', {}).get('type') in ('log', 'error', 'trace'):
|
||||
elif res and res.get('method') == 'Runtime.consoleAPICalled' and res.get('params', {}).get('type') in ('log', 'error', 'trace'):
|
||||
logs = res.get('params', {}).get('args')
|
||||
log_type = res.get('params', {}).get('type')
|
||||
content = " ".join([str(log.get('value', '')) for log in logs])
|
||||
@@ -772,7 +797,17 @@ class ChromeBrowser():
|
||||
if 'test successful' in content:
|
||||
return True
|
||||
elif res and res.get('method') == 'Page.screencastFrame':
|
||||
self.screencast_frames.append(res.get('params'))
|
||||
session_id = res.get('params').get('sessionId')
|
||||
self._websocket_send('Page.screencastFrameAck', params={'sessionId': int(session_id)})
|
||||
outfile = os.path.join(self.screencasts_frames_dir, 'frame_%05d.b64' % nb_frame)
|
||||
frame = res.get('params')
|
||||
with open(outfile, 'w') as f:
|
||||
f.write(frame.get('data'))
|
||||
nb_frame += 1
|
||||
self.screencast_frames.append({
|
||||
'file_path': outfile,
|
||||
'timestamp': frame.get('metadata').get('timestamp')
|
||||
})
|
||||
elif res:
|
||||
self._logger.debug('chrome devtools protocol event: %s', res)
|
||||
self._logger.error('Script timeout exceeded : %s', (time.time() - start_time))
|
||||
@@ -791,6 +826,8 @@ class ChromeBrowser():
|
||||
|
||||
def clear(self):
|
||||
self._websocket_send('Page.stopScreencast')
|
||||
if self.screencasts_dir and os.path.isdir(self.screencasts_frames_dir):
|
||||
shutil.rmtree(self.screencasts_frames_dir)
|
||||
self.screencast_frames = []
|
||||
self._websocket_send('Page.stopLoading')
|
||||
self._logger.info('Deleting cookies and clearing local storage')
|
||||
@@ -816,13 +853,13 @@ class HttpCase(TransactionCase):
|
||||
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__))
|
||||
cls._logger = logging.getLogger('%s.%s' % (cls.__module__, cls.__name__))
|
||||
|
||||
@classmethod
|
||||
def start_browser(cls, logger):
|
||||
def start_browser(cls):
|
||||
# start browser on demand
|
||||
if cls.browser is None:
|
||||
cls.browser = ChromeBrowser(logger, cls.browser_size)
|
||||
cls.browser = ChromeBrowser(cls._logger, cls.browser_size, cls.__name__)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
@@ -919,7 +956,8 @@ class HttpCase(TransactionCase):
|
||||
# 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.start_browser()
|
||||
|
||||
try:
|
||||
self.authenticate(login, login)
|
||||
@@ -929,8 +967,8 @@ class HttpCase(TransactionCase):
|
||||
url = "%s%s" % (base_url, url_path or '/')
|
||||
self._logger.info('Open "%s" in browser', url)
|
||||
|
||||
if odoo.tools.config['logfile']:
|
||||
self._logger.info('Starting screen cast')
|
||||
if self.browser.screencasts_dir:
|
||||
self._logger.info('Starting screencast')
|
||||
self.browser.start_screencast()
|
||||
self.browser.navigate_to(url, wait_stop=not bool(ready))
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import logging
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import odoo
|
||||
from .. import release, conf, loglevels
|
||||
from . import appdirs
|
||||
@@ -169,6 +170,13 @@ class configmanager(object):
|
||||
examples: :TestClass.test_func,/test_module,external
|
||||
""")
|
||||
|
||||
group.add_option("--screencasts", dest="screencasts", action="store", my_default=None,
|
||||
metavar='DIR',
|
||||
help="Screencasts will go in DIR/{db_name}/screencasts. '1' can be used to force the same dir as for screenshots.")
|
||||
temp_tests_dir = os.path.join(tempfile.gettempdir(), 'odoo_tests')
|
||||
group.add_option("--screenshots", dest="screenshots", action="store", my_default=temp_tests_dir,
|
||||
metavar='DIR',
|
||||
help="Screenshots will go in DIR/{db_name}/screenshots. Defaults to %s." % temp_tests_dir)
|
||||
parser.add_option_group(group)
|
||||
|
||||
# Logging Group
|
||||
@@ -413,7 +421,7 @@ class configmanager(object):
|
||||
'db_port', 'db_template', 'logfile', 'pidfile', 'smtp_port',
|
||||
'email_from', 'smtp_server', 'smtp_user', 'smtp_password',
|
||||
'db_maxconn', 'import_partial', 'addons_path',
|
||||
'syslog', 'without_demo',
|
||||
'syslog', 'without_demo', 'screencasts', 'screenshots',
|
||||
'dbfilter', 'log_level', 'log_db',
|
||||
'log_db_level', 'geoip_database', 'dev_mode', 'shell_interface'
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user