odoo-runbot 1.0.0b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ __version__ = "1.0.0-beta2"
2
+ __author__ = "Alexis Pasquier"
3
+ __author_email__ = "alexis.pasquier@mangono.io"
@@ -0,0 +1,12 @@
1
+ """This lib is a CLI using typer.
2
+
3
+ I tried to separate the code by feature part.
4
+
5
+ - [odoo_runbot.cli][] Contains the entrypoint, All the rich display is done here. (Other than log)
6
+ - [odoo_runbot.runbot_env][] Contains il a python way the env in wich the runbot is running.
7
+ - [odoo_runbot.runbot_init][] Contains all the code to execute to initialize the Odoo to run test
8
+ - [odoo_runbot.runbot_init_branch_resolver][] Contains the working branch resolving (Pull other add-ons from git)
9
+ """
10
+
11
+ __import__("os").environ["TZ"] = "UTC"
12
+ __import__("os").environ["WITHOUT_DEMO"] = "False"
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import logging
5
+ import typing
6
+
7
+ if typing.TYPE_CHECKING:
8
+ import pathlib
9
+
10
+ from unittest.mock import patch
11
+
12
+ from xmlrunner import XMLTestRunner
13
+ from xmlrunner.result import _XMLTestResult
14
+
15
+ _logger = logging.getLogger("odoo_runbot.odoo_internal")
16
+ try:
17
+ import odoo
18
+
19
+ except ModuleNotFoundError:
20
+ from unittest import mock
21
+
22
+ odoo = mock.MagicMock()
23
+
24
+ __all__ = ["odoo", "patch_odoo_test_suite", "run_odoo_and_stop", "setup_odoo"]
25
+
26
+
27
+ class _RunProto(typing.Protocol):
28
+ def run(self, result): ...
29
+
30
+
31
+ class _UpdateProto(typing.Protocol):
32
+ def update(self, other): ...
33
+
34
+
35
+ @contextlib.contextmanager
36
+ def patch_odoo_test_suite(output_dir: pathlib.Path) -> typing.Generator[None]:
37
+ """Patch odoo test suite and Odoo test result.
38
+
39
+ - In Odoo 12 and Odoo 13, The test result is the native unittest.TestResult
40
+ - In Odoo 14 and Odoo 15. The test result extend unittest.TestResult and add `update` function. So we call it
41
+ - In 16.0 and more the OdooTestResult don't extend unittest.TestResult.
42
+ I need to patch the `update` function to add result of the `_XmlTestResult`
43
+
44
+ Args:
45
+ output_dir: Te patch where to store the xml result
46
+
47
+ Returns: Nothing, but unpatched function at the end
48
+ """
49
+
50
+ odoo_suite_class, odoo_result_class, apply_update = _try_find_stuff_to_patch()
51
+ if not odoo_suite_class:
52
+ msg = f"Odoo version {odoo.release.version} not supported"
53
+ raise ValueError(msg)
54
+ output_dir.mkdir(exist_ok=True, parents=True)
55
+
56
+ _logger.info("Create test result in %s", output_dir.absolute())
57
+ _unpatched_run = odoo_suite_class.run
58
+ patch_odoo_suite_run = f"{odoo_suite_class.__module__}.{odoo_suite_class.__name__}.run"
59
+ _logger.info("Patch %s", patch_odoo_suite_run)
60
+ patch_odoo_suite_update = None
61
+ if odoo_result_class:
62
+ _unpatched_update = odoo_result_class.update
63
+ patch_odoo_suite_update = f"{odoo_result_class.__module__}.{odoo_result_class.__name__}.update"
64
+ _logger.info("Patch %s", patch_odoo_suite_update)
65
+
66
+ def patched_suite_run(self, result):
67
+ # Suite run method will be called by the XMLTestRunner,
68
+ # so we need to run the original run method
69
+ with patch.object(self, "run", lambda result: _unpatched_run(self, result)):
70
+ # Override : XMLTestRunner to run the tests and generate XML reports
71
+ xml_results = XMLTestRunner(output=str(output_dir.absolute()), verbosity=2).run(self)
72
+ if apply_update:
73
+ result.update(xml_results)
74
+ return result
75
+ return xml_results
76
+
77
+ def patched_suite_update(self, other):
78
+ # Adapt _XMLTestResult to OdooTestResult
79
+ if isinstance(other, _XMLTestResult):
80
+ self.failures_count += len(other.failures)
81
+ self.errors_count += len(other.errors)
82
+ self.skipped += len(other.skipped)
83
+ self.testsRun += other.testsRun
84
+ else:
85
+ _unpatched_update(self, other)
86
+
87
+ with patch(patch_odoo_suite_run, patched_suite_run):
88
+ if odoo_result_class:
89
+ with patch(patch_odoo_suite_update, patched_suite_update):
90
+ yield
91
+ else:
92
+ yield
93
+
94
+
95
+ def _try_find_stuff_to_patch() -> tuple[type[_RunProto] | None, type[_UpdateProto] | None, bool]:
96
+ apply_update = True
97
+ if "12.0" <= odoo.release.version <= "13.0":
98
+ from odoo.tests.common import OdooSuite as odoo_suite_class # noqa: N813
99
+
100
+ odoo_result_class = None
101
+ apply_update = False
102
+ elif "14.0" <= odoo.release.version <= "15.0":
103
+ from odoo.tests.common import OdooSuite as odoo_suite_class # noqa: N813
104
+
105
+ odoo_result_class = None
106
+ elif "16.0" <= odoo.release.version <= "18.0":
107
+ from odoo.tests.result import OdooTestResult as odoo_result_class # noqa: N813
108
+ from odoo.tests.suite import OdooSuite as odoo_suite_class # noqa: N813
109
+ else:
110
+ return None, None, False
111
+
112
+ return odoo_suite_class, odoo_result_class, apply_update
113
+
114
+
115
+ def run_odoo_and_stop(preload: list[str] | None = None) -> int:
116
+ preload = preload or odoo.tools.config["db_name"].split(",")
117
+ return odoo.service.server.start(preload=preload, stop=True)
118
+
119
+
120
+ def setup_odoo() -> None:
121
+ odoo.cli.server.check_root_user()
122
+ odoo.cli.server.check_postgres_user()
123
+ odoo.cli.server.report_configuration()
124
+ odoo.netsvc.init_logger()
125
+
126
+
127
+ def parse_config(server_args: list[str]) -> None:
128
+ __import__("os").environ["TZ"] = "UTC"
129
+ _logger.info("Start Odoo with args=%s", server_args)
130
+ # Hard set value
131
+ # In test mode we don't care about loading language
132
+ # But il you do multiple step, you load multiple time the registry.
133
+ # And in odoo.modules.loadings.load_module they `pop` this key, idkw
134
+ # So in test mode, we force this option key to None
135
+ odoo.tools.config.options["load_language"] = None
136
+ odoo.tools.config.parse_config(server_args)
odoo_runbot/cli.py ADDED
@@ -0,0 +1,234 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from rich.console import Console, ConsoleOptions, Group, RenderResult
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+ from typer import Typer
12
+ from typing_extensions import Annotated
13
+
14
+ from . import runbot_init
15
+ from .runbot_env import RunbotEnvironment, RunbotStepConfig, RunbotToolConfig, StepAction
16
+ from .runbot_run import StepRunner
17
+
18
+ app = Typer()
19
+
20
+ env: RunbotEnvironment = None
21
+
22
+
23
+ def rich_force_colors() -> bool:
24
+ """In Gitlab CI runner there is no tty, and no color.
25
+
26
+ This function force the color even if there is no TTY
27
+ See Also:
28
+ https://github.com/nf-core/tools/pull/760/files
29
+ https://github.com/Textualize/rich/issues/343
30
+ Returns:
31
+ True if in CI/CD Job or if color is forced
32
+ """
33
+ return bool(
34
+ os.getenv("CI")
35
+ or os.getenv("GITHUB_ACTIONS")
36
+ or os.getenv("FORCE_COLOR")
37
+ or os.getenv("PY_COLORS")
38
+ or os.getenv("COLORTERM") == "truecolor"
39
+ )
40
+
41
+
42
+ console = Console(width=150, force_terminal=rich_force_colors())
43
+
44
+ _logger = logging.getLogger("odoo_runbot")
45
+
46
+
47
+ @app.callback()
48
+ def _callback(workdir: Path = None, *, verbose: bool = False) -> None:
49
+ global env # noqa: PLW0603
50
+ env = RunbotEnvironment(dict(os.environ), workdir=workdir, verbose=verbose)
51
+ env.setup_logging_for_runbot(console)
52
+ env.print_info()
53
+ if not env.check_ok():
54
+ raise TypeError(300)
55
+
56
+
57
+ @app.command("init")
58
+ def init_runbot() -> None:
59
+ """Init the current project to run test
60
+ - Find external addons depedency, install them if needed (git clone + pip install)
61
+ - Init database, and wait postgresql is ready
62
+ - Create basic config file for Odoo using $ODOO_RC
63
+ """
64
+ current_project_key = f"ADDONS_LOCAL_{env.abs_curr_dir.name.upper()}"
65
+ project_config = RunbotToolConfig.from_env(env)
66
+ _logger.info(
67
+ "Add current project %s=%s ? %s",
68
+ current_project_key,
69
+ str(env.abs_curr_dir),
70
+ project_config.include_current_project,
71
+ )
72
+ if project_config.include_current_project:
73
+ env.environ[current_project_key] = str(env.abs_curr_dir)
74
+ info, addons = runbot_init.show_addons(env)
75
+ console.print(info)
76
+ runbot_init.install_addons(addons)
77
+ console.print(runbot_init.init_database(env))
78
+ console.print(runbot_init.init_config(env))
79
+
80
+
81
+ @app.command("run", help="""Run the steps after initializing the project""")
82
+ def run_runbot(
83
+ step_name: Annotated[str, typer.Option(envvar="RUNBOT_STEP")] = "all",
84
+ only_action: Annotated[StepAction, typer.Option()] = None,
85
+ ) -> None:
86
+ """Run the steps after initializing the project
87
+
88
+ You can filter the step you want to run with the `step_name` argument.
89
+
90
+ warning:
91
+ If no step is run, then mangono-runbot will exit with code 100.
92
+
93
+ Args:
94
+ step_name: The step to run, if None, are "all" then no filter is applied
95
+ only_action: Choose wich action to only run
96
+
97
+ """
98
+ if not Path(env.ODOO_RC).exists():
99
+ _logger.error("[red] Please run `mangono-runbot init config` to create your odoo config file")
100
+ raise typer.Abort
101
+
102
+ project_config = RunbotToolConfig.from_env(env)
103
+ steps_to_run = []
104
+ for step in project_config.steps:
105
+ if only_action is not None and step.action != only_action:
106
+ _logger.debug("Filter step %s, only want action=%s", step.name, only_action.name)
107
+ continue
108
+ if step_name and step_name not in (None, "all", step.name):
109
+ _logger.info("Skip warmup %s", step.name)
110
+ continue
111
+ steps_to_run.append(step)
112
+
113
+ step_runner = StepRunner(env)
114
+ step_runner.setup_warning_filter(project_config.warning_filters)
115
+ step_runner.setup_odoo()
116
+ for step in steps_to_run:
117
+ console.print(RichStep(step))
118
+ rc = step_runner.execute(step)
119
+ if rc:
120
+ console.print(f"[red] Step {step.name} {bool_to_emoji(False)}") # noqa: FBT003
121
+ raise typer.Exit(rc)
122
+ console.print(f"[green] Step {step.name} {bool_to_emoji(True)}") # noqa: FBT003
123
+
124
+ if not step_runner.has_run:
125
+ raise typer.Exit(100)
126
+
127
+
128
+ def bool_to_emoji(v: bool) -> str: # noqa: FBT001
129
+ return ":heavy_check_mark:" if v else ":x:"
130
+
131
+
132
+ ASCII_ART_MANGONO = """
133
+ [medium_spring_green] % % [/]
134
+ [medium_spring_green] %%% %%% %%%%%%% [/]
135
+ [medium_spring_green] %%%% %%% %%% %%%[/]
136
+ [medium_spring_green]%%%% %%% %% %%%[/]
137
+ [medium_spring_green] %%%% %%% %%% %%%[/]
138
+ [medium_spring_green] %%% %%% %%%%%%% [/]
139
+ [medium_spring_green] % % [/]
140
+
141
+ @@@ @@@@ @@@@ @@@ @@ @@@@@@ @@@@@@ @@@ @@ @@@@@@ [medium_spring_green]%%%[/]
142
+ @@@@ @@@@ @@@@ @@@@ @@ @@ @@@ @@@ @@@@ @@@@ @@ @@@ @@@ [medium_spring_green] %%%[/]
143
+ @@@@@ @@@@@ @@@ @@ @@ @@ @@ @@@ @@@@@@@@@ @@@ @@ @@ @@ @@@ @@@ [medium_spring_green] %%%%[/]
144
+ @@@ @@@ @@@ @@@@@@@@ @@ @@@@ @@ @@ @@@ @@@@ @@ @@@@ @@@ @@@ [medium_spring_green] %%%[/]
145
+ @@@ @@@ @@@ @@ @@@ @@ @@@ @@@@@@ @@@@@@ @@ @@@ @@@@@@ [medium_spring_green]%%%[/]
146
+
147
+ @ @ @@ @ @@ @ @ @@@ @ @@ @@ @@@ @ @ [medium_spring_green]%%%%%%%%%[/]
148
+ [medium_spring_green]%%%%%%%%%[/]
149
+ """
150
+
151
+
152
+ @app.command("diag")
153
+ def diag_print() -> None:
154
+ console.print(ASCII_ART_MANGONO)
155
+ console.print("Main Author: Alexis Pasquier")
156
+ t_warn = Table(
157
+ "Name",
158
+ "Action",
159
+ "Message Filter",
160
+ "Wanted Category",
161
+ title="py.warnings Filters",
162
+ )
163
+ project_config = RunbotToolConfig.from_env(env)
164
+ if project_config.warning_filters:
165
+ for log_filter in project_config.warning_filters:
166
+ t_warn.add_row(
167
+ log_filter.name,
168
+ log_filter.action,
169
+ f"r'{log_filter.message}'",
170
+ log_filter.category,
171
+ )
172
+ else:
173
+ t_warn.add_row("[DEFAULT] No py.warnings allowed", "always", ".*", "Warnings", "*")
174
+ console.print(t_warn)
175
+ table = Table(
176
+ "Step",
177
+ "Module",
178
+ "Run tests",
179
+ "Activate Coverage",
180
+ "Tags to test",
181
+ "Logger filter",
182
+ title="Steps",
183
+ )
184
+ for step in project_config.steps:
185
+ table.add_row(
186
+ step.name,
187
+ (step.modules and ",".join(step.modules)) or str(step.modules),
188
+ step.action.name,
189
+ bool_to_emoji(step.action == StepAction.TESTS and step.coverage),
190
+ ",".join(step.test_tags),
191
+ ",".join([f.name for f in step.log_filters]),
192
+ )
193
+ console.print(table)
194
+
195
+ for step in project_config.steps:
196
+ console.print(RichStep(step))
197
+
198
+
199
+ class RichStep:
200
+ def __init__(self, step: RunbotStepConfig) -> None:
201
+ self.step = step
202
+
203
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
204
+ t_log = Table("Name", "Regex", "Match", "logger", title="Log Filters", width=146)
205
+ if self.step.log_filters:
206
+ for log_filter in self.step.log_filters:
207
+ match_txt = f"Between {log_filter.min_match} and {log_filter.max_match}"
208
+ if log_filter.min_match == log_filter.max_match:
209
+ match_txt = f"Exactly {log_filter.min_match}"
210
+ t_log.add_row(
211
+ log_filter.name,
212
+ f"r'{log_filter.regex}'",
213
+ match_txt,
214
+ log_filter.logger + " (and all child logger)",
215
+ )
216
+ else:
217
+ t_log.add_row("[DEFAULT] No log allowed", ".*", "Exactly 0", "odoo (and all child logger)")
218
+
219
+ yield Panel(
220
+ Group(
221
+ f"Install : {self.step.modules}",
222
+ f"Action : {self.step.action.name}",
223
+ f"Activate Coverage: {bool_to_emoji(self.step.action == StepAction.TESTS and self.step.coverage)}",
224
+ f"Allow warnings: {bool_to_emoji(self.step.allow_warnings)}",
225
+ f"Test Tags: {self.step.test_tags}",
226
+ t_log,
227
+ ),
228
+ style="green" if self.step.action == StepAction.TESTS else "dodger_blue2",
229
+ title=self.step.name,
230
+ )
231
+
232
+
233
+ if __name__ == "__main__":
234
+ app()