Monkey-patching C types is not straightforward. It relies on changing the attributes or methods in memory at the right address with the exact right size. This requires the greatest caution. A simple mistake can mess up the memory used by the Python interpreter, and for instance lead to `SegmentationFault` exceptions or unforeseen behaviors. However, being able to patch C type is a very powerful tool. With great power comes great responsibility. `patch_c_type` is implemented with the greateast caution. The Python C-API documentation has been thoroughly followed and understood. In addition, this patch has been battle tested in real conditions. In the end, this allows to patch unwanted behaviors from types implemented in C. Co-authored-by: Denis Ledoux <dle@odoo.com> Co-authored-by: Christophe Simonis <chs@odoo.com> Co-authored-by: Mathieu Walravens <wama@odoo.com>
285 lines
14 KiB
Python
285 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import ast
|
|
|
|
from markupsafe import Markup
|
|
|
|
from odoo import Command
|
|
from odoo.tests.common import TransactionCase, BaseCase
|
|
from odoo.tools import mute_logger
|
|
from odoo.tools.safe_eval import safe_eval, const_eval, expr_eval
|
|
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
|
|
|
|
|
|
class TestSafeEval(BaseCase):
|
|
def test_const(self):
|
|
# NB: True and False are names in Python 2 not consts
|
|
expected = (1, {"a": {2.5}}, [None, u"foo"])
|
|
actual = const_eval('(1, {"a": {2.5}}, [None, u"foo"])')
|
|
self.assertEqual(actual, expected)
|
|
|
|
def test_expr(self):
|
|
# NB: True and False are names in Python 2 not consts
|
|
expected = 3 * 4
|
|
actual = expr_eval('3 * 4')
|
|
self.assertEqual(actual, expected)
|
|
|
|
def test_01_safe_eval(self):
|
|
""" Try a few common expressions to verify they work with safe_eval """
|
|
expected = (1, {"a": 9 * 2}, (True, False, None))
|
|
actual = safe_eval('(1, {"a": 9 * 2}, (True, False, None))')
|
|
self.assertEqual(actual, expected, "Simple python expressions are not working with safe_eval")
|
|
|
|
def test_02_literal_eval(self):
|
|
""" Try simple literal definition to verify it works with literal_eval """
|
|
expected = (1, {"a": 9}, (True, False, None))
|
|
actual = ast.literal_eval('(1, {"a": 9}, (True, False, None))')
|
|
self.assertEqual(actual, expected, "Simple python expressions are not working with literal_eval")
|
|
|
|
def test_03_literal_eval_arithmetic(self):
|
|
""" Try arithmetic expression in literal_eval to verify it does not work """
|
|
with self.assertRaises(ValueError):
|
|
ast.literal_eval('(1, {"a": 2*9}, (True, False, None))')
|
|
|
|
def test_04_literal_eval_forbidden(self):
|
|
""" Try forbidden expressions in literal_eval to verify they are not allowed """
|
|
with self.assertRaises(ValueError):
|
|
ast.literal_eval('{"a": True.__class__}')
|
|
|
|
@mute_logger('odoo.tools.safe_eval')
|
|
def test_05_safe_eval_forbiddon(self):
|
|
""" Try forbidden expressions in safe_eval to verify they are not allowed"""
|
|
# no forbidden builtin expression
|
|
with self.assertRaises(ValueError):
|
|
safe_eval('open("/etc/passwd","r")')
|
|
|
|
# no forbidden opcodes
|
|
with self.assertRaises(ValueError):
|
|
safe_eval("import odoo", mode="exec")
|
|
|
|
# no dunder
|
|
with self.assertRaises(NameError):
|
|
safe_eval("self.__name__", {'self': self}, mode="exec")
|
|
|
|
def test_06_safe_eval_format(self):
|
|
# string.format
|
|
self.assertEqual(safe_eval("'__{0}__'.format('Foo')"), '__Foo__')
|
|
self.assertEqual(safe_eval("'{0.__self__}'.format(abs)"), '{0.__self__}')
|
|
self.assertEqual(safe_eval("'{0.f_globals}'.format(abs)"), '{0.f_globals}')
|
|
|
|
# string.format_map
|
|
self.assertEqual(safe_eval("'__{foo}__'.format_map({'foo': 'Foo'})"), '__Foo__')
|
|
self.assertEqual(safe_eval("'{foo.__self__}'.format_map({'foo': abs})"), '{foo.__self__}')
|
|
self.assertEqual(safe_eval("'{foo.f_globals}'.format_map({'foo': abs})"), '{foo.f_globals}')
|
|
|
|
# Evaluation context for Markup asserts
|
|
c = {"Markup": Markup}
|
|
|
|
# Markup.format
|
|
self.assertEqual(safe_eval("Markup('__{0}__').format('Foo')", c), Markup('__Foo__'))
|
|
with self.assertRaisesRegex(ValueError, 'Access to forbidden name'):
|
|
safe_eval("Markup('{0.__self__}').format(abs)", c)
|
|
with self.assertRaisesRegex(ValueError, 'Access to forbidden name'):
|
|
safe_eval("Markup('{0.f_globals}').format(abs)", c)
|
|
|
|
# Markup.format_map
|
|
self.assertEqual(safe_eval("Markup('__{foo}__').format_map({'foo': 'Foo'})", c), Markup('__Foo__'))
|
|
self.assertEqual(safe_eval("Markup('{foo.__self__}').format_map({'foo': abs})", c), Markup('{foo.__self__}'))
|
|
self.assertEqual(safe_eval("Markup('{foo.f_globals}').format_map({'foo': abs})", c), Markup('{foo.f_globals}'))
|
|
|
|
def test_07_safe_eval_attribute_error_obj(self):
|
|
locals_dict = {}
|
|
try:
|
|
safe_eval("""
|
|
try:
|
|
dict.foo
|
|
except Exception as e:
|
|
action = {'args': e.args, 'obj': e.obj, 'name': e.name}
|
|
""", locals_dict=locals_dict, mode="exec", nocopy=True)
|
|
except ValueError as e:
|
|
# AttributeError.name, AttributeError.obj added in Python 3.10
|
|
# https://github.com/python/cpython/commit/37494b441aced0362d7edd2956ab3ea7801e60c8
|
|
self.assertIn("'AttributeError' object has no attribute 'obj'", e.args[0])
|
|
else:
|
|
exception = locals_dict.get('action')
|
|
self.assertEqual(exception['args'], ("type object 'dict' has no attribute 'foo'",))
|
|
self.assertIsNone(exception['name'])
|
|
self.assertIsNone(exception['obj'])
|
|
|
|
attribute_error = None
|
|
try:
|
|
raise AttributeError('Foo', name='Bar', obj=[])
|
|
except TypeError as e:
|
|
# AttributeError does not take keyword arguments before Python 3.10
|
|
# https://github.com/python/cpython/commit/37494b441aced0362d7edd2956ab3ea7801e60c8
|
|
# Error can be either, according to the Python version:
|
|
# - AttributeError does not take keyword arguments
|
|
# - AttributeError() takes no keyword arguments
|
|
self.assertIn("keyword arguments", e.args[0])
|
|
except AttributeError as e:
|
|
attribute_error = e
|
|
if attribute_error:
|
|
self.assertEqual(attribute_error.args, ('Foo',))
|
|
self.assertEqual(attribute_error.name, 'Bar')
|
|
self.assertIsNone(attribute_error.obj)
|
|
|
|
|
|
class TestParentStore(TransactionCase):
|
|
""" Verify that parent_store computation is done right """
|
|
|
|
def setUp(self):
|
|
super(TestParentStore, self).setUp()
|
|
|
|
# force res_partner_category.copy() to copy children
|
|
category = self.env['res.partner.category']
|
|
self.patch(category._fields['child_ids'], 'copy', True)
|
|
|
|
# setup categories
|
|
self.root = category.create({'name': 'Root category'})
|
|
self.cat0 = category.create({'name': 'Parent category', 'parent_id': self.root.id})
|
|
self.cat1 = category.create({'name': 'Child 1', 'parent_id': self.cat0.id})
|
|
self.cat2 = category.create({'name': 'Child 2', 'parent_id': self.cat0.id})
|
|
self.cat21 = category.create({'name': 'Child 2-1', 'parent_id': self.cat2.id})
|
|
|
|
def test_duplicate_parent(self):
|
|
""" Duplicate the parent category and verify that the children have been duplicated too """
|
|
new_cat0 = self.cat0.copy()
|
|
new_struct = new_cat0.search([('parent_id', 'child_of', new_cat0.id)])
|
|
self.assertEqual(len(new_struct), 4, "After duplication, the new object must have the childs records")
|
|
old_struct = new_cat0.search([('parent_id', 'child_of', self.cat0.id)])
|
|
self.assertEqual(len(old_struct), 4, "After duplication, previous record must have old childs records only")
|
|
self.assertFalse(new_struct & old_struct, "After duplication, nodes should not be mixed")
|
|
|
|
def test_duplicate_children_01(self):
|
|
""" Duplicate the children then reassign them to the new parent (1st method). """
|
|
new_cat1 = self.cat1.copy()
|
|
new_cat2 = self.cat2.copy()
|
|
new_cat0 = self.cat0.copy({'child_ids': []})
|
|
(new_cat1 + new_cat2).write({'parent_id': new_cat0.id})
|
|
new_struct = new_cat0.search([('parent_id', 'child_of', new_cat0.id)])
|
|
self.assertEqual(len(new_struct), 4, "After duplication, the new object must have the childs records")
|
|
old_struct = new_cat0.search([('parent_id', 'child_of', self.cat0.id)])
|
|
self.assertEqual(len(old_struct), 4, "After duplication, previous record must have old childs records only")
|
|
self.assertFalse(new_struct & old_struct, "After duplication, nodes should not be mixed")
|
|
|
|
def test_duplicate_children_02(self):
|
|
""" Duplicate the children then reassign them to the new parent (2nd method). """
|
|
new_cat1 = self.cat1.copy()
|
|
new_cat2 = self.cat2.copy()
|
|
new_cat0 = self.cat0.copy({'child_ids': [Command.set((new_cat1 + new_cat2).ids)]})
|
|
new_struct = new_cat0.search([('parent_id', 'child_of', new_cat0.id)])
|
|
self.assertEqual(len(new_struct), 4, "After duplication, the new object must have the childs records")
|
|
old_struct = new_cat0.search([('parent_id', 'child_of', self.cat0.id)])
|
|
self.assertEqual(len(old_struct), 4, "After duplication, previous record must have old childs records only")
|
|
self.assertFalse(new_struct & old_struct, "After duplication, nodes should not be mixed")
|
|
|
|
def test_duplicate_children_03(self):
|
|
""" Duplicate the children then reassign them to the new parent (3rd method). """
|
|
new_cat1 = self.cat1.copy()
|
|
new_cat2 = self.cat2.copy()
|
|
new_cat0 = self.cat0.copy({'child_ids': []})
|
|
new_cat0.write({'child_ids': [Command.link(new_cat1.id), Command.link(new_cat2.id)]})
|
|
new_struct = new_cat0.search([('parent_id', 'child_of', new_cat0.id)])
|
|
self.assertEqual(len(new_struct), 4, "After duplication, the new object must have the childs records")
|
|
old_struct = new_cat0.search([('parent_id', 'child_of', self.cat0.id)])
|
|
self.assertEqual(len(old_struct), 4, "After duplication, previous record must have old childs records only")
|
|
self.assertFalse(new_struct & old_struct, "After duplication, nodes should not be mixed")
|
|
|
|
|
|
class TestGroups(TransactionCase):
|
|
|
|
def test_res_groups_fullname_search(self):
|
|
all_groups = self.env['res.groups'].search([])
|
|
|
|
groups = all_groups.search([('full_name', 'like', 'Sale')])
|
|
self.assertItemsEqual(groups.ids, [g.id for g in all_groups if 'Sale' in g.full_name],
|
|
"did not match search for 'Sale'")
|
|
|
|
groups = all_groups.search([('full_name', 'like', 'Technical')])
|
|
self.assertItemsEqual(groups.ids, [g.id for g in all_groups if 'Technical' in g.full_name],
|
|
"did not match search for 'Technical'")
|
|
|
|
groups = all_groups.search([('full_name', 'like', 'Sales /')])
|
|
self.assertItemsEqual(groups.ids, [g.id for g in all_groups if 'Sales /' in g.full_name],
|
|
"did not match search for 'Sales /'")
|
|
|
|
groups = all_groups.search([('full_name', 'in', ['Administration / Access Rights','Contact Creation'])])
|
|
self.assertTrue(groups, "did not match search for 'Administration / Access Rights' and 'Contact Creation'")
|
|
|
|
def test_res_group_recursion(self):
|
|
# four groups with no cycle, check them all together
|
|
a = self.env['res.groups'].create({'name': 'A'})
|
|
b = self.env['res.groups'].create({'name': 'B'})
|
|
c = self.env['res.groups'].create({'name': 'G', 'implied_ids': [Command.set((a + b).ids)]})
|
|
d = self.env['res.groups'].create({'name': 'D', 'implied_ids': [Command.set(c.ids)]})
|
|
self.assertTrue((a + b + c + d)._check_m2m_recursion('implied_ids'))
|
|
|
|
# create a cycle and check
|
|
a.implied_ids = d
|
|
self.assertFalse(a._check_m2m_recursion('implied_ids'))
|
|
|
|
def test_res_group_copy(self):
|
|
a = self.env['res.groups'].with_context(lang='en_US').create({'name': 'A'})
|
|
b = a.copy()
|
|
self.assertFalse(a.name == b.name)
|
|
|
|
def test_apply_groups(self):
|
|
a = self.env['res.groups'].create({'name': 'A'})
|
|
b = self.env['res.groups'].create({'name': 'B'})
|
|
c = self.env['res.groups'].create({'name': 'C', 'implied_ids': [Command.set(a.ids)]})
|
|
|
|
# C already implies A, we want both B+C to imply A
|
|
(b + c)._apply_group(a)
|
|
|
|
self.assertIn(a, b.implied_ids)
|
|
self.assertIn(a, c.implied_ids)
|
|
|
|
def test_remove_groups(self):
|
|
u1 = self.env['res.users'].create({'login': 'u1', 'name': 'U1'})
|
|
u2 = self.env['res.users'].create({'login': 'u2', 'name': 'U2'})
|
|
default = self.env.ref('base.default_user')
|
|
portal = self.env.ref('base.group_portal')
|
|
p = self.env['res.users'].create({'login': 'p', 'name': 'P', 'groups_id': [Command.set([portal.id])]})
|
|
|
|
a = self.env['res.groups'].create({'name': 'A', 'users': [Command.set(u1.ids)]})
|
|
b = self.env['res.groups'].create({'name': 'B', 'users': [Command.set(u1.ids)]})
|
|
c = self.env['res.groups'].create({'name': 'C', 'implied_ids': [Command.set(a.ids)], 'users': [Command.set([p.id, u2.id, default.id])]})
|
|
d = self.env['res.groups'].create({'name': 'D', 'implied_ids': [Command.set(a.ids)], 'users': [Command.set([u2.id, default.id])]})
|
|
|
|
def assertUsersEqual(users, group):
|
|
self.assertEqual(
|
|
sorted([r.login for r in users]),
|
|
sorted([r.login for r in group.with_context(active_test=False).users])
|
|
)
|
|
# sanity checks
|
|
assertUsersEqual([u1, u2, p, default], a)
|
|
assertUsersEqual([u1], b)
|
|
assertUsersEqual([u2, p, default], c)
|
|
assertUsersEqual([u2, default], d)
|
|
|
|
# C already implies A, we want none of B+C to imply A
|
|
(b + c)._remove_group(a)
|
|
|
|
self.assertNotIn(a, b.implied_ids)
|
|
self.assertNotIn(a, c.implied_ids)
|
|
self.assertIn(a, d.implied_ids)
|
|
|
|
# - Since B didn't imply A, removing A from the implied groups of (B+C)
|
|
# should not remove user U1 from A, even though C implied A, since C does
|
|
# not have U1 as a user
|
|
# - P should be removed as was only added via inheritance to C
|
|
# - U2 should not be removed from A since it is implied via C but also via D
|
|
assertUsersEqual([u1, u2, default], a)
|
|
assertUsersEqual([u1], b)
|
|
assertUsersEqual([u2, p, default], c)
|
|
assertUsersEqual([u2, default], d)
|
|
|
|
# When adding the template user to a new group, it should add it to existing internal users
|
|
e = self.env['res.groups'].create({'name': 'E'})
|
|
default.write({'groups_id': [Command.link(e.id)]})
|
|
self.assertIn(u1, e.users)
|
|
self.assertIn(u2, e.users)
|
|
self.assertIn(default, e.with_context(active_test=False).users)
|
|
self.assertNotIn(p, e.users)
|