From 0fa99e31273bd695a9410170f06da61be3a3d9ea Mon Sep 17 00:00:00 2001
From: "Adrien Widart (awt)" <awt@odoo.com>
Date: Tue, 22 Nov 2022 12:57:11 +0000
Subject: [PATCH] [FIX] core: clean env between tests

This commit description is in three parts: a generic explanation, a real
use case and the solutions

**Explanations**
1. When getting an odoo `Environment`, we first generate a tuple used as
an environment identifier:
   https://github.com/odoo/odoo/blob/7475bcbef601b69e11c88a2ebb5fa39d7fcb52ab/odoo/api.py#L446
   Then, there are two possibilities:
   1. If such environment already exists in the environments list of the
transaction, we reuse it:
      https://github.com/odoo/odoo/blob/7475bcbef601b69e11c88a2ebb5fa39d7fcb52ab/odoo/api.py#L448-L452
   2. Else, we create a new one and store it in the transaction:
      https://github.com/odoo/odoo/blob/7475bcbef601b69e11c88a2ebb5fa39d7fcb52ab/odoo/api.py#L454-L464
2. The `company` attribute of an `Environment` object is a lazy property
   https://github.com/odoo/odoo/blob/7475bcbef601b69e11c88a2ebb5fa39d7fcb52ab/odoo/api.py#L537-L538
   It means that its value won't be recomputed unless we delete it
   https://github.com/odoo/odoo/blob/083c70bbb63f27839b2c9a4b549e216947bc4dd1/odoo/tools/func.py#L12-L17
3. When running a `SavepointCase` test class, the `setUp` method ensures
that, after each test, we do cleaning:
   https://github.com/odoo/odoo/blob/a50a65be19b682201fc010d33c1b1f0b90cdf4d3/odoo/tests/common.py#L652-L662
   The functions are added in a stack. So, in the execution order, we
will
   1. Clear the current environment and the registry
   2. Reset the environments list with the ones that were existing
before the executed test
4. Suppose a test class TC that inherits `SavepointCase`. TC has a
special `setUpClass`:
   1. We create a new user U and replace the environment with a new one,
E1, based on U. This user has a company Comp_U. Because of [1], the new
environment E1 is added to the environments list of the transaction.
   2. For some reasons, we need to do some operations in `sudo` mode:
again, because of [1], a new environment E2 (same as E1 but with the
`su` flag to `True`) is created and added to the environments list of
the transaction
5. TC contains a first test T1. In that test, we create a company
Comp_tmp and set it as default one to U. Therefore, E1 and E2 are
updated (their `company` attribute is now Comp_tmp)
6. At the end of T1, because of [3]:
   1. E1 is reset (but its lazy properties are not deleted)
   2. The environments list of the transaction is reset and still
contains E1 and E2 as they have been created in the class setup (i.e.
before the test setup)
7. In a second test T2, there will be an inconsistency: both E1 and E2
have an incorrect value for their field `company`: Comp_tmp, which is
not the value defined on `self.env.user.company_id` (the value Comp_tmp
does not even exist anymore)

**Real use case**
- The class `AccountTestInvoicingCommon` is an inheritance of
`SavepointCase`. In its class setup, we create a user and set it on the
current environment. Later on, we create a company (the sudo mode will
be activated during the company creation process)
  https://github.com/odoo/odoo/blob/1e69c4fe5f8dd8a92d92f350b6d6a9539157f206/addons/account/tests/common.py#L38-L52
- The class `TestPurchaseOrder` inherits `AccountTestInvoicingCommon`
- The test `:TestPurchaseOrder.test_06_on_time_rate` creates a company
and sets it as the one of the current user
  https://github.com/odoo/odoo/blob/87ffe5983be7f0f4a926b458c4bd1f13001048ec/addons/purchase_stock/tests/test_purchase_order.py#L313-L316
- Therefore, all next tests will have an issue with the environment
(unless we force the writing of the company on the current user to
bypass the issue)
```py
self.assertEqual(self.env.user.company_id, self.env.company) # will fail
self.assertTrue(self.env.company.exists()) # will fail
```

**Solution**
The above issue has been fixed from Odoo 15 on, thanks to commit C1.
Thanks to that diff, at step [3.1] in the above explanations, the lazy
properties of E1 are deleted (so, because of [2], the `company`
attribute of E1 will be correct again). However, once C1 is applied, we
can still add some lines in T2 to fail the test:
```py
sudo_env = self.env.user.sudo().env
self.assertEqual(self.env.user.company_id, sudo_env.company) # will fail
self.assertTrue(sudo_env.company.exists()) # will fail
```
The reason: at the end of the test T1, we reset the environments list of
the transaction ([6.2]). However, E2 has been created before the test
setup (it was in the class setup, see [4.2]). So, after the list reset,
E2 is still in that list and still has the value Comp_tmp for its
`company` attribute.

