[REF] tests: decorators and method to count queries (#21499)

Add the context manager method `assertQueryCount` to count queries,
the decorator `@users(...)` to execute a test for several users, and
the decorator `@warmup` to execute a test twice (warm-up, then real).
This commit is contained in:
Raphael Collet
2017-12-12 15:52:35 +01:00
committed by GitHub
parent cde06d5f5c
commit ba25420312
3 changed files with 185 additions and 156 deletions
+58 -49
View File
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.test_performance.tests.test_performance import TestPerformance, queryCount
from odoo.tests.common import TransactionCase, users, warmup
class TestMailPerformance(TestPerformance):
class TestMailPerformance(TransactionCase):
def setUp(self):
super(TestMailPerformance, self).setUp()
@@ -21,75 +21,83 @@ class TestMailPerformance(TestPerformance):
'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
})
# for performances test
self.admin = self.env.user
self.admin.login = 'admin'
@queryCount(admin=3, demo=3)
@users('admin', 'demo')
@warmup
def test_read_mail(self):
""" Read records inheriting from 'mail.thread'. """
records = self.env['test_performance.mail'].search([])
self.assertEqual(len(records), 5)
self.resetQueryCount()
# without cache
for record in records:
record.partner_id.country_id.name
with self.assertQueryCount(admin=3, demo=3):
# without cache
for record in records:
record.partner_id.country_id.name
# with cache
for record in records:
record.partner_id.country_id.name
with self.assertQueryCount(0):
# with cache
for record in records:
record.partner_id.country_id.name
# value_pc must have been prefetched, too
for record in records:
record.value_pc
with self.assertQueryCount(0):
# value_pc must have been prefetched, too
for record in records:
record.value_pc
@queryCount(admin=4, demo=4)
@users('admin', 'demo')
@warmup
def test_write_mail(self):
""" Write records inheriting from 'mail.thread' (no recomputation). """
records = self.env['test_performance.mail'].search([])
self.assertEqual(len(records), 5)
self.resetQueryCount()
records.write({'name': self.str('X')})
with self.assertQueryCount(admin=4, demo=4):
records.write({'name': 'X'})
@queryCount(admin=6, demo=6)
@users('admin', 'demo')
@warmup
def test_write_mail_with_recomputation(self):
""" Write records inheriting from 'mail.thread' (with recomputation). """
records = self.env['test_performance.mail'].search([])
self.assertEqual(len(records), 5)
self.resetQueryCount()
records.write({'value': self.int(20)})
with self.assertQueryCount(admin=6, demo=6):
records.write({'value': 42})
@queryCount(admin=20, demo=31)
@users('admin', 'demo')
@warmup
def test_write_mail_with_tracking(self):
""" Write records inheriting from 'mail.thread' (with field tracking). """
record = self.env['test_performance.mail'].search([], limit=1)
self.assertEqual(len(record), 1)
self.resetQueryCount()
record.track = self.str('X')
with self.assertQueryCount(admin=20, demo=31):
record.track = 'X'
@queryCount(admin=3, demo=3)
@users('admin', 'demo')
@warmup
def test_create_mail(self):
""" Create records inheriting from 'mail.thread' (without field tracking). """
model = self.env['test_performance.mail']
model.with_context(tracking_disable=True).create({'name': self.str('X')})
@queryCount(admin=38, demo=54)
with self.assertQueryCount(admin=3, demo=3):
model.with_context(tracking_disable=True).create({'name': 'X'})
@users('admin', 'demo')
@warmup
def test_create_mail_with_tracking(self):
""" Create records inheriting from 'mail.thread' (with field tracking). """
model = self.env['test_performance.mail']
model.create({'name': self.str('Y')})
with self.assertQueryCount(admin=38, demo=54):
self.env['test_performance.mail'].create({'name': 'X'})
@queryCount(admin=22, emp=29)
@users('admin', 'emp')
@warmup
def test_simple(self):
""" Create records inheriting from 'mail.thread' (simple models) """
self.env['mail.test.simple'].create({'name': self.str('Test')})
with self.assertQueryCount(admin=22, emp=29):
self.env['mail.test.simple'].create({'name': 'Test'})
class TestAdvMailPerformance(TestPerformance):
class TestAdvMailPerformance(TransactionCase):
def setUp(self):
super(TestAdvMailPerformance, self).setUp()
@@ -107,23 +115,24 @@ class TestAdvMailPerformance(TestPerformance):
'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
})
# for performances test
self.admin = self.env.user
self.admin.login = 'admin'
@queryCount(admin=25, emp=32)
@users('admin', 'emp')
@warmup
def test_activity(self):
self.env['mail.test.activity'].create({'name': self.str('Test')})
model = self.env['mail.test.activity']
@queryCount(admin=46, emp=51)
with self.assertQueryCount(admin=25, emp=32):
model.create({'name': 'Test'})
@users('admin', 'emp')
@warmup
def test_activity_full(self):
test_record_activity = self.env['mail.test.activity'].create({'name': self.str('Test')})
res_id = test_record_activity.id
self.resetQueryCount()
self.env['mail.activity'].with_context({
record = self.env['mail.test.activity'].create({'name': 'Test'})
model = self.env['mail.activity'].with_context({
'default_res_model': 'mail.test.activity',
}).create({
'summary': self.str('Test Activity'),
'res_id': res_id,
})
with self.assertQueryCount(admin=46, emp=51):
model.create({
'summary': 'Test Activity',
'res_id': record.id,
})
@@ -1,138 +1,76 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from contextlib import contextmanager
import functools
import logging
from odoo.tests.common import TransactionCase
_logger = logging.getLogger(__name__)
sql_logger = logging.getLogger('odoo.sql_db')
def queryCount(**counters):
""" Decorate a method to check the number of queries it makes. Counters
is a dict { 'user_login': expected_query_count } """
def decorate(func):
@functools.wraps(func)
def wrapper(self):
if hasattr(self, 'test_users'):
users = self.test_users.filtered(lambda user: user.login in counters.keys())
else:
users = self.env['res.users'].search([('login', 'in', list(counters.keys()))])
for user in users:
# switch user
self.uid = user.id
self.env = self.env(user=self.uid)
# warm up the caches
self._round = False
func(self)
self.env.cache.invalidate()
# test for real, and check query count
self._round = True
self.resetQueryCount()
func(self)
self.assertQueryCount(self.cr.sql_log_count - self._count,
counters[user.login], user.login)
return wrapper
return decorate
from odoo.tests.common import TransactionCase, users, warmup
class TestPerformance(TransactionCase):
def setUp(self):
super(TestPerformance, self).setUp()
self._round = False
def assertQueryCount(self, actual, expected, message):
self.assertLessEqual(actual, expected, message)
if actual < expected:
_logger.info("Warning: Got %d queries instead of %d: %s", actual, expected, message)
def resetQueryCount(self):
""" Reset the query counter. """
self._count = self.cr.sql_log_count
def str(self, value):
""" Return a value different from run to run. """
return value + 'z' if self._round else value
def int(self, value):
""" Return a value different from run to run. """
return value + 1 if self._round else value
@contextmanager
def logQueries(self):
""" Log the queries that are made in this scope. """
sql_log, level = self.cr.sql_log, sql_logger.getEffectiveLevel()
try:
sql_logger.setLevel(logging.DEBUG)
self.cr.sql_log = True
yield
finally:
self.cr.sql_log = sql_log
sql_logger.setLevel(level)
@queryCount(admin=3, demo=3)
@users('admin', 'demo')
@warmup
def test_read_base(self):
""" Read records. """
records = self.env['test_performance.base'].search([])
self.assertEqual(len(records), 5)
self.resetQueryCount()
# without cache
for record in records:
record.partner_id.country_id.name
with self.assertQueryCount(admin=3, demo=3):
# without cache
for record in records:
record.partner_id.country_id.name
# with cache
for record in records:
record.partner_id.country_id.name
with self.assertQueryCount(0):
# with cache
for record in records:
record.partner_id.country_id.name
# value_pc must have been prefetched, too
for record in records:
record.value_pc
with self.assertQueryCount(0):
# value_pc must have been prefetched, too
for record in records:
record.value_pc
@queryCount(admin=1, demo=1)
@users('admin', 'demo')
@warmup
def test_write_base(self):
""" Write records (no recomputation). """
records = self.env['test_performance.base'].search([])
self.assertEqual(len(records), 5)
self.resetQueryCount()
records.write({'name': self.str('X')})
with self.assertQueryCount(admin=1, demo=1):
records.write({'name': 'X'})
@queryCount(admin=3, demo=3)
@users('admin', 'demo')
@warmup
def test_write_base_with_recomputation(self):
""" Write records (with recomputation). """
records = self.env['test_performance.base'].search([])
self.assertEqual(len(records), 5)
self.resetQueryCount()
records.write({'value': self.int(20)})
with self.assertQueryCount(admin=3, demo=3):
records.write({'value': 42})
@queryCount(admin=6, demo=6)
@users('admin', 'demo')
@warmup
def test_create_base(self):
""" Create records. """
model = self.env['test_performance.base']
model.create({'name': self.str('X')})
with self.assertQueryCount(admin=6, demo=6):
self.env['test_performance.base'].create({'name': 'X'})
@queryCount(admin=38, demo=38)
@users('admin', 'demo')
@warmup
def test_create_base_with_lines(self):
""" Create records with one2many lines. """
model = self.env['test_performance.base']
model.create({
'name': self.str('Y'),
'line_ids': [(0, 0, {'value': val}) for val in range(10)],
})
with self.assertQueryCount(admin=38, demo=38):
self.env['test_performance.base'].create({
'name': 'X',
'line_ids': [(0, 0, {'value': val}) for val in range(10)],
})
@queryCount(admin=17, demo=17)
@users('admin', 'demo')
@warmup
def test_create_base_with_tags(self):
""" Create records with many2many tags. """
model = self.env['test_performance.base']
model.create({
'name': self.str('X'),
'tag_ids': [(0, 0, {'name': val}) for val in range(10)],
})
with self.assertQueryCount(admin=17, demo=17):
self.env['test_performance.base'].create({
'name': 'X',
'tag_ids': [(0, 0, {'name': val}) for val in range(10)],
})
+86 -4
View File
@@ -24,6 +24,7 @@ from datetime import datetime, timedelta, date
from pprint import pformat
import requests
from decorator import decorator
from lxml import etree, html
from odoo.models import BaseModel
@@ -121,10 +122,21 @@ class BaseCase(TreeCase):
"""
longMessage = True # more verbose error message by default: https://www.odoo.com/r/Vmh
warm = True # False during warm-up phase (see :func:`warmup`)
def cursor(self):
return self.registry.cursor()
@property
def uid(self):
""" Get the current uid. """
return self.env.uid
@uid.setter
def uid(self, user):
""" Set the uid by changing the test's environment. """
self.env = self.env(user=user)
def ref(self, xid):
""" Returns database ID for the provided :term:`external identifier`,
shortcut for ``get_object_reference``
@@ -162,6 +174,35 @@ class BaseCase(TreeCase):
else:
return self._assertRaises(exception)
@contextmanager
def assertQueryCount(self, default=0, **counters):
""" Context manager that counts queries. It may be invoked either with
one value, or with a set of named arguments like ``login=value``::
with self.assertQueryCount(42):
...
with self.assertQueryCount(admin=3, demo=5):
...
The second form is convenient when used with :func:`users`.
"""
if self.warm:
login = self.env.user.login
expected = counters.get(login, default)
count0 = self.cr.sql_log_count
yield
count = self.cr.sql_log_count - count0
if not count <= expected:
msg = "Query count for user %s: got %d instead of %d"
self.fail(msg % (login, count, expected))
elif count < expected:
logger = logging.getLogger(type(self).__module__)
msg = "Query count for user %s: got %d instead of %d"
logger.info(msg, login, count, expected)
else:
yield
def shortDescription(self):
doc = self._testMethodDoc
return doc and ' '.join(l.strip() for l in doc.splitlines() if not l.isspace()) or None
@@ -182,9 +223,8 @@ class TransactionCase(BaseCase):
self.registry = odoo.registry(get_db_name())
#: current transaction's cursor
self.cr = self.cursor()
self.uid = odoo.SUPERUSER_ID
#: :class:`~odoo.api.Environment` for the current test case
self.env = api.Environment(self.cr, self.uid, {})
self.env = api.Environment(self.cr, odoo.SUPERUSER_ID, {})
@self.addCleanup
def reset():
@@ -219,8 +259,7 @@ class SingleTransactionCase(BaseCase):
super(SingleTransactionCase, cls).setUpClass()
cls.registry = odoo.registry(get_db_name())
cls.cr = cls.registry.cursor()
cls.uid = odoo.SUPERUSER_ID
cls.env = api.Environment(cls.cr, cls.uid, {})
cls.env = api.Environment(cls.cr, odoo.SUPERUSER_ID, {})
@classmethod
def tearDownClass(cls):
@@ -468,6 +507,49 @@ class HttpCase(TransactionCase):
cmd = ['phantomjs', phantomtest, json.dumps(options)]
self.phantom_run(cmd, timeout)
def users(*logins):
""" Decorate a method to execute it once for each given user. """
@decorator
def wrapper(func, *args, **kwargs):
self = args[0]
old_uid = self.uid
try:
# retrieve users
user_id = {
user.login: user.id
for user in self.env['res.users'].search([('login', 'in', list(logins))])
}
for login in logins:
# switch user
self.uid = user_id[login]
# execute func
func(*args, **kwargs)
finally:
self.uid = old_uid
return wrapper
@decorator
def warmup(func, *args, **kwargs):
""" Decorate a test method to run it twice: once for a warming up phase, and
a second time for real. The test attribute ``warm`` is set to ``False``
during warm up, and ``True`` once the test is warmed up. Note that the
effects of the warmup phase are rolled back thanks to a savepoint.
"""
self = args[0]
# run once to warm up the caches
self.warm = False
self.cr.execute('SAVEPOINT test_warmup')
func(*args, **kwargs)
self.cr.execute('ROLLBACK TO SAVEPOINT test_warmup')
self.env.cache.invalidate()
# run once for real
self.warm = True
func(*args, **kwargs)
def can_import(module):
""" Checks if <module> can be imported, returns ``True`` if it can be,
``False`` otherwise.