From 0eb03981a04cd49f69dc66cb7ccc18b8d1f2d3af Mon Sep 17 00:00:00 2001 From: Pierre Verkest Date: Thu, 10 Jul 2025 13:43:20 +0200 Subject: [PATCH 1/4] [FIX] test_import_error: really test while raising import error --- pytest_odoo.py | 19 ++++++++++++++++++- tests/test_pytest_odoo.py | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pytest_odoo.py b/pytest_odoo.py index 57d41ed..7cd9944 100644 --- a/pytest_odoo.py +++ b/pytest_odoo.py @@ -10,7 +10,7 @@ import subprocess import threading from contextlib import contextmanager -from unittest import mock +from unittest import mock, TestCase from pathlib import Path from typing import Optional @@ -89,6 +89,7 @@ def pytest_cmdline_main(config): "please provide a database name in the Odoo configuration file" ) disable_odoo_test_retry() + support_subtest() monkey_patch_resolve_pkg_root_and_module_name() odoo.service.server.start(preload=[], stop=True) # odoo.service.server.start() modifies the SIGINT signal by its own @@ -215,6 +216,22 @@ def disable_odoo_test_retry(): # Odoo <= 15.0 pass +def support_subtest(): + """Odoo from version 16.0 overwrite TestCase.SubTest context manager + + This overwrite assume the usage of OdooTestResult which we are not + using with pytest-odoo. So this restaure unitest SubTest Context manager + """ + try: + from odoo.tests import BaseCase + BaseCase.subTest = TestCase.subTest + + from odoo.tests.case import _Outcome + _Outcome.result_supports_subtests = False + except (ImportError, AttributeError): + # Odoo <= 15.0 + pass + def _find_manifest_path(collection_path: Path) -> Path: """Try to locate an Odoo manifest file in the collection path.""" # check if collection_path is an addon directory diff --git a/tests/test_pytest_odoo.py b/tests/test_pytest_odoo.py index bf675c8..9428a11 100644 --- a/tests/test_pytest_odoo.py +++ b/tests/test_pytest_odoo.py @@ -127,5 +127,6 @@ def restore_basecase(): self.addCleanup(restore_basecase) + del tests.BaseCase disable_odoo_test_retry() \ No newline at end of file From ffdedcc66bfec72508b7f0f40d368373ab70c31a Mon Sep 17 00:00:00 2001 From: Pierre Verkest Date: Thu, 10 Jul 2025 14:03:51 +0200 Subject: [PATCH 2/4] =?UTF-8?q?[FIX]=C2=A0support=20subTest=20context=20ma?= =?UTF-8?q?nager=20for=20Odoo=20version=2016.0=20and=20greater?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pytest_odoo.py | 15 ++++--- tests/mock/odoo/odoo/tests/__init__.py | 4 +- tests/mock/odoo/odoo/tests/case.py | 12 +++++ tests/test_pytest_odoo.py | 62 +++++++++++++++++++++----- 4 files changed, 74 insertions(+), 19 deletions(-) create mode 100644 tests/mock/odoo/odoo/tests/case.py diff --git a/pytest_odoo.py b/pytest_odoo.py index 7cd9944..d97fc73 100644 --- a/pytest_odoo.py +++ b/pytest_odoo.py @@ -10,7 +10,7 @@ import subprocess import threading from contextlib import contextmanager -from unittest import mock, TestCase +from unittest import mock, TestCase as UnitTestTestCase from pathlib import Path from typing import Optional @@ -217,18 +217,19 @@ def disable_odoo_test_retry(): pass def support_subtest(): - """Odoo from version 16.0 overwrite TestCase.SubTest context manager + """Odoo from version 16.0 re-define its own TestCase.subTest context manager - This overwrite assume the usage of OdooTestResult which we are not - using with pytest-odoo. So this restaure unitest SubTest Context manager + Odoo assume the usage of OdooTestResult which is not our case + using with pytest-odoo. So this fallback to the unitest.TestCase.subTest + Context manager """ try: - from odoo.tests import BaseCase - BaseCase.subTest = TestCase.subTest + from odoo.tests.case import TestCase + TestCase.subTest = UnitTestTestCase.subTest from odoo.tests.case import _Outcome _Outcome.result_supports_subtests = False - except (ImportError, AttributeError): + except ImportError: # Odoo <= 15.0 pass diff --git a/tests/mock/odoo/odoo/tests/__init__.py b/tests/mock/odoo/odoo/tests/__init__.py index cda5288..96654de 100644 --- a/tests/mock/odoo/odoo/tests/__init__.py +++ b/tests/mock/odoo/odoo/tests/__init__.py @@ -1,8 +1,8 @@ from unittest.mock import MagicMock common = MagicMock() +from . import case - -class BaseCase: +class BaseCase(case.TestCase): def run(*args, **kwargs): super().run(*args, **kwargs) diff --git a/tests/mock/odoo/odoo/tests/case.py b/tests/mock/odoo/odoo/tests/case.py new file mode 100644 index 0000000..a4897dc --- /dev/null +++ b/tests/mock/odoo/odoo/tests/case.py @@ -0,0 +1,12 @@ +import contextlib + + +class TestCase: + + @contextlib.contextmanager + def subTest(self, **kwargs): + """Simulate odoo TestCase.subTest from version 15.0""" + + +class _Outcome: + pass \ No newline at end of file diff --git a/tests/test_pytest_odoo.py b/tests/test_pytest_odoo.py index 9428a11..96efe43 100644 --- a/tests/test_pytest_odoo.py +++ b/tests/test_pytest_odoo.py @@ -6,8 +6,9 @@ from _pytest import pathlib as pytest_pathlib from pytest_odoo import ( _find_manifest_path, - monkey_patch_resolve_pkg_root_and_module_name, disable_odoo_test_retry, + monkey_patch_resolve_pkg_root_and_module_name, + support_subtest, ) @@ -93,7 +94,7 @@ def test_disable_odoo_test_retry(self): def restore_basecase_run(): BaseCase.run = original_basecase_run - + self.addCleanup(restore_basecase_run) disable_odoo_test_retry() @@ -107,26 +108,67 @@ def test_disable_odoo_test_retry_ignore_run_doesnt_exists(self): def restore_basecase_run(): BaseCase.run = original_basecase_run - + self.addCleanup(restore_basecase_run) - + del BaseCase.run - + disable_odoo_test_retry() self.assertFalse(hasattr(BaseCase, "run")) def test_import_error(self): - from odoo import tests - + from odoo import tests + original_BaseCase = tests.BaseCase def restore_basecase(): tests.BaseCase = original_BaseCase - + self.addCleanup(restore_basecase) - + del tests.BaseCase disable_odoo_test_retry() - \ No newline at end of file + + def test_support_subtest(self): + from odoo.tests import case + + original_test_case = case.TestCase + original_outcome = case._Outcome + + def restore(): + case.TestCase = original_test_case + case._Outcome = original_outcome + + self.addCleanup(restore) + support_subtest() + from odoo.tests import BaseCase + + self.assertTrue(BaseCase.subTest is TestCase.subTest) + + def test_support_subtest_no_base_case(self): + from odoo import tests + + original_BaseCase = tests.BaseCase + + def restore_basecase(): + tests.BaseCase = original_BaseCase + + self.addCleanup(restore_basecase) + + del tests.BaseCase + support_subtest() + + def test_support_subtest_import_error(self): + from odoo.tests import case + + original_odoo_test_case = case.TestCase + + def restore_testcase(): + case.TestCase = original_odoo_test_case + + self.addCleanup(restore_testcase) + + del case.TestCase + support_subtest() From 0140c26b38ff7fa2806d8d906dfe97b10a1fa397 Mon Sep 17 00:00:00 2001 From: Pierre Verkest Date: Thu, 10 Jul 2025 17:48:42 +0200 Subject: [PATCH 3/4] [FIX] Make pytest-odoo compatible with pytest-subtests and subTest context manager --- README.rst | 2 ++ pytest_odoo.py | 40 +++++++++++++------------- tests/mock/odoo/odoo/tests/__init__.py | 9 ++++-- tests/mock/odoo/odoo/tests/case.py | 8 ++++-- tests/test_pytest_odoo.py | 29 +++++++------------ 5 files changed, 45 insertions(+), 43 deletions(-) diff --git a/README.rst b/README.rst index 77ef676..03254cf 100644 --- a/README.rst +++ b/README.rst @@ -53,3 +53,5 @@ You can use the ``ODOO_RC`` environment variable using an odoo configuration fil The plugin is also compatible with distributed run provided by the `pytest-xdist `_ library. When tests are distributed, a copy of the database is created for each worker at the start of the test session. This is useful to avoid concurrent access to the same database, which can lead to deadlocks. The provided database is therefore used only as template. At the end of the tests, all the created databases are dropped. + +The plugin is also compatible with `pytest-subtests `_ library. When test use the `subTest` context manager you'll get a nice output for each sub-tests failing. diff --git a/pytest_odoo.py b/pytest_odoo.py index d97fc73..f92d223 100644 --- a/pytest_odoo.py +++ b/pytest_odoo.py @@ -10,9 +10,10 @@ import subprocess import threading from contextlib import contextmanager -from unittest import mock, TestCase as UnitTestTestCase from pathlib import Path from typing import Optional +from unittest import TestCase as UnitTestTestCase +from unittest import mock import _pytest import _pytest.python @@ -88,8 +89,8 @@ def pytest_cmdline_main(config): raise Exception( "please provide a database name in the Odoo configuration file" ) - disable_odoo_test_retry() support_subtest() + disable_odoo_test_retry() monkey_patch_resolve_pkg_root_and_module_name() odoo.service.server.start(preload=[], stop=True) # odoo.service.server.start() modifies the SIGINT signal by its own @@ -106,7 +107,6 @@ def pytest_cmdline_main(config): else: yield - @pytest.fixture(scope="module", autouse=True) def load_http(request): if request.config.getoption("--odoo-http"): @@ -153,7 +153,7 @@ def _worker_db_name(): odoo.tools.config["db_name"] = original_db_name odoo.tools.config["dbfilter"] = f"^{original_db_name}$" - + @pytest.fixture(scope='session', autouse=True) def load_registry(): # Initialize the registry before running tests. @@ -203,19 +203,6 @@ def resolve_pkg_root_and_module_name( _pytest.pathlib.resolve_pkg_root_and_module_name= resolve_pkg_root_and_module_name -def disable_odoo_test_retry(): - """Odoo BaseCase.run method overload TestCase.run and manage - a retry mechanism that breaks using pytest launcher. - Using `pytest-rerunfailures` we can use `--reruns` parameters - if needs equivalent feature, so we remove such overload here. - """ - try: - from odoo.tests import BaseCase - del BaseCase.run - except (ImportError, AttributeError): - # Odoo <= 15.0 - pass - def support_subtest(): """Odoo from version 16.0 re-define its own TestCase.subTest context manager @@ -226,13 +213,26 @@ def support_subtest(): try: from odoo.tests.case import TestCase TestCase.subTest = UnitTestTestCase.subTest - - from odoo.tests.case import _Outcome - _Outcome.result_supports_subtests = False + TestCase.run = UnitTestTestCase.run except ImportError: # Odoo <= 15.0 pass + +def disable_odoo_test_retry(): + """Odoo BaseCase.run method overload TestCase.run and manage + a retry mechanism that breaks using pytest launcher. + Using `pytest-rerunfailures` we can use `--reruns` parameters + if needs equivalent feature, so we remove such overload here. + """ + try: + from odoo.tests import BaseCase + del BaseCase.run + except (ImportError, AttributeError): + # Odoo <= 15.0 + pass + + def _find_manifest_path(collection_path: Path) -> Path: """Try to locate an Odoo manifest file in the collection path.""" # check if collection_path is an addon directory diff --git a/tests/mock/odoo/odoo/tests/__init__.py b/tests/mock/odoo/odoo/tests/__init__.py index 96654de..c2980c4 100644 --- a/tests/mock/odoo/odoo/tests/__init__.py +++ b/tests/mock/odoo/odoo/tests/__init__.py @@ -1,8 +1,13 @@ +from . import case + from unittest.mock import MagicMock common = MagicMock() -from . import case class BaseCase(case.TestCase): - def run(*args, **kwargs): + def run(self, *args, **kwargs): super().run(*args, **kwargs) + self._call_something() + + def _call_something(self): + pass diff --git a/tests/mock/odoo/odoo/tests/case.py b/tests/mock/odoo/odoo/tests/case.py index a4897dc..4d45f38 100644 --- a/tests/mock/odoo/odoo/tests/case.py +++ b/tests/mock/odoo/odoo/tests/case.py @@ -2,11 +2,13 @@ class TestCase: - + @contextlib.contextmanager def subTest(self, **kwargs): """Simulate odoo TestCase.subTest from version 15.0""" + def run(self, *args, **kwargs): + self._call_a_method() -class _Outcome: - pass \ No newline at end of file + def _call_a_method(self): + pass diff --git a/tests/test_pytest_odoo.py b/tests/test_pytest_odoo.py index 96efe43..7f9e622 100644 --- a/tests/test_pytest_odoo.py +++ b/tests/test_pytest_odoo.py @@ -2,6 +2,7 @@ from contextlib import contextmanager from pathlib import Path from unittest import TestCase +from unittest.mock import patch from _pytest import pathlib as pytest_pathlib from pytest_odoo import ( @@ -98,7 +99,9 @@ def restore_basecase_run(): self.addCleanup(restore_basecase_run) disable_odoo_test_retry() - self.assertFalse(hasattr(BaseCase, "run")) + with patch("odoo.tests.BaseCase._call_something") as mock: + BaseCase().run() + mock.assert_not_called() def test_disable_odoo_test_retry_ignore_run_doesnt_exists(self): @@ -114,9 +117,10 @@ def restore_basecase_run(): del BaseCase.run disable_odoo_test_retry() - self.assertFalse(hasattr(BaseCase, "run")) - + with patch("odoo.tests.BaseCase._call_something") as mock: + BaseCase().run() + mock.assert_not_called() def test_import_error(self): from odoo import tests @@ -135,30 +139,19 @@ def test_support_subtest(self): from odoo.tests import case original_test_case = case.TestCase - original_outcome = case._Outcome def restore(): case.TestCase = original_test_case - case._Outcome = original_outcome self.addCleanup(restore) support_subtest() + from odoo.tests import BaseCase + from odoo.tests.case import TestCase as OdooTestCase + self.assertTrue(OdooTestCase.subTest is TestCase.subTest) self.assertTrue(BaseCase.subTest is TestCase.subTest) - - def test_support_subtest_no_base_case(self): - from odoo import tests - - original_BaseCase = tests.BaseCase - - def restore_basecase(): - tests.BaseCase = original_BaseCase - - self.addCleanup(restore_basecase) - - del tests.BaseCase - support_subtest() + self.assertTrue(OdooTestCase.run is TestCase.run) def test_support_subtest_import_error(self): from odoo.tests import case From ab8c896332dfbba25e6eaa222dfbabf67c73c714 Mon Sep 17 00:00:00 2001 From: Pierre Verkest Date: Thu, 10 Jul 2025 17:50:21 +0200 Subject: [PATCH 4/4] CI: support python 3.13 --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f447421..ae94cc2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,8 @@ jobs: os: ubuntu-latest - PYTHON_VERSION: "3.12" os: ubuntu-latest + - PYTHON_VERSION: "3.13" + os: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python