# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from collections import Counter import datetime import errno from lxml import etree 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, tagged 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']._activate_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.assertEqual(len(self._any_ira_for_bundle('js')), 0) self.assertEqual(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.assertEqual(len(self._any_ira_for_bundle('js')), 1) self.assertEqual(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.assertEqual(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.assertEqual(len(self._any_ira_for_bundle('js')), 1) version1 = bundle1.version ira1 = self._any_ira_for_bundle('js') date1 = ira1.create_date self.assertEqual(version0, version1) self.assertEqual(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.assertNotEqual(last_modified0, last_modified1) self.assertNotEqual(version0, version1) # check if the previous attachment is correctly cleaned self.assertEqual(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.assertEqual(len(self._any_ira_for_bundle('js')), 1) view_arch = """ """ % format_data).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.assertEqual(len(attachments), 0) asset_data = etree.HTML(html).xpath('//*[@data-asset-xmlid]')[0] asset_xmlid = asset_data.attrib.get('data-asset-xmlid') asset_version = asset_data.attrib.get('data-asset-version') format_data = { "asset_xmlid": asset_xmlid, "asset_version": asset_version, } self.assertEqual(html.strip(), (""" """ % format_data).encode('utf8')) @tagged('-at_install', 'post_install') class TestAssetsBundleInBrowser(HttpCase): def test_01_js_interpretation(self): """ Checks that the javascript of a bundle is correctly interpreted. """ self.browser_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 = """ """ 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.browser_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.assertEqual(self.counter['create'], int(should_create)) self.assertEqual(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)