1+ r"""
2+ There are two main classes used testing during the upgrade.
3+
4+ * :class:`~odoo.upgrade.testing.UpgradeCase` for testing upgrade scripts,
5+ * :class:`~odoo.upgrade.testing.UpgradeCase` for testing invariants across versions.
6+
7+ Subclasses must implement:
8+
9+ * For ``UpgradeCase``:
10+
11+ - ``prepare`` method: prepare data before upgrade,
12+ - ``check`` method: check data was correctly upgraded.
13+
14+ * For ``IntegrityCase``:
15+
16+ - ``invariant`` method: compute an invariant to check.
17+
18+ Put your test classes in a ``tests`` Python module (folder) in any of the folders
19+ containing the upgrade scripts of your modules. The script containing your tests should
20+ have a `test_` prefix. The ``tests`` module must contain an ``__init__.py`` file to be
21+ detected by Odoo.
22+
23+ Example directory structure::
24+
25+ myupgrades/
26+ └── mymodule1/
27+ ├── 18.0.1.1.2/
28+ │ └── pre-myupgrade.py
29+ └── tests/
30+ ├── __init__.py
31+ └── test_myupgrade.py
32+
33+ .. note::
34+
35+ The tests in the example above will be loaded only if ``mymodule1`` is being
36+ **upgraded.**
37+
38+ Running Upgrade Tests
39+ ---------------------
40+
41+ After receiving an upgraded database with all standard Odoo modules already upgraded to
42+ their target version, you can test the upgrade of custom modules by following a three-step
43+ process:
44+
45+ 1. Prepare the test data
46+
47+ .. code-block:: bash
48+
49+ $ ~/odoo/$version/odoo-bin -d DB --test-tags=$prepare_test_tag \
50+ --upgrade-path=~/upgrade-util/src,~/myupgrades \
51+ --addons=~/odoo/$version/addons,~/enterprise/$version --stop
52+
53+ 2. Upgrade the modules
54+
55+ .. code-block:: bash
56+
57+ $ ~/odoo/$version/odoo-bin -d DB -u mymodule1,mymodule2 \
58+ --upgrade-path=~/upgrade-util/src,~/myupgrades \
59+ --addons=~/odoo/$version/addons,~/enterprise/$version --stop
60+
61+ 3. Check the upgraded data
62+
63+ .. code-block:: bash
64+
65+ $ ~/odoo/$version/odoo-bin -d DB --test-tags=$check_test_tag \
66+ --upgrade-path=~/upgrade-util/src,~/myupgrades \
67+ --addons=~/odoo/$version/addons,~/enterprise/$version --stop
68+
69+ The example above assumes that ``$version`` is the target version of your upgrade (e.g.
70+ ``18.0``), ``DB`` is the name of your database, and ``mymodule1,mymodule2`` are the
71+ modules you want to upgrade. The directory structure assumes that ``~/odoo/$version`` and
72+ ``~/enterprise/$version`` contain the Community and Enterprise source code for the target
73+ Odoo version, respectively. The ``~/myupgrades`` directory contains your custom upgrade
74+ scripts, and ``~/upgrade-util/src`` contains the `upgrade utils
75+ <https://github.com/odoo/upgrade-util/>`_ repo.
76+
77+ The variables ``$prepare_test_tag`` and ``$check_test_tag`` must be set according to:
78+
79+ .. list-table::
80+ :header-rows: 1
81+ :stub-columns: 1
82+
83+ * - Variable
84+ - ``UpgradeCase``
85+ - ``IntegrityCase``
86+ * - ``$prepare_test_tag``
87+ - ``upgrade.test_prepare``
88+ - ``integrity_case.test_prepare``
89+ * - ``$check_test_tag``
90+ - ``upgrade.test_check``
91+ - ``integrity_test.test_check``
92+
93+ .. note::
94+
95+ `upgrade.test_prepare` also runs ``IntegrityCase`` tests, so you can prepare data
96+ for both ``UpgradeCase`` and ``IntegrityCase`` tests with only this tag.
97+
98+ .. warning::
99+
100+ Do **not** run any ``prepare`` method of an ``UpgradeCase`` before sending your
101+ database for a **production** upgrade to `upgrade.odoo.com
102+ <https://upgrade.odoo.com>`_. Doing so may risk your upgrade being blocked and marked
103+ as failed.
104+
105+ API documentation
106+ -----------------
107+ """
108+
1109import functools
2110import inspect
3111import logging
@@ -40,22 +148,17 @@ def parametrize(argvalues):
40148 """
41149 Parametrize a test function.
42150
43- Decorator for UnitTestCase test functions to parametrize the decorated test.
44-
45- Usage:
46- ```python
47- @parametrize([
48- (1, 2),
49- (2, 4),
50- (-1, -2),
51- (0, 0),
52- ])
53- def test_double(self, input, expected):
54- self.assertEqual(input * 2, expected)
55- ```
56-
57- It works by injecting test functions in the containing class.
58- Idea taken from the `parameterized` package (https://pypi.org/project/parameterized/).
151+ Decorator for upgrade test functions to parametrize and generate multiple tests from
152+ it.
153+
154+ Usage::
155+
156+ @parametrize([(1, 2), (2, 4), (-1, -2), (0, 0)])
157+ def test_double(self, input, expected):
158+ self.assertEqual(input * 2, expected)
159+
160+ Works by injecting test functions in the containing class.
161+ Inspired by the `parameterized <https://pypi.org/project/parameterized/>`_ package.
59162 """
60163
61164 def make_func (func , name , args ):
@@ -116,6 +219,8 @@ def __init_subclass__(cls):
116219
117220
118221class UnitTestCase (TransactionCase , _create_meta (10 , "upgrade_unit" )):
222+ """:meta private: exclude from online docs."""
223+
119224 @classmethod
120225 def setUpClass (cls ):
121226 super ().setUpClass ()
@@ -211,6 +316,8 @@ def assertUpdated(self, table, ids=None, msg=None):
211316
212317
213318class UpgradeCommon (BaseCase ):
319+ """:meta private: exclude from online docs."""
320+
214321 __initialized = False
215322
216323 change_version = (None , None )
@@ -332,6 +439,25 @@ def convert_check(self, value):
332439
333440
334441def change_version (version_str ):
442+ """
443+ Class decorator to specify the version on which a test is relevant.
444+
445+ Using ``@change_version(version)`` indicates:
446+
447+ * ``test_prepare`` will only run if the current Odoo version is in the range
448+ ``[next_major_version-1, version)``.
449+ * ``test_check`` will only run if the current Odoo version is in the range ``[version,
450+ next_major_version)``.
451+
452+ ``next_major_version`` is the next major version after ``version``, e.g. for
453+ ``saas~17.2`` it is ``18.0``.
454+
455+ .. note::
456+
457+ Do not use this decorator if your upgrade is in the same major version. Otherwise,
458+ your tests will not run.
459+ """
460+
335461 def version_decorator (obj ):
336462 match = VERSION_RE .match (version_str )
337463 if not match :
@@ -370,24 +496,42 @@ def get_previous_major(major, minor):
370496# pylint: disable=inherit-non-class
371497class UpgradeCase (UpgradeCommon , _create_meta (10 , "upgrade_case" )):
372498 """
373- Test case to modify data in origin version, and assert in target version .
499+ Test case to verify that the upgrade scripts correctly upgrade data .
374500
375- User must define a "prepare" and a "check" method.
376- - prepare method can write in database, return value will be stored in a dedicated table and
377- passed as argument to check.
378- - check method can assert that the received argument is the one expected,
379- executing any code to retrieve equivalent information in migrated database.
380- Note: check argument is a loaded json dump, meaning that tuple are converted to list.
381- convert_check can be used to normalise the right part of the comparison.
501+ Override:
382502
383- check method is only called if corresponding prepared was run in previous version
503+ * ``prepare`` to set up data,
504+ * ``check`` to assert expectations after the upgrade.
384505
385- prepare and check implementation may contains version conditional code to match api changes.
506+ The ORM can be used in these methods to perform the functional flow under test. The
507+ return value of ``prepare`` is persisted and passed as an argument to ``check``. It
508+ must be JSON-serializable.
386509
387- using @change_version class decorator can indicate with script version is tested here if any:
388- Example: to test a saas~12.3 script, using @change_version('saas-12,3') will only run prepare if
389- version in [12.0, 12.3[ and run check if version is in [12.3, 13]
510+ .. note::
390511
512+ Since ``prepare`` injects or modifies data, this type of test is intended **only
513+ for development**. Use it to test upgrade scripts while developing them. **Do not**
514+ run these tests for a production upgrade. To verify that upgrades preserved
515+ important invariants in production, use ``IntegrityCase`` tests instead.
516+
517+ .. example::
518+
519+ .. code-block:: python
520+
521+ from odoo.upgrade.testing import UpgradeCase, change_version
522+
523+
524+ class DeactivateBobUsers(UpgradeCase):
525+
526+ def prepare(self):
527+ u = self.env["res.users"].create({"login": "bob", "name": "Bob"})
528+ return u.id # will be passed to check
529+
530+ def check(self, uid): # uid is the value returned by prepare
531+ self.env.cr.execute(
532+ "SELECT * FROM res_users WHERE id=%s AND NOT active", [uid]
533+ )
534+ self.assertEqual(self.env.cr.rowcount, 1)
391535 """
392536
393537 def __init_subclass__ (cls , abstract = False ):
@@ -403,12 +547,27 @@ def test_prepare(self):
403547# pylint: disable=inherit-non-class
404548class IntegrityCase (UpgradeCommon , _create_meta (20 , "integrity_case" )):
405549 """
406- Test case to check invariant through any version.
550+ Test case for validating invariants across upgrades.
551+
552+ Override:
553+
554+ * ``invariant`` to return a JSON-serializable value representing
555+ the invariant to check.
556+
557+ The ``invariant`` method is called both before and after the upgrade,
558+ and the results are compared.
559+
560+
561+ .. example::
562+
563+ .. code-block:: python
564+
565+ from odoo.upgrade.testing import IntegrityCase
407566
408- User must define a "invariant" method.
409- invariant return value will be compared between the two version.
410567
411- invariant implementation may contains version conditional code to match api changes.
568+ class NoNewUsers(IntegrityCase):
569+ def invariant(self):
570+ return self.env["res.users"].search_count([])
412571 """
413572
414573 message = "Invariant check fail"
@@ -418,7 +577,7 @@ def __init_subclass__(cls, abstract=False):
418577 if not abstract and not hasattr (cls , "invariant" ):
419578 _logger .error ("%s (IntegrityCase) must define an invariant method" , cls .__name__ )
420579
421- # IntegrityCase should not alterate database:
580+ # IntegrityCase should not alter the database:
422581 # TODO give a test cursor, don't commit after prepare, use a protected cursor to set_value
423582
424583 def prepare (self ):
@@ -438,6 +597,7 @@ def _setup_registry(self):
438597 self .addCleanup (self .registry .leave_test_mode )
439598
440599 def setUp (self ):
600+ """:meta private: exclude from online docs."""
441601 super (IntegrityCase , self ).setUp ()
442602
443603 def commit (self ):
0 commit comments