|
@ -6,19 +6,39 @@ Contains common helpers to develop using this child project. |
|
|
""" |
|
|
""" |
|
|
import json |
|
|
import json |
|
|
import os |
|
|
import os |
|
|
|
|
|
import stat |
|
|
import tempfile |
|
|
import tempfile |
|
|
import time |
|
|
import time |
|
|
|
|
|
from datetime import datetime |
|
|
from itertools import chain |
|
|
from itertools import chain |
|
|
from logging import getLogger |
|
|
from logging import getLogger |
|
|
from pathlib import Path |
|
|
from pathlib import Path |
|
|
from shutil import which |
|
|
from shutil import which |
|
|
|
|
|
|
|
|
from invoke import exceptions, task |
|
|
from invoke import exceptions, task |
|
|
from invoke.util import yaml |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
import yaml |
|
|
|
|
|
except ImportError: |
|
|
|
|
|
from invoke.util import yaml |
|
|
|
|
|
|
|
|
PROJECT_ROOT = Path(__file__).parent.absolute() |
|
|
PROJECT_ROOT = Path(__file__).parent.absolute() |
|
|
SRC_PATH = PROJECT_ROOT / "odoo" / "custom" / "src" |
|
|
SRC_PATH = PROJECT_ROOT / "odoo" / "custom" / "src" |
|
|
UID_ENV = {"GID": str(os.getgid()), "UID": str(os.getuid()), "UMASK": "27"} |
|
|
|
|
|
|
|
|
UID_ENV = { |
|
|
|
|
|
"GID": os.environ.get("DOODBA_GID", str(os.getgid())), |
|
|
|
|
|
"UID": os.environ.get("DOODBA_UID", str(os.getuid())), |
|
|
|
|
|
"DOODBA_UMASK": os.environ.get("DOODBA_UMASK", "27"), |
|
|
|
|
|
} |
|
|
|
|
|
UID_ENV.update( |
|
|
|
|
|
{ |
|
|
|
|
|
"DOODBA_GITAGGREGATE_GID": os.environ.get( |
|
|
|
|
|
"DOODBA_GITAGGREGATE_GID", UID_ENV["GID"] |
|
|
|
|
|
), |
|
|
|
|
|
"DOODBA_GITAGGREGATE_UID": os.environ.get( |
|
|
|
|
|
"DOODBA_GITAGGREGATE_UID", UID_ENV["UID"] |
|
|
|
|
|
), |
|
|
|
|
|
} |
|
|
|
|
|
) |
|
|
SERVICES_WAIT_TIME = int(os.environ.get("SERVICES_WAIT_TIME", 4)) |
|
|
SERVICES_WAIT_TIME = int(os.environ.get("SERVICES_WAIT_TIME", 4)) |
|
|
ODOO_VERSION = float( |
|
|
ODOO_VERSION = float( |
|
|
yaml.safe_load((PROJECT_ROOT / "common.yaml").read_text())["services"]["odoo"][ |
|
|
yaml.safe_load((PROJECT_ROOT / "common.yaml").read_text())["services"]["odoo"][ |
|
@ -59,7 +79,7 @@ def _remove_auto_reload(file, orig_file): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_cwd_addon(file): |
|
|
def _get_cwd_addon(file): |
|
|
cwd = Path(file) |
|
|
|
|
|
|
|
|
cwd = Path(file).resolve() |
|
|
manifest_file = False |
|
|
manifest_file = False |
|
|
while PROJECT_ROOT < cwd: |
|
|
while PROJECT_ROOT < cwd: |
|
|
manifest_file = (cwd / "__manifest__.py").exists() or ( |
|
|
manifest_file = (cwd / "__manifest__.py").exists() or ( |
|
@ -108,7 +128,29 @@ def write_code_workspace_file(c, cw_path=None): |
|
|
pass # Nevermind, we start with a new config |
|
|
pass # Nevermind, we start with a new config |
|
|
# Static settings |
|
|
# Static settings |
|
|
cw_config.setdefault("settings", {}) |
|
|
cw_config.setdefault("settings", {}) |
|
|
cw_config["settings"].update({"search.followSymlinks": False}) |
|
|
|
|
|
|
|
|
cw_config["settings"].update( |
|
|
|
|
|
{ |
|
|
|
|
|
"python.autoComplete.extraPaths": [f"{str(SRC_PATH)}/odoo"], |
|
|
|
|
|
"python.linting.flake8Enabled": True, |
|
|
|
|
|
"python.linting.ignorePatterns": [f"{str(SRC_PATH)}/odoo/**/*.py"], |
|
|
|
|
|
"python.linting.pylintArgs": [ |
|
|
|
|
|
f"--init-hook=\"import sys;sys.path.append('{str(SRC_PATH)}/odoo')\"", |
|
|
|
|
|
"--load-plugins=pylint_odoo", |
|
|
|
|
|
], |
|
|
|
|
|
"python.linting.pylintEnabled": True, |
|
|
|
|
|
"python.pythonPath": "python%s" % (2 if ODOO_VERSION < 11 else 3), |
|
|
|
|
|
"restructuredtext.confPath": "", |
|
|
|
|
|
"search.followSymlinks": False, |
|
|
|
|
|
"search.useIgnoreFiles": False, |
|
|
|
|
|
# Language-specific configurations |
|
|
|
|
|
"[python]": {"editor.defaultFormatter": "ms-python.python"}, |
|
|
|
|
|
"[json]": {"editor.defaultFormatter": "esbenp.prettier-vscode"}, |
|
|
|
|
|
"[jsonc]": {"editor.defaultFormatter": "esbenp.prettier-vscode"}, |
|
|
|
|
|
"[markdown]": {"editor.defaultFormatter": "esbenp.prettier-vscode"}, |
|
|
|
|
|
"[yaml]": {"editor.defaultFormatter": "esbenp.prettier-vscode"}, |
|
|
|
|
|
"[xml]": {"editor.formatOnSave": False}, |
|
|
|
|
|
} |
|
|
|
|
|
) |
|
|
# Launch configurations |
|
|
# Launch configurations |
|
|
debugpy_configuration = { |
|
|
debugpy_configuration = { |
|
|
"name": "Attach Python debugger to running container", |
|
|
"name": "Attach Python debugger to running container", |
|
@ -221,6 +263,24 @@ def write_code_workspace_file(c, cw_path=None): |
|
|
"problemMatcher": [], |
|
|
"problemMatcher": [], |
|
|
"options": {"statusbar": {"label": "$(play-circle) Start Odoo"}}, |
|
|
"options": {"statusbar": {"label": "$(play-circle) Start Odoo"}}, |
|
|
}, |
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
"label": "Install current module", |
|
|
|
|
|
"type": "process", |
|
|
|
|
|
"command": "invoke", |
|
|
|
|
|
"args": ["install", "--cur-file", "${file}", "restart"], |
|
|
|
|
|
"presentation": { |
|
|
|
|
|
"echo": True, |
|
|
|
|
|
"reveal": "always", |
|
|
|
|
|
"focus": True, |
|
|
|
|
|
"panel": "shared", |
|
|
|
|
|
"showReuseMessage": True, |
|
|
|
|
|
"clear": False, |
|
|
|
|
|
}, |
|
|
|
|
|
"problemMatcher": [], |
|
|
|
|
|
"options": { |
|
|
|
|
|
"statusbar": {"label": "$(symbol-property) Install module"} |
|
|
|
|
|
}, |
|
|
|
|
|
}, |
|
|
{ |
|
|
{ |
|
|
"label": "Run Odoo Tests for current module", |
|
|
"label": "Run Odoo Tests for current module", |
|
|
"type": "process", |
|
|
"type": "process", |
|
@ -306,6 +366,24 @@ def write_code_workspace_file(c, cw_path=None): |
|
|
"problemMatcher": [], |
|
|
"problemMatcher": [], |
|
|
"options": {"statusbar": {"label": "$(history) Restart Odoo"}}, |
|
|
"options": {"statusbar": {"label": "$(history) Restart Odoo"}}, |
|
|
}, |
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
"label": "See container logs", |
|
|
|
|
|
"type": "process", |
|
|
|
|
|
"command": "invoke", |
|
|
|
|
|
"args": ["logs"], |
|
|
|
|
|
"presentation": { |
|
|
|
|
|
"echo": True, |
|
|
|
|
|
"reveal": "always", |
|
|
|
|
|
"focus": False, |
|
|
|
|
|
"panel": "shared", |
|
|
|
|
|
"showReuseMessage": True, |
|
|
|
|
|
"clear": False, |
|
|
|
|
|
}, |
|
|
|
|
|
"problemMatcher": [], |
|
|
|
|
|
"options": { |
|
|
|
|
|
"statusbar": {"label": "$(list-selection) See container logs"} |
|
|
|
|
|
}, |
|
|
|
|
|
}, |
|
|
], |
|
|
], |
|
|
} |
|
|
} |
|
|
# Sort project folders |
|
|
# Sort project folders |
|
@ -329,7 +407,12 @@ def write_code_workspace_file(c, cw_path=None): |
|
|
def develop(c): |
|
|
def develop(c): |
|
|
"""Set up a basic development environment.""" |
|
|
"""Set up a basic development environment.""" |
|
|
# Prepare environment |
|
|
# Prepare environment |
|
|
Path(PROJECT_ROOT, "odoo", "auto", "addons").mkdir(parents=True, exist_ok=True) |
|
|
|
|
|
|
|
|
auto = Path(PROJECT_ROOT, "odoo", "auto") |
|
|
|
|
|
addons = auto / "addons" |
|
|
|
|
|
addons.mkdir(parents=True, exist_ok=True) |
|
|
|
|
|
# Allow others writing, for podman support |
|
|
|
|
|
auto.chmod(0o777) |
|
|
|
|
|
addons.chmod(0o777) |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
c.run("git init") |
|
|
c.run("git init") |
|
|
c.run("ln -sf devel.yaml docker-compose.yml") |
|
|
c.run("ln -sf devel.yaml docker-compose.yml") |
|
@ -345,7 +428,7 @@ def git_aggregate(c): |
|
|
""" |
|
|
""" |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
c.run( |
|
|
c.run( |
|
|
"docker-compose --file setup-devel.yaml run --rm odoo", |
|
|
|
|
|
|
|
|
"docker-compose --file setup-devel.yaml run --rm -T odoo", |
|
|
env=UID_ENV, |
|
|
env=UID_ENV, |
|
|
) |
|
|
) |
|
|
write_code_workspace_file(c) |
|
|
write_code_workspace_file(c) |
|
@ -360,23 +443,31 @@ def git_aggregate(c): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@task(develop) |
|
|
@task(develop) |
|
|
|
|
|
def closed_prs(c): |
|
|
|
|
|
"""Test closed PRs from repos.yaml""" |
|
|
|
|
|
with c.cd(str(PROJECT_ROOT / "odoo/custom/src")): |
|
|
|
|
|
cmd = "gitaggregate -c {} show-closed-prs".format("repos.yaml") |
|
|
|
|
|
c.run(cmd, env=UID_ENV, pty=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@task() |
|
|
def img_build(c, pull=True): |
|
|
def img_build(c, pull=True): |
|
|
"""Build docker images.""" |
|
|
"""Build docker images.""" |
|
|
cmd = "docker-compose build" |
|
|
cmd = "docker-compose build" |
|
|
if pull: |
|
|
if pull: |
|
|
cmd += " --pull" |
|
|
cmd += " --pull" |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
c.run(cmd, env=UID_ENV) |
|
|
|
|
|
|
|
|
c.run(cmd, env=UID_ENV, pty=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@task(develop) |
|
|
|
|
|
|
|
|
@task() |
|
|
def img_pull(c): |
|
|
def img_pull(c): |
|
|
"""Pull docker images.""" |
|
|
"""Pull docker images.""" |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
c.run("docker-compose pull") |
|
|
|
|
|
|
|
|
c.run("docker-compose pull", pty=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@task(develop) |
|
|
|
|
|
|
|
|
@task() |
|
|
def lint(c, verbose=False): |
|
|
def lint(c, verbose=False): |
|
|
"""Lint & format source code.""" |
|
|
"""Lint & format source code.""" |
|
|
cmd = "pre-commit run --show-diff-on-failure --all-files --color=always" |
|
|
cmd = "pre-commit run --show-diff-on-failure --all-files --color=always" |
|
@ -386,7 +477,7 @@ def lint(c, verbose=False): |
|
|
c.run(cmd) |
|
|
c.run(cmd) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@task(develop) |
|
|
|
|
|
|
|
|
@task() |
|
|
def start(c, detach=True, debugpy=False): |
|
|
def start(c, detach=True, debugpy=False): |
|
|
"""Start environment.""" |
|
|
"""Start environment.""" |
|
|
cmd = "docker-compose up" |
|
|
cmd = "docker-compose up" |
|
@ -415,29 +506,43 @@ def start(c, detach=True, debugpy=False): |
|
|
DOODBA_DEBUGPY_ENABLE=str(int(debugpy)), |
|
|
DOODBA_DEBUGPY_ENABLE=str(int(debugpy)), |
|
|
), |
|
|
), |
|
|
) |
|
|
) |
|
|
if not ("Recreating" in result.stdout or "Starting" in result.stdout): |
|
|
|
|
|
|
|
|
if not ( |
|
|
|
|
|
"Recreating" in result.stdout |
|
|
|
|
|
or "Starting" in result.stdout |
|
|
|
|
|
or "Creating" in result.stdout |
|
|
|
|
|
): |
|
|
restart(c) |
|
|
restart(c) |
|
|
_logger.info("Waiting for services to spin up...") |
|
|
_logger.info("Waiting for services to spin up...") |
|
|
time.sleep(SERVICES_WAIT_TIME) |
|
|
time.sleep(SERVICES_WAIT_TIME) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@task( |
|
|
@task( |
|
|
develop, |
|
|
|
|
|
help={ |
|
|
help={ |
|
|
"modules": "Comma-separated list of modules to install.", |
|
|
"modules": "Comma-separated list of modules to install.", |
|
|
"core": "Install all core addons. Default: False", |
|
|
"core": "Install all core addons. Default: False", |
|
|
"extra": "Install all extra addons. Default: False", |
|
|
"extra": "Install all extra addons. Default: False", |
|
|
"private": "Install all private addons. Default: False", |
|
|
"private": "Install all private addons. Default: False", |
|
|
|
|
|
"enterprise": "Install all enterprise addons. Default: False", |
|
|
|
|
|
"cur-file": "Path to the current file." |
|
|
|
|
|
" Addon name will be obtained from there to install.", |
|
|
}, |
|
|
}, |
|
|
) |
|
|
) |
|
|
def install(c, modules=None, core=False, extra=False, private=False): |
|
|
|
|
|
|
|
|
def install( |
|
|
|
|
|
c, |
|
|
|
|
|
modules=None, |
|
|
|
|
|
cur_file=None, |
|
|
|
|
|
core=False, |
|
|
|
|
|
extra=False, |
|
|
|
|
|
private=False, |
|
|
|
|
|
enterprise=False, |
|
|
|
|
|
): |
|
|
"""Install Odoo addons |
|
|
"""Install Odoo addons |
|
|
|
|
|
|
|
|
By default, installs addon from directory being worked on, |
|
|
By default, installs addon from directory being worked on, |
|
|
unless other options are specified. |
|
|
unless other options are specified. |
|
|
""" |
|
|
""" |
|
|
if not (modules or core or extra or private): |
|
|
|
|
|
cur_module = _get_cwd_addon(Path.cwd()) |
|
|
|
|
|
|
|
|
if not (modules or core or extra or private or enterprise): |
|
|
|
|
|
cur_module = _get_cwd_addon(cur_file or Path.cwd()) |
|
|
if not cur_module: |
|
|
if not cur_module: |
|
|
raise exceptions.ParseError( |
|
|
raise exceptions.ParseError( |
|
|
msg="Odoo addon to install not found. " |
|
|
msg="Odoo addon to install not found. " |
|
@ -453,9 +558,12 @@ def install(c, modules=None, core=False, extra=False, private=False): |
|
|
cmd += " --extra" |
|
|
cmd += " --extra" |
|
|
if private: |
|
|
if private: |
|
|
cmd += " --private" |
|
|
cmd += " --private" |
|
|
|
|
|
if enterprise: |
|
|
|
|
|
cmd += " --enterprise" |
|
|
if modules: |
|
|
if modules: |
|
|
cmd += f" -w {modules}" |
|
|
cmd += f" -w {modules}" |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
|
|
|
c.run("docker-compose stop odoo") |
|
|
c.run( |
|
|
c.run( |
|
|
cmd, |
|
|
cmd, |
|
|
env=UID_ENV, |
|
|
env=UID_ENV, |
|
@ -463,6 +571,71 @@ def install(c, modules=None, core=False, extra=False, private=False): |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@task( |
|
|
|
|
|
help={ |
|
|
|
|
|
"modules": "Comma-separated list of modules to uninstall.", |
|
|
|
|
|
}, |
|
|
|
|
|
) |
|
|
|
|
|
def uninstall( |
|
|
|
|
|
c, |
|
|
|
|
|
modules=None, |
|
|
|
|
|
cur_file=None, |
|
|
|
|
|
): |
|
|
|
|
|
"""Uninstall Odoo addons |
|
|
|
|
|
|
|
|
|
|
|
By default, uninstalls addon from directory being worked on, |
|
|
|
|
|
unless other options are specified. |
|
|
|
|
|
""" |
|
|
|
|
|
if not modules: |
|
|
|
|
|
cur_module = _get_cwd_addon(cur_file or Path.cwd()) |
|
|
|
|
|
if not cur_module: |
|
|
|
|
|
raise exceptions.ParseError( |
|
|
|
|
|
msg="Odoo addon to uninstall not found. " |
|
|
|
|
|
"You must provide at least one option for modules" |
|
|
|
|
|
" or be in a subdirectory of one." |
|
|
|
|
|
" See --help for details." |
|
|
|
|
|
) |
|
|
|
|
|
modules = cur_module |
|
|
|
|
|
cmd = ( |
|
|
|
|
|
f"docker-compose run --rm odoo click-odoo-uninstall -m {modules or cur_module}" |
|
|
|
|
|
) |
|
|
|
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
|
|
|
c.run( |
|
|
|
|
|
cmd, |
|
|
|
|
|
env=UID_ENV, |
|
|
|
|
|
pty=True, |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_module_dependencies( |
|
|
|
|
|
c, modules=None, core=False, extra=False, private=False, enterprise=False |
|
|
|
|
|
): |
|
|
|
|
|
"""Returns a list of the addons' dependencies |
|
|
|
|
|
|
|
|
|
|
|
By default, refers to the addon from directory being worked on, |
|
|
|
|
|
unless other options are specified. |
|
|
|
|
|
""" |
|
|
|
|
|
# Get list of dependencies for addon |
|
|
|
|
|
cmd = "docker-compose run --rm odoo addons list --dependencies" |
|
|
|
|
|
if core: |
|
|
|
|
|
cmd += " --core" |
|
|
|
|
|
if extra: |
|
|
|
|
|
cmd += " --extra" |
|
|
|
|
|
if private: |
|
|
|
|
|
cmd += " --private" |
|
|
|
|
|
if enterprise: |
|
|
|
|
|
cmd += " --enterprise" |
|
|
|
|
|
if modules: |
|
|
|
|
|
cmd += f" -w {modules}" |
|
|
|
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
|
|
|
dependencies = c.run( |
|
|
|
|
|
cmd, |
|
|
|
|
|
env=UID_ENV, |
|
|
|
|
|
hide="stdout", |
|
|
|
|
|
).stdout.splitlines()[-1] |
|
|
|
|
|
return dependencies |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _test_in_debug_mode(c, odoo_command): |
|
|
def _test_in_debug_mode(c, odoo_command): |
|
|
with tempfile.NamedTemporaryFile( |
|
|
with tempfile.NamedTemporaryFile( |
|
|
mode="w", suffix=".yaml" |
|
|
mode="w", suffix=".yaml" |
|
@ -484,23 +657,80 @@ def _test_in_debug_mode(c, odoo_command): |
|
|
UID_ENV, |
|
|
UID_ENV, |
|
|
DOODBA_DEBUGPY_ENABLE="1", |
|
|
DOODBA_DEBUGPY_ENABLE="1", |
|
|
), |
|
|
), |
|
|
|
|
|
pty=True, |
|
|
) |
|
|
) |
|
|
_logger.info("Waiting for services to spin up...") |
|
|
_logger.info("Waiting for services to spin up...") |
|
|
time.sleep(SERVICES_WAIT_TIME) |
|
|
time.sleep(SERVICES_WAIT_TIME) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_module_list( |
|
|
|
|
|
c, |
|
|
|
|
|
modules=None, |
|
|
|
|
|
core=False, |
|
|
|
|
|
extra=False, |
|
|
|
|
|
private=False, |
|
|
|
|
|
enterprise=False, |
|
|
|
|
|
only_installable=True, |
|
|
|
|
|
): |
|
|
|
|
|
"""Returns a list of addons according to the passed parameters. |
|
|
|
|
|
|
|
|
|
|
|
By default, refers to the addon from directory being worked on, |
|
|
|
|
|
unless other options are specified. |
|
|
|
|
|
""" |
|
|
|
|
|
# Get list of dependencies for addon |
|
|
|
|
|
cmd = "docker-compose run --rm odoo addons list" |
|
|
|
|
|
if core: |
|
|
|
|
|
cmd += " --core" |
|
|
|
|
|
if extra: |
|
|
|
|
|
cmd += " --extra" |
|
|
|
|
|
if private: |
|
|
|
|
|
cmd += " --private" |
|
|
|
|
|
if enterprise: |
|
|
|
|
|
cmd += " --enterprise" |
|
|
|
|
|
if modules: |
|
|
|
|
|
cmd += f" -w {modules}" |
|
|
|
|
|
if only_installable: |
|
|
|
|
|
cmd += " --installable" |
|
|
|
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
|
|
|
module_list = c.run( |
|
|
|
|
|
cmd, |
|
|
|
|
|
env=UID_ENV, |
|
|
|
|
|
pty=True, |
|
|
|
|
|
hide="stdout", |
|
|
|
|
|
).stdout.splitlines()[-1] |
|
|
|
|
|
return module_list |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@task( |
|
|
@task( |
|
|
develop, |
|
|
|
|
|
help={ |
|
|
help={ |
|
|
"modules": "Comma-separated list of modules to test.", |
|
|
"modules": "Comma-separated list of modules to test.", |
|
|
|
|
|
"core": "Test all core addons. Default: False", |
|
|
|
|
|
"extra": "Test all extra addons. Default: False", |
|
|
|
|
|
"private": "Test all private addons. Default: False", |
|
|
|
|
|
"enterprise": "Test all enterprise addons. Default: False", |
|
|
|
|
|
"skip": "List of addons to skip. Default: []", |
|
|
"debugpy": "Whether or not to run tests in a VSCode debugging session. " |
|
|
"debugpy": "Whether or not to run tests in a VSCode debugging session. " |
|
|
"Default: False", |
|
|
"Default: False", |
|
|
"cur-file": "Path to the current file." |
|
|
"cur-file": "Path to the current file." |
|
|
" Addon name will be obtained from there to run tests", |
|
|
" Addon name will be obtained from there to run tests", |
|
|
"mode": "Mode in which tests run. Options: ['init'(default), 'update']", |
|
|
"mode": "Mode in which tests run. Options: ['init'(default), 'update']", |
|
|
|
|
|
"db_filter": "DB_FILTER regex to pass to the test container Set to ''" |
|
|
|
|
|
" to disable. Default: '^devel$'", |
|
|
}, |
|
|
}, |
|
|
) |
|
|
) |
|
|
def test(c, modules=None, debugpy=False, cur_file=None, mode="init"): |
|
|
|
|
|
|
|
|
def test( |
|
|
|
|
|
c, |
|
|
|
|
|
modules=None, |
|
|
|
|
|
core=False, |
|
|
|
|
|
extra=False, |
|
|
|
|
|
private=False, |
|
|
|
|
|
enterprise=False, |
|
|
|
|
|
skip="", |
|
|
|
|
|
debugpy=False, |
|
|
|
|
|
cur_file=None, |
|
|
|
|
|
mode="init", |
|
|
|
|
|
db_filter="^devel$", |
|
|
|
|
|
): |
|
|
"""Run Odoo tests |
|
|
"""Run Odoo tests |
|
|
|
|
|
|
|
|
By default, tests addon from directory being worked on, |
|
|
By default, tests addon from directory being worked on, |
|
@ -508,17 +738,18 @@ def test(c, modules=None, debugpy=False, cur_file=None, mode="init"): |
|
|
|
|
|
|
|
|
NOTE: Odoo must be restarted manually after this to go back to normal mode |
|
|
NOTE: Odoo must be restarted manually after this to go back to normal mode |
|
|
""" |
|
|
""" |
|
|
if not modules: |
|
|
|
|
|
|
|
|
if not (modules or core or extra or private or enterprise): |
|
|
cur_module = _get_cwd_addon(cur_file or Path.cwd()) |
|
|
cur_module = _get_cwd_addon(cur_file or Path.cwd()) |
|
|
if not cur_module: |
|
|
if not cur_module: |
|
|
raise exceptions.ParseError( |
|
|
raise exceptions.ParseError( |
|
|
msg="Odoo addon to test not found. " |
|
|
|
|
|
"You must provide at least one option for modules/file " |
|
|
|
|
|
"or be in a subdirectory of one. " |
|
|
|
|
|
"See --help for details." |
|
|
|
|
|
|
|
|
msg="Odoo addon to install not found. " |
|
|
|
|
|
"You must provide at least one option for modules" |
|
|
|
|
|
" or be in a subdirectory of one." |
|
|
|
|
|
" See --help for details." |
|
|
) |
|
|
) |
|
|
else: |
|
|
|
|
|
modules = cur_module |
|
|
|
|
|
|
|
|
modules = cur_module |
|
|
|
|
|
else: |
|
|
|
|
|
modules = _get_module_list(c, modules, core, extra, private, enterprise) |
|
|
odoo_command = ["odoo", "--test-enable", "--stop-after-init", "--workers=0"] |
|
|
odoo_command = ["odoo", "--test-enable", "--stop-after-init", "--workers=0"] |
|
|
if mode == "init": |
|
|
if mode == "init": |
|
|
odoo_command.append("-i") |
|
|
odoo_command.append("-i") |
|
@ -528,11 +759,30 @@ def test(c, modules=None, debugpy=False, cur_file=None, mode="init"): |
|
|
raise exceptions.ParseError( |
|
|
raise exceptions.ParseError( |
|
|
msg="Available modes are 'init' or 'update'. See --help for details." |
|
|
msg="Available modes are 'init' or 'update'. See --help for details." |
|
|
) |
|
|
) |
|
|
|
|
|
# Skip test in some modules |
|
|
|
|
|
modules_list = modules.split(",") |
|
|
|
|
|
for m_to_skip in skip.split(","): |
|
|
|
|
|
if not m_to_skip: |
|
|
|
|
|
continue |
|
|
|
|
|
if m_to_skip not in modules: |
|
|
|
|
|
_logger.warn( |
|
|
|
|
|
"%s not found in the list of addons to test: %s" % (m_to_skip, modules) |
|
|
|
|
|
) |
|
|
|
|
|
modules_list.remove(m_to_skip) |
|
|
|
|
|
modules = ",".join(modules_list) |
|
|
odoo_command.append(modules) |
|
|
odoo_command.append(modules) |
|
|
|
|
|
if ODOO_VERSION >= 12: |
|
|
|
|
|
# Limit tests to explicit list |
|
|
|
|
|
# Filter spec format (comma-separated) |
|
|
|
|
|
# [-][tag][/module][:class][.method] |
|
|
|
|
|
odoo_command.extend(["--test-tags", "/" + ",/".join(modules_list)]) |
|
|
if debugpy: |
|
|
if debugpy: |
|
|
_test_in_debug_mode(c, odoo_command) |
|
|
_test_in_debug_mode(c, odoo_command) |
|
|
else: |
|
|
else: |
|
|
cmd = ["docker-compose", "run", "--rm", "odoo"] |
|
|
|
|
|
|
|
|
cmd = ["docker-compose", "run", "--rm"] |
|
|
|
|
|
if db_filter: |
|
|
|
|
|
cmd.extend(["-e", "DB_FILTER='%s'" % db_filter]) |
|
|
|
|
|
cmd.append("odoo") |
|
|
cmd.extend(odoo_command) |
|
|
cmd.extend(odoo_command) |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
c.run( |
|
|
c.run( |
|
@ -543,33 +793,53 @@ def test(c, modules=None, debugpy=False, cur_file=None, mode="init"): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@task( |
|
|
@task( |
|
|
develop, |
|
|
|
|
|
help={"purge": "Remove all related containers, networks images and volumes"}, |
|
|
help={"purge": "Remove all related containers, networks images and volumes"}, |
|
|
) |
|
|
) |
|
|
def stop(c, purge=False): |
|
|
def stop(c, purge=False): |
|
|
"""Stop and (optionally) purge environment.""" |
|
|
"""Stop and (optionally) purge environment.""" |
|
|
cmd = "docker-compose" |
|
|
|
|
|
|
|
|
cmd = "docker-compose down --remove-orphans" |
|
|
if purge: |
|
|
if purge: |
|
|
cmd += " down --remove-orphans --rmi local --volumes" |
|
|
|
|
|
else: |
|
|
|
|
|
cmd += " stop" |
|
|
|
|
|
|
|
|
cmd += " --rmi local --volumes" |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
c.run(cmd) |
|
|
|
|
|
|
|
|
c.run(cmd, pty=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@task( |
|
|
@task( |
|
|
develop, |
|
|
|
|
|
help={ |
|
|
help={ |
|
|
"dbname": "The DB that will be DESTROYED and recreated. Default: 'devel'.", |
|
|
"dbname": "The DB that will be DESTROYED and recreated. Default: 'devel'.", |
|
|
"modules": "Comma-separated list of modules to install. Default: 'base'.", |
|
|
"modules": "Comma-separated list of modules to install. Default: 'base'.", |
|
|
|
|
|
"core": "Install all core addons. Default: False", |
|
|
|
|
|
"extra": "Install all extra addons. Default: False", |
|
|
|
|
|
"private": "Install all private addons. Default: False", |
|
|
|
|
|
"enterprise": "Install all enterprise addons. Default: False", |
|
|
|
|
|
"populate": "Run preparedb task right after (only available for v11+)." |
|
|
|
|
|
" Default: True", |
|
|
|
|
|
"dependencies": "Install only the dependencies of the specified addons." |
|
|
|
|
|
"Default: False", |
|
|
}, |
|
|
}, |
|
|
) |
|
|
) |
|
|
def resetdb(c, modules="base", dbname="devel"): |
|
|
|
|
|
|
|
|
def resetdb( |
|
|
|
|
|
c, |
|
|
|
|
|
modules=None, |
|
|
|
|
|
core=False, |
|
|
|
|
|
extra=False, |
|
|
|
|
|
private=False, |
|
|
|
|
|
enterprise=False, |
|
|
|
|
|
dbname="devel", |
|
|
|
|
|
populate=True, |
|
|
|
|
|
dependencies=False, |
|
|
|
|
|
): |
|
|
"""Reset the specified database with the specified modules. |
|
|
"""Reset the specified database with the specified modules. |
|
|
|
|
|
|
|
|
Uses click-odoo-initdb behind the scenes, which has a caching system that |
|
|
Uses click-odoo-initdb behind the scenes, which has a caching system that |
|
|
makes DB resets quicker. See its docs for more info. |
|
|
makes DB resets quicker. See its docs for more info. |
|
|
""" |
|
|
""" |
|
|
|
|
|
if dependencies: |
|
|
|
|
|
modules = _get_module_dependencies(c, modules, core, extra, private, enterprise) |
|
|
|
|
|
elif core or extra or private or enterprise: |
|
|
|
|
|
modules = _get_module_list(c, modules, core, extra, private, enterprise) |
|
|
|
|
|
else: |
|
|
|
|
|
modules = modules or "base" |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
c.run("docker-compose stop odoo", pty=True) |
|
|
c.run("docker-compose stop odoo", pty=True) |
|
|
_run = "docker-compose run --rm -l traefik.enable=false odoo" |
|
|
_run = "docker-compose run --rm -l traefik.enable=false odoo" |
|
@ -584,9 +854,34 @@ def resetdb(c, modules="base", dbname="devel"): |
|
|
env=UID_ENV, |
|
|
env=UID_ENV, |
|
|
pty=True, |
|
|
pty=True, |
|
|
) |
|
|
) |
|
|
|
|
|
if populate and ODOO_VERSION < 11: |
|
|
|
|
|
_logger.warn( |
|
|
|
|
|
"Skipping populate task as it is not available in v%s" % ODOO_VERSION |
|
|
|
|
|
) |
|
|
|
|
|
populate = False |
|
|
|
|
|
if populate: |
|
|
|
|
|
preparedb(c) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@task(develop) |
|
|
|
|
|
|
|
|
@task() |
|
|
|
|
|
def preparedb(c): |
|
|
|
|
|
"""Run the `preparedb` script inside the container |
|
|
|
|
|
|
|
|
|
|
|
Populates the DB with some helpful config |
|
|
|
|
|
""" |
|
|
|
|
|
if ODOO_VERSION < 11: |
|
|
|
|
|
raise exceptions.PlatformError( |
|
|
|
|
|
"The preparedb script is not available for Doodba environments bellow v11." |
|
|
|
|
|
) |
|
|
|
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
|
|
|
c.run( |
|
|
|
|
|
"docker-compose run --rm -l traefik.enable=false odoo preparedb", |
|
|
|
|
|
env=UID_ENV, |
|
|
|
|
|
pty=True, |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@task() |
|
|
def restart(c, quick=True): |
|
|
def restart(c, quick=True): |
|
|
"""Restart odoo container(s).""" |
|
|
"""Restart odoo container(s).""" |
|
|
cmd = "docker-compose restart" |
|
|
cmd = "docker-compose restart" |
|
@ -594,11 +889,10 @@ def restart(c, quick=True): |
|
|
cmd = f"{cmd} -t0" |
|
|
cmd = f"{cmd} -t0" |
|
|
cmd = f"{cmd} odoo odoo_proxy" |
|
|
cmd = f"{cmd} odoo odoo_proxy" |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
c.run(cmd, env=UID_ENV) |
|
|
|
|
|
|
|
|
c.run(cmd, env=UID_ENV, pty=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@task( |
|
|
@task( |
|
|
develop, |
|
|
|
|
|
help={ |
|
|
help={ |
|
|
"container": "Names of the containers from which logs will be obtained." |
|
|
"container": "Names of the containers from which logs will be obtained." |
|
|
" You can specify a single one, or several comma-separated names." |
|
|
" You can specify a single one, or several comma-separated names." |
|
@ -615,4 +909,132 @@ def logs(c, tail=10, follow=True, container=None): |
|
|
if container: |
|
|
if container: |
|
|
cmd += f" {container.replace(',', ' ')}" |
|
|
cmd += f" {container.replace(',', ' ')}" |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
c.run(cmd) |
|
|
|
|
|
|
|
|
c.run(cmd, pty=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@task |
|
|
|
|
|
def after_update(c): |
|
|
|
|
|
"""Execute some actions after a copier update or init""" |
|
|
|
|
|
# Make custom build scripts executable |
|
|
|
|
|
if ODOO_VERSION < 11: |
|
|
|
|
|
files = ( |
|
|
|
|
|
Path(PROJECT_ROOT, "odoo", "custom", "build.d", "20-update-pg-repos"), |
|
|
|
|
|
Path(PROJECT_ROOT, "odoo", "custom", "build.d", "10-fix-certs"), |
|
|
|
|
|
) |
|
|
|
|
|
for script_file in files: |
|
|
|
|
|
# Ignore if, for some reason, the file didn't end up in the generated |
|
|
|
|
|
# project despite of the correct version (e.g. Copier exclusions) |
|
|
|
|
|
if not script_file.exists(): |
|
|
|
|
|
continue |
|
|
|
|
|
cur_stat = script_file.stat() |
|
|
|
|
|
# Like chmod ug+x |
|
|
|
|
|
script_file.chmod(cur_stat.st_mode | stat.S_IXUSR | stat.S_IXGRP) |
|
|
|
|
|
else: |
|
|
|
|
|
# Remove version-specific build scripts if the copier update didn't |
|
|
|
|
|
# HACK: https://github.com/copier-org/copier/issues/461 |
|
|
|
|
|
files = ( |
|
|
|
|
|
Path(PROJECT_ROOT, "odoo", "custom", "build.d", "20-update-pg-repos"), |
|
|
|
|
|
Path(PROJECT_ROOT, "odoo", "custom", "build.d", "10-fix-certs"), |
|
|
|
|
|
) |
|
|
|
|
|
for script_file in files: |
|
|
|
|
|
# missing_ok argument would take care of this, but it was only added for |
|
|
|
|
|
# Python 3.8 |
|
|
|
|
|
if script_file.exists(): |
|
|
|
|
|
script_file.unlink() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@task( |
|
|
|
|
|
help={ |
|
|
|
|
|
"source_db": "The source DB name. Default: 'devel'.", |
|
|
|
|
|
"destination_db": "The destination DB name. Default: '[SOURCE_DB_NAME]-[CURRENT_DATE]'", |
|
|
|
|
|
}, |
|
|
|
|
|
) |
|
|
|
|
|
def snapshot( |
|
|
|
|
|
c, |
|
|
|
|
|
source_db="devel", |
|
|
|
|
|
destination_db=None, |
|
|
|
|
|
): |
|
|
|
|
|
"""Snapshot current database and filestore. |
|
|
|
|
|
|
|
|
|
|
|
Uses click-odoo-copydb behind the scenes to make a snapshot. |
|
|
|
|
|
""" |
|
|
|
|
|
if not destination_db: |
|
|
|
|
|
destination_db = "%s-%s" % ( |
|
|
|
|
|
source_db, |
|
|
|
|
|
datetime.now().strftime("%Y_%m_%d-%H_%M"), |
|
|
|
|
|
) |
|
|
|
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
|
|
|
cur_state = c.run("docker-compose stop odoo db", pty=True).stdout |
|
|
|
|
|
_logger.info("Snapshoting current %s DB to %s" % (source_db, destination_db)) |
|
|
|
|
|
_run = "docker-compose run --rm -l traefik.enable=false odoo" |
|
|
|
|
|
c.run( |
|
|
|
|
|
f"{_run} click-odoo-copydb {source_db} {destination_db}", |
|
|
|
|
|
env=UID_ENV, |
|
|
|
|
|
pty=True, |
|
|
|
|
|
) |
|
|
|
|
|
if "Stopping" in cur_state: |
|
|
|
|
|
# Restart services if they were previously active |
|
|
|
|
|
c.run("docker-compose start odoo db", pty=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@task( |
|
|
|
|
|
help={ |
|
|
|
|
|
"snapshot_name": "The snapshot name. If not provided," |
|
|
|
|
|
"the script will try to find the last snapshot" |
|
|
|
|
|
" that starts with the destination_db name", |
|
|
|
|
|
"destination_db": "The destination DB name. Default: 'devel'", |
|
|
|
|
|
}, |
|
|
|
|
|
) |
|
|
|
|
|
def restore_snapshot( |
|
|
|
|
|
c, |
|
|
|
|
|
snapshot_name=None, |
|
|
|
|
|
destination_db="devel", |
|
|
|
|
|
): |
|
|
|
|
|
"""Restore database and filestore snapshot. |
|
|
|
|
|
|
|
|
|
|
|
Uses click-odoo-copydb behind the scenes to restore a DB snapshot. |
|
|
|
|
|
""" |
|
|
|
|
|
with c.cd(str(PROJECT_ROOT)): |
|
|
|
|
|
cur_state = c.run("docker-compose stop odoo db", pty=True).stdout |
|
|
|
|
|
if not snapshot_name: |
|
|
|
|
|
# List DBs |
|
|
|
|
|
res = c.run( |
|
|
|
|
|
"docker-compose run --rm -e LOG_LEVEL=WARNING odoo psql -tc" |
|
|
|
|
|
" 'SELECT datname FROM pg_database;'", |
|
|
|
|
|
env=UID_ENV, |
|
|
|
|
|
hide="stdout", |
|
|
|
|
|
) |
|
|
|
|
|
db_list = [] |
|
|
|
|
|
for db in res.stdout.splitlines(): |
|
|
|
|
|
# Parse and filter DB List |
|
|
|
|
|
if not db.lstrip().startswith(destination_db): |
|
|
|
|
|
continue |
|
|
|
|
|
db_name = db.lstrip() |
|
|
|
|
|
try: |
|
|
|
|
|
db_date = datetime.strptime( |
|
|
|
|
|
db_name.lstrip(destination_db + "-"), "%Y_%m_%d-%H_%M" |
|
|
|
|
|
) |
|
|
|
|
|
db_list.append((db_name, db_date)) |
|
|
|
|
|
except ValueError: |
|
|
|
|
|
continue |
|
|
|
|
|
snapshot_name = max(db_list, key=lambda x: x[1])[0] |
|
|
|
|
|
if not snapshot_name: |
|
|
|
|
|
raise exceptions.PlatformError( |
|
|
|
|
|
"No snapshot found for destination_db %s" % destination_db |
|
|
|
|
|
) |
|
|
|
|
|
_logger.info("Restoring snapshot %s to %s" % (snapshot_name, destination_db)) |
|
|
|
|
|
_run = "docker-compose run --rm -l traefik.enable=false odoo" |
|
|
|
|
|
c.run( |
|
|
|
|
|
f"{_run} click-odoo-dropdb {destination_db}", |
|
|
|
|
|
env=UID_ENV, |
|
|
|
|
|
warn=True, |
|
|
|
|
|
pty=True, |
|
|
|
|
|
) |
|
|
|
|
|
c.run( |
|
|
|
|
|
f"{_run} click-odoo-copydb {snapshot_name} {destination_db}", |
|
|
|
|
|
env=UID_ENV, |
|
|
|
|
|
pty=True, |
|
|
|
|
|
) |
|
|
|
|
|
if "Stopping" in cur_state: |
|
|
|
|
|
# Restart services if they were previously active |
|
|
|
|
|
c.run("docker-compose start odoo db", pty=True) |