Skip to content

Commit de0bfd6

Browse files
committed
[FIX] odoo_repository: fix '_get_recursive_dependencies' method
1 parent 6000479 commit de0bfd6

3 files changed

Lines changed: 217 additions & 4 deletions

File tree

odoo_repository/models/odoo_module_branch.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ def init(self):
208208
CREATE UNIQUE INDEX IF NOT EXISTS odoo_module_branch_uniq_null
209209
ON odoo_module_branch (module_id, branch_id)
210210
WHERE repository_id IS NULL;
211-
"""
211+
""",
212212
# PostgreSQL >= 15 (with NULLS NOT DISTINCT)
213213
# """
214214
# CREATE UNIQUE INDEX odoo_module_branch_uniq
@@ -296,21 +296,30 @@ def _compute_dependency_level(self):
296296
else 0
297297
)
298298

299-
def _get_recursive_dependencies(self, domain=None):
299+
def _get_recursive_dependencies(self, domain=None, _visited=None):
300300
"""Return all dependencies recursively.
301301
302302
A domain can be applied to restrict the modules to return, e.g:
303303
304304
>>> mod._get_recursive_dependencies([("org_id", "=", "OCA")])
305305
306306
"""
307+
# NOTE: Circular dependencies are allowed
307308
if not domain:
308309
domain = []
309-
dependencies = self.dependency_ids.filtered_domain(domain)
310+
if _visited is None:
311+
_visited = set()
312+
if self.id in _visited:
313+
return self.browse()
314+
_visited.add(self.id)
315+
# Apply domain and exclude self
316+
dependencies = (self.dependency_ids - self).filtered_domain(domain)
310317
dep_ids = set(dependencies.ids)
311318
for dep in dependencies:
312319
dep_ids |= set(
313-
dep._get_recursive_dependencies().filtered_domain(domain).ids
320+
dep._get_recursive_dependencies(domain, _visited)
321+
.filtered_domain(domain)
322+
.ids
314323
)
315324
return self.browse(dep_ids)
316325

