Files
odoo_source/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py
T
Raphael Collet 9920f20e4c [IMP] models: ORM speedup
This branch is the combination of several optimizations in the ORM:

* store field values once in the cache: the cache reflects more
faithfully the database, only fields that explicitly depend on the
context have an extra indirection in the cache;

* delay recomputations by default: use method `recompute` to explicitly
flush out pending recomputations;

* delay updates in method `write`: updates are stored in a data
structure that can be flushed efficiently to the database with method
`flush` (which also flush out recomputations);

* make method `modified` take advantage of inverse fields to inverse
dependencies;

* filter records by evaluating a domain on records in Python;

* a computed field with `readonly=False` behaves like a normal field
with an onchange method;

* computed fields are computed in superuser mode by default.

Work done by Toufik Ben Jaa, Raphael Collet, Denis Ledoux and Fabien
Pinckaers.

closes odoo/odoo#35659

Signed-off-by: Denis Ledoux <beledouxdenis@users.noreply.github.com>
2019-08-20 12:43:59 +00:00

611 lines
25 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import Counter
import datetime
import errno
import os
import time
from unittest.mock import patch
from odoo import api
from odoo.addons.base.models.assetsbundle import AssetsBundle
from odoo.addons.base.models.ir_attachment import IrAttachment
from odoo.modules.module import get_resource_path
from odoo.tests import HttpCase
from odoo.tests.common import TransactionCase
GETMTINE = os.path.getmtime
class FileTouchable(TransactionCase):
def setUp(self):
super(FileTouchable, self).setUp()
self.touches = {}
def _touch(self, filepath, touch_time=None):
self.touches[filepath] = touch_time or time.time()
return patch('os.path.getmtime', lambda filename: self.touches.get(filename) or GETMTINE(filename))
class TestJavascriptAssetsBundle(FileTouchable):
def setUp(self):
super(TestJavascriptAssetsBundle, self).setUp()
self.jsbundle_xmlid = 'test_assetsbundle.bundle1'
self.cssbundle_xmlid = 'test_assetsbundle.bundle2'
self.env['res.lang'].load_lang('ar_SY')
def _get_asset(self, xmlid, env=None):
env = (env or self.env)
files, remains = env['ir.qweb']._get_asset_content(xmlid, env.context)
return AssetsBundle(xmlid, files, env=env)
def _any_ira_for_bundle(self, type, lang=None):
""" Returns all ir.attachments associated to a bundle, regardless of the verion.
"""
user_direction = self.env['res.lang']._lang_get(lang or self.env.user.lang).direction
bundle = self.jsbundle_xmlid if type == 'js' else self.cssbundle_xmlid
url = '/web/content/%-%/{0}{1}.{2}'.format(('rtl/' if type == 'css' and user_direction == 'rtl' else ''), bundle, type)
domain = [('url', '=like', url)]
return self.env['ir.attachment'].search(domain)
def _node_to_list(self, nodes):
res = []
for index, (tagName, t_attrs, content) in enumerate(nodes):
for name, value in t_attrs.items():
res.append(value)
return res
def test_01_generation(self):
""" Checks that a bundle creates an ir.attachment record when its `js` method is called
for the first time.
"""
self.bundle = self._get_asset(self.jsbundle_xmlid, env=self.env)
# there shouldn't be any attachment associated to this bundle
self.assertEquals(len(self._any_ira_for_bundle('js')), 0)
self.assertEquals(len(self.bundle.get_attachments('js')), 0)
# trigger the first generation and, thus, the first save in database
self.bundle.js()
# there should be one attachment associated to this bundle
self.assertEquals(len(self._any_ira_for_bundle('js')), 1)
self.assertEquals(len(self.bundle.get_attachments('js')), 1)
def test_02_access(self):
""" Checks that the bundle's cache is working, i.e. that the bundle creates only one
ir.attachment record when rendered multiple times.
"""
bundle0 = self._get_asset(self.jsbundle_xmlid)
bundle0.js()
self.assertEquals(len(self._any_ira_for_bundle('js')), 1)
version0 = bundle0.version
ira0 = self._any_ira_for_bundle('js')
date0 = ira0.create_date
bundle1 = self._get_asset(self.jsbundle_xmlid)
bundle1.js()
self.assertEquals(len(self._any_ira_for_bundle('js')), 1)
version1 = bundle1.version
ira1 = self._any_ira_for_bundle('js')
date1 = ira1.create_date
self.assertEquals(version0, version1)
self.assertEquals(date0, date1)
def test_03_date_invalidation(self):
""" Checks that a bundle is invalidated when one of its assets' modification date is changed.
"""
bundle0 = self._get_asset(self.jsbundle_xmlid)
bundle0.js()
last_modified0 = bundle0.last_modified
version0 = bundle0.version
path = get_resource_path('test_assetsbundle', 'static', 'src', 'js', 'test_jsfile1.js')
bundle1 = self._get_asset(self.jsbundle_xmlid)
with self._touch(path):
bundle1.js()
last_modified1 = bundle1.last_modified
version1 = bundle1.version
self.assertNotEquals(last_modified0, last_modified1)
self.assertNotEquals(version0, version1)
# check if the previous attachment is correctly cleaned
self.assertEquals(len(self._any_ira_for_bundle('js')), 1)
def test_04_content_invalidation(self):
""" Checks that a bundle is invalidated when its content is modified by adding a file to
source.
"""
bundle0 = self._get_asset(self.jsbundle_xmlid)
bundle0.js()
files0 = bundle0.files
version0 = bundle0.version
self.assertEquals(len(self._any_ira_for_bundle('js')), 1)
view_arch = """
<data>
<xpath expr="." position="inside">
<script type="text/javascript" src="/test_assetsbundle/static/src/js/test_jsfile4.js"/>
</xpath>
</data>
"""
bundle = self.browse_ref(self.jsbundle_xmlid)
view = self.env['ir.ui.view'].create({
'name': 'test bundle inheritance',
'type': 'qweb',
'arch': view_arch,
'inherit_id': bundle.id,
})
bundle1 = self._get_asset(self.jsbundle_xmlid, env=self.env(context={'check_view_ids': view.ids}))
bundle1.js()
files1 = bundle1.files
version1 = bundle1.version
self.assertNotEquals(files0, files1)
self.assertNotEquals(version0, version1)
# check if the previous attachment are correctly cleaned
self.assertEquals(len(self._any_ira_for_bundle('js')), 1)
def test_05_debug(self):
""" Checks that a bundle rendered in debug mode outputs non-minified assets.
"""
debug_bundle = self._get_asset(self.jsbundle_xmlid)
nodes = debug_bundle.to_node(debug='assets')
content = self._node_to_list(nodes)
# find back one of the original asset file
self.assertIn('/test_assetsbundle/static/src/js/test_jsfile1.js', content)
# there shouldn't be any assets created in debug mode
self.assertEquals(len(self._any_ira_for_bundle('js')), 0)
def test_08_css_generation3(self):
# self.cssbundle_xlmid contains 3 rules
self.bundle = self._get_asset(self.cssbundle_xmlid)
self.bundle.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 1)
self.assertEquals(len(self.bundle.get_attachments('css')), 1)
def test_09_css_access(self):
""" Checks that the bundle's cache is working, i.e. that a bundle creates only enough
ir.attachment records when rendered multiple times.
"""
bundle0 = self._get_asset(self.cssbundle_xmlid)
bundle0.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 1)
version0 = bundle0.version
ira0 = self._any_ira_for_bundle('css')
date0 = ira0.create_date
bundle1 = self._get_asset(self.cssbundle_xmlid)
bundle1.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 1)
version1 = bundle1.version
ira1 = self._any_ira_for_bundle('css')
date1 = ira1.create_date
self.assertEquals(version0, version1)
self.assertEquals(date0, date1)
def test_11_css_content_invalidation(self):
""" Checks that a bundle is invalidated when its content is modified by adding a file to
source.
"""
bundle0 = self._get_asset(self.cssbundle_xmlid)
bundle0.css()
files0 = bundle0.files
version0 = bundle0.version
self.assertEquals(len(self._any_ira_for_bundle('css')), 1)
view_arch = """
<data>
<xpath expr="." position="inside">
<link rel="stylesheet" href="/test_assetsbundle/static/src/css/test_cssfile2.css"/>
</xpath>
</data>
"""
bundle = self.browse_ref(self.cssbundle_xmlid)
view = self.env['ir.ui.view'].create({
'name': 'test bundle inheritance',
'type': 'qweb',
'arch': view_arch,
'inherit_id': bundle.id,
})
bundle1 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'check_view_ids': view.ids}))
bundle1.css()
files1 = bundle1.files
version1 = bundle1.version
self.assertNotEquals(files0, files1)
self.assertNotEquals(version0, version1)
# check if the previous attachment are correctly cleaned
self.assertEquals(len(self._any_ira_for_bundle('css')), 1)
def test_12_css_debug(self):
""" Check that a bundle in debug mode outputs non-minified assets.
"""
debug_bundle = self._get_asset(self.cssbundle_xmlid)
nodes = debug_bundle.to_node(debug='assets')
content = self._node_to_list(nodes)
# find back one of the original asset file
self.assertIn('/test_assetsbundle/static/src/css/test_cssfile1.css', content)
# there shouldn't be any assets created in debug mode
self.assertEquals(len(self._any_ira_for_bundle('css')), 0)
def test_14_duplicated_css_assets(self):
""" Checks that if the bundle's ir.attachment record is duplicated, the bundle is only sourced once. This could
happen if multiple transactions try to render the bundle simultaneously.
"""
bundle0 = self._get_asset(self.cssbundle_xmlid)
bundle0.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 1)
# duplicate the asset bundle
ira0 = self._any_ira_for_bundle('css')
ira1 = ira0.copy()
self.assertEquals(len(self._any_ira_for_bundle('css')), 2)
self.assertEquals(ira0.store_fname, ira1.store_fname)
# the ir.attachment records should be deduplicated in the bundle's content
nodes = bundle0.to_node()
content = self._node_to_list(nodes)
self.assertEqual(content[2].count('test_assetsbundle.bundle2.css'), 1)
# Language direction specific tests
def test_15_rtl_css_generation(self):
""" Checks that a bundle creates an ir.attachment record when its `css` method is called
for the first time for language with different direction and separate bundle is created for rtl direction.
"""
self.bundle = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'lang': 'ar_SY'}))
# there shouldn't be any attachment associated to this bundle
self.assertEquals(len(self._any_ira_for_bundle('css', lang='ar_SY')), 0)
self.assertEquals(len(self.bundle.get_attachments('css')), 0)
# trigger the first generation and, thus, the first save in database
self.bundle.css()
# there should be one attachment associated to this bundle
self.assertEquals(len(self._any_ira_for_bundle('css', lang='ar_SY')), 1)
self.assertEquals(len(self.bundle.get_attachments('css')), 1)
def test_16_ltr_and_rtl_css_access(self):
""" Checks that the bundle's cache is working, i.e. that the bundle creates only one
ir.attachment record when rendered multiple times for rtl direction also check we have two css bundles,
one for ltr and one for rtl.
"""
# Assets access for en_US language
ltr_bundle0 = self._get_asset(self.cssbundle_xmlid)
ltr_bundle0.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 1)
ltr_version0 = ltr_bundle0.version
ltr_ira0 = self._any_ira_for_bundle('css')
ltr_date0 = ltr_ira0.create_date
ltr_bundle1 = self._get_asset(self.cssbundle_xmlid)
ltr_bundle1.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 1)
ltr_version1 = ltr_bundle1.version
ltr_ira1 = self._any_ira_for_bundle('css')
ltr_date1 = ltr_ira1.create_date
self.assertEquals(ltr_version0, ltr_version1)
self.assertEquals(ltr_date0, ltr_date1)
# Assets access for ar_SY language
rtl_bundle0 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'lang': 'ar_SY'}))
rtl_bundle0.css()
self.assertEquals(len(self._any_ira_for_bundle('css', lang='ar_SY')), 1)
rtl_version0 = rtl_bundle0.version
rtl_ira0 = self._any_ira_for_bundle('css', lang='ar_SY')
rtl_date0 = rtl_ira0.create_date
rtl_bundle1 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'lang': 'ar_SY'}))
rtl_bundle1.css()
self.assertEquals(len(self._any_ira_for_bundle('css', lang='ar_SY')), 1)
rtl_version1 = rtl_bundle1.version
rtl_ira1 = self._any_ira_for_bundle('css', lang='ar_SY')
rtl_date1 = rtl_ira1.create_date
self.assertEquals(rtl_version0, rtl_version1)
self.assertEquals(rtl_date0, rtl_date1)
# Checks rtl and ltr bundles are different
self.assertNotEquals(ltr_ira1.id, rtl_ira1.id)
# Check two bundles are available, one for ltr and one for rtl
css_bundles = self.env['ir.attachment'].search([
('url', '=like', '/web/content/%-%/{0}%.{1}'.format(self.cssbundle_xmlid, 'css'))
])
self.assertEquals(len(css_bundles), 2)
def test_17_css_bundle_date_invalidation(self):
""" Checks that both css bundles are invalidated when one of its assets' modification date is changed
"""
# Assets access for en_US language
ltr_bundle0 = self._get_asset(self.cssbundle_xmlid)
ltr_bundle0.css()
ltr_last_modified0 = ltr_bundle0.last_modified
ltr_version0 = ltr_bundle0.version
# Assets access for ar_SY language
rtl_bundle0 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'lang': 'ar_SY'}))
rtl_bundle0.css()
rtl_last_modified0 = rtl_bundle0.last_modified
rtl_version0 = rtl_bundle0.version
# Touch test_cssfile1.css
# Note: No lang specific context given while calling _get_asset so it will load assets for en_US
path = get_resource_path('test_assetsbundle', 'static', 'src', 'css', 'test_cssfile1.css')
ltr_bundle1 = self._get_asset(self.cssbundle_xmlid)
with self._touch(path):
ltr_bundle1.css()
ltr_last_modified1 = ltr_bundle1.last_modified
ltr_version1 = ltr_bundle1.version
ltr_ira1 = self._any_ira_for_bundle('css')
self.assertNotEquals(ltr_last_modified0, ltr_last_modified1)
self.assertNotEquals(ltr_version0, ltr_version1)
rtl_bundle1 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'lang': 'ar_SY'}))
rtl_bundle1.css()
rtl_last_modified1 = rtl_bundle1.last_modified
rtl_version1 = rtl_bundle1.version
rtl_ira1 = self._any_ira_for_bundle('css', lang='ar_SY')
self.assertNotEquals(rtl_last_modified0, rtl_last_modified1)
self.assertNotEquals(rtl_version0, rtl_version1)
# Checks rtl and ltr bundles are different
self.assertNotEquals(ltr_ira1.id, rtl_ira1.id)
# check if the previous attachment is correctly cleaned
css_bundles = self.env['ir.attachment'].search([
('url', '=like', '/web/content/%-%/{0}%.{1}'.format(self.cssbundle_xmlid, 'css'))
])
self.assertEquals(len(css_bundles), 2)
def test_18_css_bundle_content_invalidation(self):
""" Checks that a bundle is invalidated when its content is modified by adding a file to
source.
"""
# Assets for en_US
ltr_bundle0 = self._get_asset(self.cssbundle_xmlid)
ltr_bundle0.css()
ltr_files0 = ltr_bundle0.files
ltr_version0 = ltr_bundle0.version
rtl_bundle0 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'lang': 'ar_SY'}))
rtl_bundle0.css()
rtl_files0 = rtl_bundle0.files
rtl_version0 = rtl_bundle0.version
css_bundles = self.env['ir.attachment'].search([
('url', '=like', '/web/content/%-%/{0}%.{1}'.format(self.cssbundle_xmlid, 'css'))
])
self.assertEquals(len(css_bundles), 2)
view_arch = """
<data>
<xpath expr="." position="inside">
<script type="text/css" src="/test_assetsbundle/static/src/css/test_cssfile3.css"/>
</xpath>
</data>
"""
bundle = self.browse_ref(self.cssbundle_xmlid)
view = self.env['ir.ui.view'].create({
'name': 'test bundle inheritance',
'type': 'qweb',
'arch': view_arch,
'inherit_id': bundle.id,
})
ltr_bundle1 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'check_view_ids': view.ids}))
ltr_bundle1.css()
ltr_files1 = ltr_bundle1.files
ltr_version1 = ltr_bundle1.version
ltr_ira1 = self._any_ira_for_bundle('css')
self.assertNotEquals(ltr_files0, ltr_files1)
self.assertNotEquals(ltr_version0, ltr_version1)
rtl_bundle1 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'check_view_ids': view.ids, 'lang': 'ar_SY'}))
rtl_bundle1.css()
rtl_files1 = rtl_bundle1.files
rtl_version1 = rtl_bundle1.version
rtl_ira1 = self._any_ira_for_bundle('css', lang='ar_SY')
self.assertNotEquals(rtl_files0, rtl_files1)
self.assertNotEquals(rtl_version0, rtl_version1)
# Checks rtl and ltr bundles are different
self.assertNotEquals(ltr_ira1.id, rtl_ira1.id)
# check if the previous attachment are correctly cleaned
css_bundles = self.env['ir.attachment'].search([
('url', '=like', '/web/content/%-%/{0}%.{1}'.format(self.cssbundle_xmlid, 'css'))
])
self.assertEquals(len(css_bundles), 2)
def test_19_css_in_debug_assets(self):
""" Checks that a bundle rendered in debug mode(assets) with right to left language direction stores css files in assets bundle.
"""
debug_bundle = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'lang': 'ar_SY'}))
nodes = debug_bundle.to_node(debug='assets')
content = self._node_to_list(nodes)
# css file should be available in assets bundle as user's lang direction is rtl
self.assertIn('/test_assetsbundle/static/src/css/test_cssfile1/rtl/{0}.css'.format(self.cssbundle_xmlid), content)
# there should be assets(css) created in debug mode as user's lang direction is rtl
css_bundle = self.env['ir.attachment'].search([
('url', '=', '/test_assetsbundle/static/src/css/test_cssfile1/rtl/{0}.css'.format(self.cssbundle_xmlid))
])
self.assertEquals(len(css_bundle), 1)
def test_20_exteral_lib_assets(self):
html = self.env['ir.ui.view'].render_template('test_assetsbundle.template2')
attachments = self.env['ir.attachment'].search([('url', '=like', '/web/content/%-%/test_assetsbundle.bundle4.%')])
self.assertEquals(len(attachments), 2)
self.assertEqual(html.strip(), ("""<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="http://test.external.link/style1.css"/>
<link rel="stylesheet" href="http://test.external.link/style2.css"/>
<link type="text/css" rel="stylesheet" href="%(css)s"/>
<meta/>
<script type="text/javascript" src="http://test.external.link/javascript1.js"></script>
<script type="text/javascript" src="http://test.external.link/javascript2.js"></script>
<script type="text/javascript" src="%(js)s"></script>
</head>
<body>
</body>
</html>""" % {"js": attachments[0].url, "css": attachments[1].url}).encode('utf8'))
def test_21_exteral_lib_assets_debug_mode(self):
html = self.env['ir.ui.view'].render_template('test_assetsbundle.template2', {"debug": "assets"})
attachments = self.env['ir.attachment'].search([('url', '=like', '/web/content/%-%/test_assetsbundle.bundle4.%')])
self.assertEquals(len(attachments), 0)
self.assertEqual(html.strip(), ("""<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="http://test.external.link/style1.css"/>
<link rel="stylesheet" href="http://test.external.link/style2.css"/>
<link type="text/css" rel="stylesheet" href="/test_assetsbundle/static/src/css/test_cssfile1.css"/>
<link type="text/css" rel="stylesheet" href="/test_assetsbundle/static/src/css/test_cssfile2.css"/>
<meta/>
<script type="text/javascript" src="http://test.external.link/javascript1.js"></script>
<script type="text/javascript" src="http://test.external.link/javascript2.js"></script>
<script type="text/javascript" src="/test_assetsbundle/static/src/js/test_jsfile1.js"></script>
<script type="text/javascript" src="/test_assetsbundle/static/src/js/test_jsfile2.js"></script>
<script type="text/javascript" src="/test_assetsbundle/static/src/js/test_jsfile3.js"></script>
</head>
<body>
</body>
</html>""").encode('utf8'))
class TestAssetsBundleInBrowser(HttpCase):
def test_01_js_interpretation(self):
""" Checks that the javascript of a bundle is correctly interpreted.
"""
self.phantom_js(
"/test_assetsbundle/js",
"a + b + c === 6 ? console.log('test successful') : console.log('error')",
login="admin"
)
def test_02_js_interpretation_inline(self):
""" Checks that the javascript of a bundle is correctly interpretet when mixed with inline.
"""
view_arch = """
<data>
<xpath expr="." position="inside">
<script type="text/javascript">
var d = 4;
</script>
</xpath>
</data>
"""
self.env['ir.ui.view'].create({
'name': 'test bundle inheritance inline js',
'type': 'qweb',
'arch': view_arch,
'inherit_id': self.browse_ref('test_assetsbundle.bundle1').id,
})
self.env.user.flush()
self.phantom_js(
"/test_assetsbundle/js",
"a + b + c + d === 10 ? console.log('test successful') : console.log('error')",
login="admin",
)
class TestAssetsBundleWithIRAMock(FileTouchable):
def setUp(self):
super(TestAssetsBundleWithIRAMock, self).setUp()
self.stylebundle_xmlid = 'test_assetsbundle.bundle3'
self.counter = counter = Counter()
# patch methods 'create' and 'unlink' of model 'ir.attachment'
origin_create = IrAttachment.create
origin_unlink = IrAttachment.unlink
@api.model
def create(self, vals):
counter.update(['create'])
return origin_create(self, vals)
def unlink(self):
counter.update(['unlink'])
return origin_unlink(self)
self.patch(IrAttachment, 'create', create)
self.patch(IrAttachment, 'unlink', unlink)
def _get_asset(self):
files, remains = self.env['ir.qweb']._get_asset_content(self.stylebundle_xmlid, {})
return AssetsBundle(self.stylebundle_xmlid, files, env=self.env)
def _bundle(self, asset, should_create, should_unlink):
self.counter.clear()
asset.to_node(debug='assets')
self.assertEquals(self.counter['create'], int(should_create))
self.assertEquals(self.counter['unlink'], int(should_unlink))
def test_01_debug_mode_assets(self):
""" Checks that the ir.attachments records created for compiled assets in debug mode
are correctly invalidated.
"""
# Compile for the first time
self._bundle(self._get_asset(), True, False)
# Compile a second time, without changes
self._bundle(self._get_asset(), False, False)
# Touch the file and compile a third time
path = get_resource_path('test_assetsbundle', 'static', 'src', 'scss', 'test_file1.scss')
t = time.time() + 5
asset = self._get_asset()
with self._touch(path, t):
self._bundle(asset, True, True)
# Because we are in the same transaction since the beginning of the test, the first asset
# created and the second one have the same write_date, but the file's last modified date
# has really been modified. If we do not update the write_date to a posterior date, we are
# not able to reproduce the case where we compile this bundle again without changing
# anything.
self.env['ir.attachment'].flush(['checksum'])
self.cr.execute("update ir_attachment set write_date=clock_timestamp() + interval '10 seconds' where id = (select max(id) from ir_attachment)")
# Compile a fourth time, without changes
self._bundle(self._get_asset(), False, False)