Because the `savepoint` and `clear` calls are nested inside the `assertRaises` context, if one of them happens to throw *the exception we're looking for* the interpreter will jump back to the `with`, the `assertRaises` will swallow the exception (and count it as a success) and the function will end having not gone through a `yield`. This is rather frustrating to debug as it's easy to forget that a `with` is a control flow structure, leading to a seemingly impossible error. We can fix this by initializing and `__enter__`-ing the savepoint first, but doing this by hand is a bit iffy and not really future-proof as the addition of more fallible steps during the initialization phase of `_assertRaises` could lead to the savepoint not being properly disposed of. Furthermore once in the scope of the "actual" assertRaises we want the savepoint to unwind first (otherwise the savepoint won't be rolled back when the exception *we are expecting* gets raised). As it turns out `ExitStack` offers the solution to our woes though it's a bit tricky at first glance: while modifying the cleanup queue in-place is haram, `pop_all` allows moving cleanup callbacks from one queue to the next. This means we can first add the savepoint to one stack and get its errors (if any) correctly reported, cover the rest of the initialization, then move the savepoint from one stack to an other, in order to correctly order the coverage of the `yield` (and the userland code). closes odoo/odoo#87733 X-original-commit: b1cd4e4e3c918b4b08d27b30b1017fc898d4b08a Signed-off-by: Xavier Morel (xmo) <xmo@odoo.com>
90 lines
3.4 KiB
Python
90 lines
3.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
import itertools
|
|
from unittest import mock, TestCase
|
|
|
|
import psycopg2
|
|
|
|
from odoo.exceptions import AccessError
|
|
from odoo.sql_db import BaseCursor
|
|
from odoo.tests import common
|
|
from odoo.tools import mute_logger
|
|
|
|
|
|
class CustomError(Exception):
|
|
...
|
|
|
|
class TestBasic(common.TransactionCase):
|
|
def test_assertRecordValues(self):
|
|
X1 = {'f1': "X", 'f2': 1}
|
|
Y2 = {'f1': "Y", 'f2': 2}
|
|
Y3 = {'f1': "Y", 'f2': 3}
|
|
records = self.env['test_testing_utilities.a'].create([X1, Y2])
|
|
|
|
self.assertRecordValues(records, [X1, Y2])
|
|
|
|
with self.assertRaises(AssertionError):
|
|
# order should match
|
|
self.assertRecordValues(records, [Y2, X1])
|
|
|
|
# fail if wrong size
|
|
with self.assertRaises(AssertionError):
|
|
self.assertRecordValues(records, [X1])
|
|
with self.assertRaises(AssertionError):
|
|
self.assertRecordValues(records, [X1, Y2, Y3])
|
|
|
|
# fail if fields don't match
|
|
with self.assertRaises(AssertionError):
|
|
self.assertRecordValues(records, [X1, Y3])
|
|
with self.assertRaises(AssertionError):
|
|
self.assertRecordValues(records, [Y3, X1])
|
|
|
|
def test_assertRaises_rollbacks(self):
|
|
"""Checks that a "correctly" executing assertRaises (where the expected
|
|
exception has been raised and caught) will properly rollback.
|
|
"""
|
|
self.env.cr.execute("SET LOCAL test_testing_utilities.a_flag = ''")
|
|
with self.assertRaises(CustomError):
|
|
self.env.cr.execute("SET LOCAL test_testing_utilities.a_flag = 'yes'")
|
|
raise CustomError
|
|
|
|
self.env.cr.execute("SHOW test_testing_utilities.a_flag")
|
|
self.assertEqual(self.env.cr.fetchone(), ('',))
|
|
|
|
def test_assertRaises_error(self):
|
|
"""Checks that an exception raised during the *setup* of assertRaises
|
|
bubbles up correctly.
|
|
|
|
Raises an exception when `savepoint()` calls `flush()` during setup.
|
|
"""
|
|
# ensure we catch the error with the "base" method to avoid any interference
|
|
with mock.patch.object(BaseCursor, 'flush', side_effect=CustomError), \
|
|
TestCase.assertRaises(self, CustomError):
|
|
with self.assertRaises(CustomError):
|
|
raise NotImplementedError
|
|
|
|
@mute_logger('odoo.sql_db')
|
|
def test_assertRaises_clear_recovery(self):
|
|
"""Checks that the savepoint is correctly rolled back if an error occurs
|
|
during the assertRaises setup
|
|
|
|
Raises an exception during the first `clear()` calls which immediately
|
|
follows the initialisation of the savepoint iff we're expecting an
|
|
AccessError.
|
|
"""
|
|
# on the first `clear` call, break the current transaction with nonsense
|
|
# (on further calls do nothing as savepoint() needs to clear() for its
|
|
# own recovery)
|
|
def clear(call_count=itertools.count()):
|
|
if next(call_count) == 0:
|
|
self.env.cr.execute('select nonsense')
|
|
|
|
with mock.patch.object(BaseCursor, 'clear', side_effect=clear),\
|
|
TestCase.assertRaises(self, psycopg2.Error):
|
|
with self.assertRaises(AccessError):
|
|
raise NotImplementedError
|
|
|
|
# check that the transaction has been rolled back and we can perform
|
|
# queries again
|
|
self.env.cr.execute('select 1')
|