odoo_repository/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
from . import test_sync_node
66
from . import test_odoo_module_branch
77
from . import test_oca_repository_synchronizer
8+
from . import test_odoo_module_branch_recursive_dependencies
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# Copyright 2026 Sébastien Alix
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
3+
4+
from .common import Common
5+
6+
7+
class TestOdooModuleBranchRecursiveDependencies(Common):
8+
@classmethod
9+
def setUpClass(cls):
10+
super().setUpClass()
11+
# Create modules
12+
cls.mod_base = cls._create_odoo_module("base")
13+
cls.mod_a = cls._create_odoo_module("module_a")
14+
cls.mod_b = cls._create_odoo_module("module_b")
15+
cls.mod_c = cls._create_odoo_module("module_c")
16+
cls.mod_d = cls._create_odoo_module("module_d")
17+
cls.mod_e = cls._create_odoo_module("module_e")
18+
# Create module branches
19+
cls.mod_base_branch = cls._create_odoo_module_branch(
20+
cls.mod_base, cls.branch, is_standard=True
21+
)
22+
cls.mod_a_branch = cls._create_odoo_module_branch(cls.mod_a, cls.branch)
23+
cls.mod_b_branch = cls._create_odoo_module_branch(cls.mod_b, cls.branch)
24+
cls.mod_c_branch = cls._create_odoo_module_branch(cls.mod_c, cls.branch)
25+
cls.mod_d_branch = cls._create_odoo_module_branch(cls.mod_d, cls.branch)
26+
cls.mod_e_branch = cls._create_odoo_module_branch(cls.mod_e, cls.branch)
27+
28+
def test_get_recursive_dependencies_simple(self):
29+
"""Test _get_recursive_dependencies with a simple dependency chain."""
30+
self.mod_a_branch.dependency_ids = self.mod_base_branch
31+
self.mod_b_branch.dependency_ids = self.mod_a_branch
32+
self.mod_c_branch.dependency_ids = self.mod_b_branch
33+
# Test recursive dependencies for module_c
34+
deps = self.mod_c_branch._get_recursive_dependencies()
35+
self.assertIn(self.mod_base_branch, deps)
36+
self.assertIn(self.mod_a_branch, deps)
37+
self.assertIn(self.mod_b_branch, deps)
38+
self.assertEqual(len(deps), 3) # base, module_a, module_b
39+
40+
def test_get_recursive_dependencies_with_domain(self):
41+
"""Test _get_recursive_dependencies with domain filtering."""
42+
# Create organization
43+
org_oca = self.env.ref("odoo_repository.odoo_repository_org_oca")
44+
org_other = self.env["odoo.repository.org"].create({"name": "Other"})
45+
# Create repositories
46+
repo_oca = self.env["odoo.repository"].create(
47+
{
48+
"org_id": org_oca.id,
49+
"name": "repo_oca",
50+
"repo_url": "https://github.com/OCA/repo_oca",
51+
"clone_url": "https://github.com/OCA/repo_oca",
52+
"repo_type": "github",
53+
}
54+
)
55+
repo_other = self.env["odoo.repository"].create(
56+
{
57+
"org_id": org_other.id,
58+
"name": "repo_other",
59+
"repo_url": "https://github.com/Other/repo_other",
60+
"clone_url": "https://github.com/Other/repo_other",
61+
"repo_type": "github",
62+
}
63+
)
64+
# Create modules
65+
mod_oca = self._create_odoo_module("module_oca")
66+
mod_other = self._create_odoo_module("module_other")
67+
mod_mixed = self._create_odoo_module("module_mixed")
68+
# Create repository branches
69+
repo_oca_branch = self._create_odoo_repository_branch(repo_oca, self.branch)
70+
repo_other_branch = self._create_odoo_repository_branch(repo_other, self.branch)
71+
# Create module branches
72+
mod_oca_branch = self._create_odoo_module_branch(
73+
mod_oca,
74+
self.branch,
75+
repository_branch_id=repo_oca_branch.id,
76+
dependency_ids=[(4, self.mod_base_branch.id)],
77+
)
78+
mod_other_branch = self._create_odoo_module_branch(
79+
mod_other,
80+
self.branch,
81+
repository_branch_id=repo_other_branch.id,
82+
dependency_ids=[(4, self.mod_base_branch.id)],
83+
)
84+
mod_mixed_branch = self._create_odoo_module_branch(
85+
mod_mixed,
86+
self.branch,
87+
repository_branch_id=repo_oca_branch.id,
88+
dependency_ids=[(4, mod_oca_branch.id), (4, mod_other_branch.id)],
89+
)
90+
# Test recursive dependencies with OCA filter
91+
deps = mod_mixed_branch._get_recursive_dependencies(
92+
[("org_id", "=", org_oca.id)]
93+
)
94+
self.assertIn(mod_oca_branch, deps)
95+
self.assertNotIn(mod_other_branch, deps) # filtered out
96+
# base has no org, so it's filtered out by the domain
97+
self.assertNotIn(self.mod_base_branch, deps)
98+
self.assertEqual(len(deps), 1) # only module_oca
99+
100+
def test_get_recursive_dependencies_circular_1(self):
101+
"""Test _get_recursive_dependencies handles circular dependencies (1)."""
102+
# Create module branches with circular dependencies
103+
# base
104+
# ├── a ─┐
105+
# ├── b │
106+
# ├── c │
107+
# └── d <┘
108+
self.mod_a_branch.dependency_ids = self.mod_base_branch
109+
self.mod_b_branch.dependency_ids = self.mod_a_branch
110+
self.mod_c_branch.dependency_ids = self.mod_b_branch
111+
self.mod_d_branch.dependency_ids = self.mod_c_branch
112+
# Create circular dependency: mod_a -> mod_d
113+
self.mod_a_branch.dependency_ids |= self.mod_d_branch
114+
# Test recursive dependencies - should not infinite loop
115+
deps = self.mod_b_branch._get_recursive_dependencies()
116+
self.assertIn(self.mod_base_branch, deps)
117+
self.assertIn(self.mod_a_branch, deps)
118+
self.assertIn(self.mod_b_branch, deps)
119+
self.assertIn(self.mod_c_branch, deps)
120+
self.assertIn(self.mod_d_branch, deps)
121+
# Check that we get the expected dependencies
122+
# We expect base, a, b, c, d (mod_b is part from its own dependencies)
123+
self.assertEqual(len(deps), 5)
124+
125+
def test_get_recursive_dependencies_circular_2(self):
126+
"""Test _get_recursive_dependencies handles circular dependencies (2)."""
127+
# Create module branches with circular dependencies
128+
# base
129+
# ├── a ─┐
130+
# ├── b │
131+
# ├── c <┘
132+
# └── d
133+
self.mod_a_branch.dependency_ids = self.mod_base_branch
134+
self.mod_b_branch.dependency_ids = self.mod_a_branch
135+
self.mod_c_branch.dependency_ids = self.mod_b_branch
136+
self.mod_d_branch.dependency_ids = self.mod_c_branch
137+
# Create circular dependency: mod_a -> mod_d
138+
self.mod_a_branch.dependency_ids |= self.mod_c_branch
139+
# Test recursive dependencies - should not infinite loop
140+
deps = self.mod_d_branch._get_recursive_dependencies()
141+
self.assertIn(self.mod_base_branch, deps)
142+
self.assertIn(self.mod_a_branch, deps)
143+
self.assertIn(self.mod_b_branch, deps)
144+
self.assertIn(self.mod_c_branch, deps)
145+
self.assertNotIn(self.mod_d_branch, deps)
146+
# Check that we get the expected dependencies
147+
# We expect base, a, b, c (mod_d is excluded from its own dependencies)
148+
self.assertEqual(len(deps), 4)
149+
150+
def test_get_recursive_dependencies_complex_tree(self):
151+
"""Test _get_recursive_dependencies with complex dependency tree."""
152+
# Create module branches with complex dependencies:
153+
# base
154+
# ├── a
155+
# │ ├── c
156+
# │ └── d
157+
# └── b
158+
# └── e
159+
self.mod_a_branch.dependency_ids = self.mod_base_branch
160+
self.mod_b_branch.dependency_ids = self.mod_base_branch
161+
self.mod_c_branch.dependency_ids = self.mod_a_branch
162+
self.mod_d_branch.dependency_ids = self.mod_a_branch
163+
self.mod_e_branch.dependency_ids = self.mod_b_branch
164+
# Test from module_c
165+
deps_c = self.mod_c_branch._get_recursive_dependencies()
166+
self.assertIn(self.mod_base_branch, deps_c)
167+
self.assertIn(self.mod_a_branch, deps_c)
168+
self.assertNotIn(self.mod_b_branch, deps_c)
169+
self.assertNotIn(self.mod_c_branch, deps_c)
170+
self.assertNotIn(self.mod_d_branch, deps_c)
171+
self.assertNotIn(self.mod_e_branch, deps_c)
172+
self.assertEqual(len(deps_c), 2) # base, module_a
173+
# Test from module_e
174+
deps_e = self.mod_e_branch._get_recursive_dependencies()
175+
self.assertIn(self.mod_base_branch, deps_e)
176+
self.assertIn(self.mod_b_branch, deps_e)
177+
self.assertNotIn(self.mod_a_branch, deps_e)
178+
self.assertNotIn(self.mod_c_branch, deps_e)
179+
self.assertNotIn(self.mod_d_branch, deps_e)
180+
self.assertEqual(len(deps_e), 2) # base, module_b
181+
182+
def test_get_recursive_dependencies_empty(self):
183+
"""Test _get_recursive_dependencies with no dependencies."""
184+
# Create module with no dependencies
185+
mod_alone = self._create_odoo_module("module_alone")
186+
mod_alone_branch = self._create_odoo_module_branch(mod_alone, self.branch)
187+
# Test recursive dependencies
188+
deps = mod_alone_branch._get_recursive_dependencies()
189+
self.assertEqual(len(deps), 0)
190+
191+
def test_get_recursive_dependencies_self_exclusion(self):
192+
"""Test that _get_recursive_dependencies excludes self."""
193+
# Create modules
194+
mod_self = self._create_odoo_module("module_self")
195+
# Create module branches
196+
mod_self_branch = self._create_odoo_module_branch(
197+
mod_self, self.branch, dependency_ids=[(4, self.mod_base_branch.id)]
198+
)
199+
# Test recursive dependencies - self should not be included
200+
deps = mod_self_branch._get_recursive_dependencies()
201+
self.assertIn(self.mod_base_branch, deps)
202+
self.assertNotIn(mod_self_branch, deps)
203+
self.assertEqual(len(deps), 1)

0 commit comments

Comments
 (0)