So... first we can conclude that C1 is not enough. Second, once the
environments list of the transaction is reset ([3.2]), we also have to
reset each environment to ensure that their lazy properties are correct.
We can then remove [3.1] since it will be included in that new step.

C1 a40511cd29d0ded5f104edf3ad3f41dbf6ea2a29

closes odoo/odoo#106252

Signed-off-by: Raphael Collet <rco@odoo.com>
---
 .../test_testing_utilities/tests/__init__.py  |  1 +
 .../test_testing_utilities/tests/test_env.py  | 44 +++++++++++++++++++
 odoo/api.py                                   |  1 +
 odoo/tests/common.py                          |  4 +-
 4 files changed, 49 insertions(+), 1 deletion(-)
 create mode 100644 odoo/addons/test_testing_utilities/tests/test_env.py

diff --git a/odoo/addons/test_testing_utilities/tests/__init__.py b/odoo/addons/test_testing_utilities/tests/__init__.py
index 78398a30bce9..2ae17dd13856 100644
--- a/odoo/addons/test_testing_utilities/tests/__init__.py
+++ b/odoo/addons/test_testing_utilities/tests/__init__.py
@@ -3,3 +3,4 @@ from . import test_methods
 from . import test_lxml
 from . import test_form_impl
 from . import test_xml_tools
+from . import test_env
diff --git a/odoo/addons/test_testing_utilities/tests/test_env.py b/odoo/addons/test_testing_utilities/tests/test_env.py
new file mode 100644
index 000000000000..77b6353f9310
--- /dev/null
+++ b/odoo/addons/test_testing_utilities/tests/test_env.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests.common import SavepointCase
+
+
+class TestEnv(SavepointCase):
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestEnv, cls).setUpClass()
+        user = cls.env['res.users'].create({
+            'name': 'superuser',
+            'login': 'superuser',
+            'password': 'superuser',
+            'groups_id': [(6, 0, cls.env.user.groups_id.ids)],
+        })
+        cls.env = cls.env(user=user)
+
+        # make sure there is at least another environment in the current transaction
+        cls.sudo_env = cls.env(su=True)
+
+    def test_env_company_part_01(self):
+        """
+        The main goal of the test is actually to check the values of the
+        environment after this test execution (see test_env_company_part_02)
+        """
+        company = self.env['res.company'].create({
+            "name": "Test Company",
+        })
+        self.env.user.write({
+            'company_id': company.id,
+            'company_ids': [(4, company.id), (4, self.env.company.id)],
+        })
+        self.assertEqual(self.env.company, self.env.user.company_id)
+        self.assertTrue(self.env.company.exists())
+        self.assertEqual(self.sudo_env.company, self.env.user.company_id)
+        self.assertTrue(self.sudo_env.company.exists())
+
+    def test_env_company_part_02(self):
+        self.assertEqual(self.env.company, self.env.user.company_id)
+        self.assertTrue(self.env.company.exists())
+        self.assertEqual(self.sudo_env.company, self.env.user.company_id)
+        self.assertTrue(self.sudo_env.company.exists())
diff --git a/odoo/api.py b/odoo/api.py
index 606a06e71374..c95d3daa3e0c 100644
--- a/odoo/api.py
+++ b/odoo/api.py
@@ -616,6 +616,7 @@ class Environment(Mapping):
         """ Clear all record caches, and discard all fields to recompute.
             This may be useful when recovering from a failed ORM operation.
         """
+        lazy_property.reset_all(self)
         self.cache.invalidate()
         self.all.tocompute.clear()
         self.all.towrite.clear()
diff --git a/odoo/tests/common.py b/odoo/tests/common.py
index 0778c059305d..b39a15617188 100644
--- a/odoo/tests/common.py
+++ b/odoo/tests/common.py
@@ -655,11 +655,13 @@ class SavepointCase(SingleTransactionCase):
         # restore environments after the test to avoid invoking flush() with an
         # invalid environment (inexistent user id) from another test
         envs = self.env.all.envs
+        for env in list(envs):
+            self.addCleanup(env.clear)
+        # restore the set of known environments as it was at setUp
         self.addCleanup(envs.update, list(envs))
         self.addCleanup(envs.clear)
 
         self.addCleanup(self.registry.clear_caches)
-        self.addCleanup(self.env.clear)
 
         self._savepoint_id = next(savepoint_seq)
         self.cr.execute('SAVEPOINT test_%d' % self._savepoint_id)
-- 
GitLab