diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/common.py b/tests/common.py new file mode 100644 index 0000000..b485a62 --- /dev/null +++ b/tests/common.py @@ -0,0 +1,97 @@ +import ast +import contextlib +import os +import shutil +import subprocess +import tempfile +import textwrap +from pathlib import Path + +ODOO_VENV = "/opt/odoo-venv" + +test_addons_dir = Path(__file__).parent / "data" / "addons" + +odoo_version_info = tuple(map(int, os.environ["ODOO_VERSION"].split("."))) + +odoo_bin = "odoo" + + +@contextlib.contextmanager +def preserve_odoo_rc(): + odoo_rc_path = Path(os.environ["ODOO_RC"]) + odoo_rc = odoo_rc_path.read_bytes() + try: + yield + finally: + odoo_rc_path.write_bytes(odoo_rc) + + +@contextlib.contextmanager +def preserve_odoo_venv(): + subprocess.check_call(["cp", "-arl", ODOO_VENV, ODOO_VENV + ".org"]) + try: + yield + finally: + subprocess.check_call(["rm", "-r", ODOO_VENV]) + subprocess.check_call(["mv", ODOO_VENV + ".org", ODOO_VENV]) + + +@contextlib.contextmanager +def make_addons_dir(test_addons): + """Copy test addons to a temporary directory. + + Adjust the addons version to match the Odoo version being tested. + Rename __manifest__.py to __openerp__.py for older Odoo versions. + Add pyproject.toml. + """ + with tempfile.TemporaryDirectory() as tmpdir: + tmppath = Path(tmpdir) + for addon_name in test_addons: + shutil.copytree(test_addons_dir / addon_name, tmppath / addon_name) + # prefix Odoo version + manifest_path = tmppath / addon_name / "__manifest__.py" + manifest = ast.literal_eval(manifest_path.read_text()) + manifest["version"] = os.environ["ODOO_VERSION"] + "." + manifest["version"] + manifest_path.write_text(repr(manifest)) + if odoo_version_info < (10, 0): + manifest_path.rename(manifest_path.parent / "__openerp__.py") + pyproject_toml_path = tmppath / addon_name / "pyproject.toml" + pyproject_toml_path.write_text( + textwrap.dedent( + """\ + [build-system] + requires = ["whool"] + build-backend = "whool.buildapi" + """ + ) + ) + yield tmppath + + +@contextlib.contextmanager +def install_test_addons(test_addons): + with preserve_odoo_rc(), preserve_odoo_venv(), make_addons_dir( + test_addons + ) as addons_dir: + subprocess.check_call(["oca_install_addons"], cwd=addons_dir) + yield addons_dir + + +def dropdb(): + subprocess.check_call(["dropdb", "--if-exists", os.environ["PGDATABASE"]]) + + +def did_run_test_module(output, test_module): + """Check that a test did run by looking in the Odoo log. + + test_module is the full name of the test (addon_name.tests.test_module). + """ + return "odoo.addons." + test_module in output + + +def make_addon_dist_name(addon_name): + odoo_series = int(os.getenv("ODOO_VERSION").partition(".")[0]) + return "odoo{odoo_series}-addon-{name}".format( + name=addon_name, + odoo_series=odoo_series if odoo_series < 15 else "", + ) diff --git a/tests/data/addons/addon_success/__init__.py b/tests/data/addons/addon_success/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/addons/addon_success/__manifest__.py b/tests/data/addons/addon_success/__manifest__.py new file mode 100644 index 0000000..4aa2671 --- /dev/null +++ b/tests/data/addons/addon_success/__manifest__.py @@ -0,0 +1,4 @@ +{ + "name": "addon with successful test", + "version": "1.0.0", +} diff --git a/tests/data/addons/addon_success/tests/__init__.py b/tests/data/addons/addon_success/tests/__init__.py new file mode 100644 index 0000000..6566688 --- /dev/null +++ b/tests/data/addons/addon_success/tests/__init__.py @@ -0,0 +1 @@ +from . import test_success \ No newline at end of file diff --git a/tests/data/addons/addon_success/tests/test_success.py b/tests/data/addons/addon_success/tests/test_success.py new file mode 100644 index 0000000..11c05e8 --- /dev/null +++ b/tests/data/addons/addon_success/tests/test_success.py @@ -0,0 +1,6 @@ +from odoo.tests.common import TransactionCase + + +class Test(TransactionCase): + def test_success(self): + pass diff --git a/tests/data/addons/addon_warning/__init__.py b/tests/data/addons/addon_warning/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/addons/addon_warning/__manifest__.py b/tests/data/addons/addon_warning/__manifest__.py new file mode 100644 index 0000000..e8101e7 --- /dev/null +++ b/tests/data/addons/addon_warning/__manifest__.py @@ -0,0 +1,4 @@ +{ + "name": "addon that generates warnings", + "version": "1.0.0", +} diff --git a/tests/data/addons/addon_warning/tests/__init__.py b/tests/data/addons/addon_warning/tests/__init__.py new file mode 100644 index 0000000..cdfa6eb --- /dev/null +++ b/tests/data/addons/addon_warning/tests/__init__.py @@ -0,0 +1 @@ +from . import test_warning diff --git a/tests/data/addons/addon_warning/tests/test_warning.py b/tests/data/addons/addon_warning/tests/test_warning.py new file mode 100644 index 0000000..9dafb12 --- /dev/null +++ b/tests/data/addons/addon_warning/tests/test_warning.py @@ -0,0 +1,9 @@ +import logging +from odoo.tests.common import TransactionCase + + +_logger = logging.getLogger(__name__) + +class Test(TransactionCase): + def test_log_warning(self): + _logger.warning("This is a warning") diff --git a/tests/data/addons/addon_with_deb_dep/__init__.py b/tests/data/addons/addon_with_deb_dep/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/addons/addon_with_deb_dep/__manifest__.py b/tests/data/addons/addon_with_deb_dep/__manifest__.py new file mode 100644 index 0000000..2b1581c --- /dev/null +++ b/tests/data/addons/addon_with_deb_dep/__manifest__.py @@ -0,0 +1,8 @@ +{ + "name": "addon_with_deb_dep", + "summary": "Depends on 'nano' debian dependency.", + "version": "1.0.0", + "external_dependencies": { + "deb": ["nano"], + } +} diff --git a/tests/data/addons/addon_with_deb_dep2/__init__.py b/tests/data/addons/addon_with_deb_dep2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/addons/addon_with_deb_dep2/__manifest__.py b/tests/data/addons/addon_with_deb_dep2/__manifest__.py new file mode 100644 index 0000000..ecb9c90 --- /dev/null +++ b/tests/data/addons/addon_with_deb_dep2/__manifest__.py @@ -0,0 +1,11 @@ +{ + "name": "addon_with_deb_dep2", + "summary": "Depends on 'curl' debian dependency, and on addon_with_deb_dep.", + "version": "1.0.0", + "depends": [ + "addon_with_deb_dep", + ], + "external_dependencies": { + "deb": ["curl"], + } +} diff --git a/tests/data/addons/uninstallable_addon/__manifest__.py b/tests/data/addons/uninstallable_addon/__manifest__.py new file mode 100644 index 0000000..b718962 --- /dev/null +++ b/tests/data/addons/uninstallable_addon/__manifest__.py @@ -0,0 +1,5 @@ +{ + "name": "uninstallable_addon", + "version": "1.0.0", + "installable": False, +} diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 0000000..4808b47 --- /dev/null +++ b/tests/docker-compose.yml @@ -0,0 +1,20 @@ +services: + test: + image: registry.ethumada.com/odoo/odoo17co:latest + # build: + # context: .. + # args: + # codename: jammy + # python_version: "3.10" + # odoo_version: "16.0" + volumes: + - ..:/mnt/odoo + working_dir: /mnt/odoo/tests + depends_on: + - postgres + postgres: + image: postgres + environment: + POSTGRES_USER: odoo + POSTGRES_PASSWORD: odoo + POSTGRES_DB: odoo diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..b420b44 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,2 @@ +# test requirements +pytest diff --git a/tests/runtests.sh b/tests/runtests.sh new file mode 100644 index 0000000..3fcee4c --- /dev/null +++ b/tests/runtests.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e + +# Run pytest on the tests directory, +# which is assumed to be mounted somewhere in the docker image. + +here=$(dirname $0) + +testvenv=/tmp/testvenv +/usr/bin/python3 -m venv $testvenv +$testvenv/bin/pip install -r $here/requirements.txt + +export PATH=$here/../bin:$PATH + +$testvenv/bin/pytest --color=yes --ignore $here/data $here "$@" diff --git a/tests/test_addons_path.py b/tests/test_addons_path.py new file mode 100644 index 0000000..079044e --- /dev/null +++ b/tests/test_addons_path.py @@ -0,0 +1,19 @@ +import os +import subprocess +from pathlib import Path + +from .common import install_test_addons + + +def test_addons_path(): + """Test must not fail where there are no installable addons.""" + assert ( + Path(os.environ["ODOO_RC"]).read_text() + == "[options]\n" + ) + with install_test_addons(["addon_success"]): + assert ( + Path(os.environ["ODOO_RC"]).read_text() + == "[options]\naddons_path=/opt/odoo/addons,.\n" + ) + subprocess.check_call(["python", "-c", "import odoo.cli"]) diff --git a/tests/test_checklog.py b/tests/test_checklog.py new file mode 100644 index 0000000..263f16f --- /dev/null +++ b/tests/test_checklog.py @@ -0,0 +1,26 @@ +import os +import subprocess +from .common import install_test_addons, dropdb, did_run_test_module + + +def test_checklog_enabled(): + """Test addon_warning with checklog enabled.""" + with install_test_addons(["addon_warning"]) as addons_dir: + dropdb() + subprocess.check_call(["oca_init_test_database"], cwd=addons_dir) + os.environ["OCA_ENABLE_CHECKLOG_ODOO"] = "1" + result = subprocess.run( + ["oca_run_tests"], cwd=addons_dir, text=True, capture_output=True + ) + os.environ["OCA_ENABLE_CHECKLOG_ODOO"] = "" + assert result.returncode == 1 and "Error: Errors detected in log." in result.stderr + +def test_checklog_disabled(): + """Test addon_warning with checklog disabled.""" + with install_test_addons(["addon_warning"]) as addons_dir: + dropdb() + subprocess.check_call(["oca_init_test_database"], cwd=addons_dir) + result = subprocess.check_output( + ["oca_run_tests"], cwd=addons_dir, text=True + ) + assert did_run_test_module(result, "addon_warning.tests.test_warning") diff --git a/tests/test_list_addons_to_test_as_url_reqs.py b/tests/test_list_addons_to_test_as_url_reqs.py new file mode 100644 index 0000000..5e7d4c5 --- /dev/null +++ b/tests/test_list_addons_to_test_as_url_reqs.py @@ -0,0 +1,58 @@ +import subprocess +import textwrap + +from .common import make_addons_dir, make_addon_dist_name + + +def test_oca_list_addons_to_test_as_url_reqs__basic(): + """Basic successful test.""" + with make_addons_dir( + ["addon_success", "addon_with_deb_dep", "uninstallable_addon"] + ) as addons_dir: + result = subprocess.check_output( + ["oca_list_addons_to_test_as_url_reqs"], cwd=addons_dir, text=True + ) + assert result == textwrap.dedent( + f"""\ + {make_addon_dist_name('addon_success')} @ {addons_dir.as_uri()}/addon_success + {make_addon_dist_name('addon_with_deb_dep')} @ {addons_dir.as_uri()}/addon_with_deb_dep + """ + ) + + +def test_oca_list_addons_to_test_as_url_reqs__editable(): + """Basic successful test with editables.""" + with make_addons_dir( + ["addon_success", "addon_with_deb_dep", "uninstallable_addon"] + ) as addons_dir: + result = subprocess.check_output( + ["oca_list_addons_to_test_as_url_reqs", "--editable"], + cwd=addons_dir, + text=True, + ) + assert result == textwrap.dedent( + f"""\ + -e {addons_dir.as_uri()}/addon_success#egg={make_addon_dist_name('addon_success')} + -e {addons_dir.as_uri()}/addon_with_deb_dep#egg={make_addon_dist_name('addon_with_deb_dep')} + """ + ) + + +def test_oca_list_addons_to_test_as_url_reqs__skip_test_requirement(): + """Basic successful test.""" + with make_addons_dir( + ["addon_success", "addon_with_deb_dep", "uninstallable_addon"] + ) as addons_dir: + # add URL reference to addon_success + addons_dir.joinpath("test-requirements.txt").write_text( + f"{make_addon_dist_name('addon_success')} @ git+https://github.com/oca/dummy@refs/pull/123/head" + ) + result = subprocess.check_output( + ["oca_list_addons_to_test_as_url_reqs"], cwd=addons_dir, text=True + ) + # addon_success should not be in result because it is already in test-requirements.txt + assert result == textwrap.dedent( + f"""\ + {make_addon_dist_name('addon_with_deb_dep')} @ {addons_dir.as_uri()}/addon_with_deb_dep + """ + ) diff --git a/tests/test_list_external_dependencies.py b/tests/test_list_external_dependencies.py new file mode 100644 index 0000000..6bfef7a --- /dev/null +++ b/tests/test_list_external_dependencies.py @@ -0,0 +1,24 @@ +from pathlib import Path +import os +import subprocess +from .common import make_addons_dir, preserve_odoo_rc + + +def test_list_external_dependencies(): + with make_addons_dir(["addon_with_deb_dep"]) as addons_dir: + res = subprocess.check_output( + ["oca_list_external_dependencies", "deb"], cwd=addons_dir + ) + assert res == b"nano\n" + + +def test_list_external_dependencies_transitive(): + """Test that transitive external dependencies are returned.""" + with preserve_odoo_rc(), make_addons_dir( + ["addon_with_deb_dep"] + ) as dep_addons_dir, make_addons_dir(["addon_with_deb_dep2"]) as addons_dir: + Path(os.getenv("ODOO_RC")).write_text(f"[options]\naddons_path={dep_addons_dir}\n") + res = subprocess.check_output( + ["oca_list_external_dependencies", "deb"], cwd=addons_dir + ) + assert res == b"curl\nnano\n" diff --git a/tests/test_no_addons.py b/tests/test_no_addons.py new file mode 100644 index 0000000..e7abf99 --- /dev/null +++ b/tests/test_no_addons.py @@ -0,0 +1,9 @@ +import subprocess +from .common import install_test_addons + + +def test_no_addons(): + """Test must not fail where there are no installable addons.""" + with install_test_addons(["uninstallable_addon"]) as addons_dir: + # no need to initialize test database because tests will not be attempted + subprocess.check_call(["oca_run_tests"], cwd=addons_dir) diff --git a/tests/test_oca_git_push_if_remote_did_not_change.py b/tests/test_oca_git_push_if_remote_did_not_change.py new file mode 100644 index 0000000..1566ee8 --- /dev/null +++ b/tests/test_oca_git_push_if_remote_did_not_change.py @@ -0,0 +1,83 @@ +from pathlib import Path +import subprocess + +import pytest + + +@pytest.fixture(scope="module") +def repo_dir(tmp_path_factory) -> str: + repo_path = tmp_path_factory.mktemp("repo") + subprocess.check_call(["git", "init"], cwd=repo_path) + subprocess.check_call( + ["git", "config", "user.email", "test@example.com"], + cwd=repo_path, + ) + subprocess.check_call( + ["git", "config", "user.name", "test"], + cwd=repo_path, + ) + (repo_path / "README").touch() + subprocess.check_call(["git", "add", "."], cwd=repo_path) + subprocess.check_call(["git", "commit", "-m", "initial commit"], cwd=repo_path) + bare_repo_path = tmp_path_factory.mktemp("bare_repo") + subprocess.check_call( + ["git", "clone", "--bare", str(repo_path), str(bare_repo_path)] + ) + return str(bare_repo_path) + + +@pytest.fixture() +def git_clone_path(repo_dir: str, tmp_path) -> Path: + clone_path = tmp_path / "clone" + subprocess.check_call( + ["git", "clone", "--depth=1", "file://" + repo_dir, str(clone_path)] + ) + subprocess.check_call( + ["git", "config", "user.email", "test@example.com"], + cwd=clone_path, + ) + subprocess.check_call( + ["git", "config", "user.name", "test"], + cwd=clone_path, + ) + return clone_path + + +def test_no_change(git_clone_path: Path): + output = subprocess.check_output( + ["oca_git_push_if_remote_did_not_change", "origin"], + cwd=git_clone_path, + text=True, + ) + assert "No local change to push" in output + + +def test_local_change(git_clone_path: Path): + (git_clone_path / "local-change-1").touch() + subprocess.check_call(["git", "add", "."], cwd=git_clone_path) + subprocess.check_call(["git", "commit", "-m", "local-change-1"], cwd=git_clone_path) + output = subprocess.check_output( + ["oca_git_push_if_remote_did_not_change", "origin"], + cwd=git_clone_path, + text=True, + ) + assert "Pushing changes" in output + + +def test_remote_change(git_clone_path: Path): + # push a change and reset + (git_clone_path / "remote-change").touch() + subprocess.check_call(["git", "add", "."], cwd=git_clone_path) + subprocess.check_call(["git", "commit", "-m", "remote-change"], cwd=git_clone_path) + subprocess.check_call(["git", "push"], cwd=git_clone_path) + subprocess.check_call(["git", "reset", "--hard", "HEAD^"], cwd=git_clone_path) + # create a local change + (git_clone_path / "local-change-2").touch() + subprocess.check_call(["git", "add", "."], cwd=git_clone_path) + subprocess.check_call(["git", "commit", "-m", "local-change-2"], cwd=git_clone_path) + output = subprocess.check_output( + ["oca_git_push_if_remote_did_not_change", "origin"], + cwd=git_clone_path, + text=True, + ) + assert "Remote has evolved since we cloned, not pushing" in output diff --git a/tests/test_preinstalled.py b/tests/test_preinstalled.py new file mode 100644 index 0000000..56e6384 --- /dev/null +++ b/tests/test_preinstalled.py @@ -0,0 +1,76 @@ +"""Various test that the Dockerfile did what the README promises.""" + +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +from .common import odoo_bin, make_addons_dir + + +def test_odoo_bin_in_path(): + assert shutil.which(odoo_bin) + + +def test_wkhtomtopdf_in_path(): + assert shutil.which("wkhtmltopdf") + + +def test_python_in_path(): + assert shutil.which("python") + assert Path(shutil.which("python")).parent == Path(shutil.which(odoo_bin)).parent + + +def test_pip_in_path(): + assert shutil.which("pip") + assert Path(shutil.which("pip")).parent == Path(shutil.which(odoo_bin)).parent + + +def test_addons_dir(): + assert os.environ["ADDONS_DIR"] == "." + + +def test_odoo_rc(): + odoo_rc = Path(os.environ["ODOO_RC"]) + assert odoo_rc.exists() + assert odoo_rc.read_text() == "[options]\n" + + +def test_openerp_server_rc(): + assert os.environ["OPENERP_SERVER"] == os.environ["ODOO_RC"] + + +def test_import_odoo(): + subprocess.check_call(["python", "-c", "import odoo; odoo.addons.__path__"]) + subprocess.check_call(["python", "-c", "import odoo.cli"]) + + +def _target_python_version(): + version = subprocess.check_output( + ["python", "-c", "import platform; print(platform.python_version())"], + universal_newlines=True, + ) + major, minor = version.split(".")[:2] + return int(major), int(minor) + + +@pytest.mark.skipif( + _target_python_version() < (3, 7), reason="Whool requires python3.7 or higher" +) +def test_import_odoo_after_addon_install(): + with make_addons_dir(["addon_success"]) as addons_dir: + addon_dir = addons_dir / "addon_success" + subprocess.check_call(["git", "init"], cwd=addon_dir) + subprocess.check_call(["git", "add", "."], cwd=addon_dir) + subprocess.check_call(["git", "config", "user.email", "..."], cwd=addon_dir) + subprocess.check_call( + ["git", "config", "user.name", "me@example.com"], cwd=addon_dir + ) + subprocess.check_call(["git", "commit", "-m", "..."], cwd=addon_dir) + subprocess.check_call( + ["python", "-m", "pip", "install", addons_dir / "addon_success"] + ) + subprocess.check_call(["python", "-c", "import odoo.cli"]) diff --git a/tests/test_success.py b/tests/test_success.py new file mode 100644 index 0000000..d662812 --- /dev/null +++ b/tests/test_success.py @@ -0,0 +1,13 @@ +import subprocess +from .common import install_test_addons, dropdb, did_run_test_module + + +def test_success(): + """Basic successful test.""" + with install_test_addons(["addon_success"]) as addons_dir: + dropdb() + subprocess.check_call(["oca_init_test_database"], cwd=addons_dir) + result = subprocess.check_output( + ["oca_run_tests"], cwd=addons_dir, text=True + ) + assert did_run_test_module(result, "addon_success.tests.test_success")