Files
Raphael Collet 0a877ddfc7 [FIX] core: when flush() crashes while exiting a savepoint
When exiting a flushing savepoint, the method flush() is invoked before
closing the savepoint (which releases it):

    def _close(self, rollback):
        if not rollback:
            self._cr.flush()
        super()._close(rollback)

If method flush() crashes at that point, the savepoint is not closed at
all (because of the exception) and is therefore not even rolled back.
The expected behavior is that the savepoint rollbacks and the exception
bubbles up.

closes odoo/odoo#126866

X-original-commit: 5f9b4f5c998388625de27fbd8fe0f7a0524c04fd
Signed-off-by: Rémy Voet (ryv) <ryv@odoo.com>
Signed-off-by: Raphael Collet <rco@odoo.com>
2023-06-30 07:06:44 +02:00

103 lines
4.1 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_at_setup(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
def test_assertRaises_error_at_exit(self):
"""Checks that a "correctly" executing assertRaises (where the expected
exception has been raised and caught) will properly rollback when the
error is raised by flush() while exiting the savepoint.
"""
self.env.cr.execute("SET LOCAL test_testing_utilities.a_flag = ''")
with mock.patch.object(BaseCursor, 'flush', side_effect=[None, CustomError]):
with self.assertRaises(CustomError):
self.env.cr.execute("SET LOCAL test_testing_utilities.a_flag = 'yes'")
self.env.cr.execute("SHOW test_testing_utilities.a_flag")
self.assertEqual(self.env.cr.fetchone(), ('',))
@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')