Skip to content

Commit b7cdea3

Browse files
cammarosanofda-odoo
authored andcommitted
[IMP] sort modules by load order
Module packages are now built in the same order as the Odoo server loads them.
1 parent 631b2e5 commit b7cdea3

File tree

4 files changed

+400
-4
lines changed

4 files changed

+400
-4
lines changed

server/src/core/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub mod entry_point;
66
pub mod file_mgr;
77
pub mod import_resolver;
88
pub mod model;
9+
pub mod module_load_order;
910
pub mod odoo;
1011
pub mod python_arch_builder;
1112
pub mod python_arch_builder_hooks;
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
use std::collections::HashMap;
2+
3+
pub struct SortResult<'a> {
4+
pub sorted: Vec<&'a str>,
5+
pub invalid: Vec<&'a str>, // due to missing direct/indirect dependencies or dependency cycle
6+
pub missing: Vec<&'a str>,
7+
pub cycles: Vec<Vec<&'a str>>,
8+
}
9+
10+
/// Sort modules based on Odoo module load order logic.
11+
///
12+
/// Input: Vec of (module_name, dependencies)
13+
///
14+
/// Output: SortResult containing sorted valid modules and invalid modules
15+
///
16+
/// Note: 'base' module is expected to be included in the input modules.
17+
pub fn sort_by_load_order<'a>(modules: Vec<(&'a str, Vec<&'a str>)>) -> SortResult<'a> {
18+
let mut graph = Graph::from(modules);
19+
let (sorted, invalid) = graph.get_load_order();
20+
let mut missing = Vec::new();
21+
let mut cycles = Vec::new();
22+
for error in graph.errors {
23+
match error {
24+
ValidationError::MissingModule(m) => missing.push(m),
25+
ValidationError::DependencyCycle(cycle) => cycles.push(cycle),
26+
}
27+
}
28+
SortResult {
29+
sorted,
30+
invalid,
31+
missing,
32+
cycles,
33+
}
34+
}
35+
36+
/// Replicates (roughly) the module load order logic from the ModuleGraph in Odoo framework
37+
struct Graph<'a> {
38+
modules_to_dependencies: HashMap<&'a str, Vec<&'a str>>, // keys: nodes, v: edges from that node
39+
errors: Vec<ValidationError<'a>>,
40+
// Caches for memoization
41+
validation_cache: HashMap<&'a str, bool>,
42+
depth_cache: HashMap<&'a str, usize>,
43+
order_name_cache: HashMap<&'a str, String>,
44+
}
45+
46+
enum ValidationError<'a> {
47+
MissingModule(&'a str),
48+
DependencyCycle(Vec<&'a str>),
49+
}
50+
51+
/// Simple macro to cache some method calls in Graph.
52+
/// Limitation: $compute block cannot have return statements.
53+
macro_rules! cached {
54+
($cache:expr, $key:expr, $compute:block) => {{
55+
if let Some(value) = $cache.get($key) {
56+
value.clone() // this is a simple copy for &str and usize
57+
} else {
58+
let value = $compute;
59+
$cache.insert($key, value.clone());
60+
value
61+
}
62+
}};
63+
}
64+
65+
impl<'a> Graph<'a> {
66+
fn from(nodes: Vec<(&'a str, Vec<&'a str>)>) -> Self {
67+
let mut graph = Self {
68+
modules_to_dependencies: nodes.into_iter().collect(),
69+
validation_cache: HashMap::new(),
70+
depth_cache: HashMap::new(),
71+
order_name_cache: HashMap::new(),
72+
errors: Vec::new(),
73+
};
74+
graph.add_base_dependency();
75+
graph
76+
}
77+
78+
/// Returns (sorted valid modules, invalid modules)
79+
fn get_load_order(&mut self) -> (Vec<&'a str>, Vec<&'a str>) {
80+
self.errors.clear();
81+
// sort modules between valid and invalid
82+
let modules: Vec<_> = self.modules_to_dependencies.keys().cloned().collect();
83+
let mut valid_modules = vec![];
84+
let mut invalid_modules = vec![];
85+
for module in modules {
86+
if self.is_valid_module(module, &mut Vec::new()) {
87+
valid_modules.push(module);
88+
} else {
89+
invalid_modules.push(module);
90+
}
91+
}
92+
// sort valid modules by load order
93+
valid_modules.sort_by_cached_key(|m| self.get_sort_key(m));
94+
// sort invalid modules lexicographically (avoid hashmap indeterministic order)
95+
invalid_modules.sort();
96+
(valid_modules, invalid_modules)
97+
}
98+
99+
// ====== Setup ==========
100+
101+
// `depends` = [] in the manifest implies a dependency on 'base'
102+
// See _load_manifest @ module.py in Odoo
103+
fn add_base_dependency(&mut self) {
104+
for (module, dependencies) in self.modules_to_dependencies.iter_mut() {
105+
if *module != "base" && dependencies.is_empty() {
106+
dependencies.push("base");
107+
}
108+
}
109+
}
110+
111+
// ====== Validation ==========
112+
113+
fn is_valid_module(&mut self, name: &'a str, recursion_stack: &mut Vec<&'a str>) -> bool {
114+
// Cache key is just `name` (not recursion_stack) because:
115+
// - recursion_stack is only for cycle detection during traversal
116+
// - if a cycle is detected, the module is cached as `false` from the inner recursive call
117+
// - once cached, the result is valid for all future lookups regardless of traversal path
118+
// Note: the block for cached! macro cannot contain return statements (therefore the indirection)
119+
cached!(self.validation_cache, name, {
120+
self._is_valid_module(name, recursion_stack)
121+
})
122+
}
123+
124+
/// A module is valid when:
125+
/// - it exists
126+
/// - is not part of a dependency cycle
127+
/// - all its dependencies are valid modules
128+
///
129+
/// This implementation is different from the Odoo python framework (see
130+
/// ModuleGraph._update_depends and _update_depth)
131+
fn _is_valid_module(&mut self, name: &'a str, recursion_stack: &mut Vec<&'a str>) -> bool {
132+
if recursion_stack.contains(&name) {
133+
// dependency cycle detected
134+
self.errors
135+
.push(ValidationError::new_dep_cycle_error(recursion_stack));
136+
return false;
137+
}
138+
let Some(dependencies) = self.modules_to_dependencies.get(name).cloned() else {
139+
// module does not exist
140+
self.errors.push(ValidationError::MissingModule(name));
141+
return false;
142+
};
143+
recursion_stack.push(name);
144+
let is_valid = dependencies
145+
.iter()
146+
.all(|&dep| self.is_valid_module(dep, recursion_stack));
147+
recursion_stack.pop();
148+
is_valid
149+
}
150+
151+
// ===== Sort algo (topological sort) =======
152+
153+
/// Sorting key for a module: max depth of its dependencies + 1, then
154+
/// lexicographical by module name.
155+
///
156+
/// test_* modules have a special rule: they load right after their last
157+
/// loaded dependency.
158+
fn get_sort_key(&mut self, module: &'a str) -> (usize, String) {
159+
let depth = self.get_depth(module);
160+
let order_name = self.get_order_name(module);
161+
(depth, order_name)
162+
}
163+
164+
fn get_depth(&mut self, module: &'a str) -> usize {
165+
cached!(self.depth_cache, module, {
166+
let dependencies = self
167+
.modules_to_dependencies
168+
.get(module)
169+
.expect("module to exist")
170+
.clone();
171+
let deps_max_depth = dependencies.iter().map(|&dep| self.get_depth(dep)).max();
172+
match deps_max_depth {
173+
None => 0, // empty dependencies (base module)
174+
Some(d) if module.starts_with("test_") => d, // test_ modules
175+
Some(d) => d + 1, // regular module
176+
}
177+
})
178+
}
179+
180+
fn get_order_name(&mut self, module: &'a str) -> String {
181+
cached!(self.order_name_cache, module, {
182+
if module.starts_with("test_") {
183+
let last_loaded_dep = self
184+
.get_last_loaded_dep(module)
185+
.expect("test_ module to have at least 'base' as dependency");
186+
self.get_order_name(last_loaded_dep) + " " + module
187+
} else {
188+
return module.to_string();
189+
}
190+
})
191+
}
192+
193+
fn get_last_loaded_dep(&mut self, module: &'a str) -> Option<&'a str> {
194+
let dependencies = self
195+
.modules_to_dependencies
196+
.get(module)
197+
.expect("module to exist")
198+
.clone();
199+
dependencies
200+
.into_iter()
201+
.max_by_key(|&d| self.get_sort_key(d))
202+
}
203+
}
204+
205+
impl<'a> ValidationError<'a> {
206+
pub fn new_dep_cycle_error(recursion_stack: &[&'a str]) -> Self {
207+
let top = *recursion_stack.last().expect("non-empty recursion stack");
208+
let mut modules_in_cycle = vec![top];
209+
for &module in recursion_stack.iter().rev().skip(1) {
210+
if module == top {
211+
break;
212+
}
213+
modules_in_cycle.push(module);
214+
}
215+
modules_in_cycle.reverse();
216+
Self::DependencyCycle(modules_in_cycle)
217+
}
218+
}
219+
220+
#[cfg(test)]
221+
mod tests {
222+
use super::*;
223+
224+
#[test]
225+
fn test_simple_linear_dependency() {
226+
// a -> b -> c -> base
227+
let nodes = vec![
228+
("a", vec!["b"]),
229+
("b", vec!["c"]),
230+
("c", vec!["base"]),
231+
("base", vec![]),
232+
];
233+
let mut graph = Graph::from(nodes);
234+
let (order, _) = graph.get_load_order();
235+
// c must come before b, b before a
236+
assert_eq!(order, vec!["base", "c", "b", "a"]);
237+
}
238+
239+
#[test]
240+
fn test_base_loads_first() {
241+
let nodes = vec![
242+
("base", vec![]),
243+
("a", vec![]), // implicitly depends on base
244+
("b", vec!["base"]),
245+
("c", vec!["a", "b"]),
246+
];
247+
let mut graph = Graph::from(nodes);
248+
let (order, _) = graph.get_load_order();
249+
assert_eq!(order, vec!["base", "a", "b", "c"]);
250+
}
251+
252+
#[test]
253+
fn test_multiple_independent_modules() {
254+
// base dependency is implicit
255+
let nodes = vec![
256+
("a", vec![]),
257+
("b", vec![]),
258+
("c", vec![]),
259+
("base", vec![]),
260+
];
261+
let mut graph = Graph::from(nodes);
262+
let (order, _) = graph.get_load_order();
263+
assert_eq!(order, vec!["base", "a", "b", "c"]);
264+
}
265+
266+
#[test]
267+
fn test_branching_dependencies() {
268+
let nodes = vec![
269+
("a", vec!["b", "c"]),
270+
("b", vec!["d"]),
271+
("c", vec!["d"]),
272+
("d", vec!["base"]),
273+
("base", vec![]),
274+
];
275+
let mut graph = Graph::from(nodes);
276+
let (order, _) = graph.get_load_order();
277+
assert_eq!(order, vec!["base", "d", "b", "c", "a"]);
278+
}
279+
280+
#[test]
281+
fn test_test_modules() {
282+
let nodes = vec![
283+
("base", vec![]),
284+
("a", vec!["base"]),
285+
("b", vec!["a"]),
286+
("test_b", vec!["a", "b"]), // should load right after b, before c
287+
("c", vec!["a", "b"]),
288+
];
289+
let mut graph = Graph::from(nodes);
290+
let (order, _) = graph.get_load_order();
291+
assert_eq!(order, vec!["base", "a", "b", "test_b", "c"]);
292+
}
293+
294+
#[test]
295+
fn test_nested_test_modules() {
296+
let nodes = vec![
297+
("base", vec![]),
298+
("a", vec!["base"]),
299+
("b", vec!["a"]),
300+
("test_a", vec!["a"]), // should load right after a
301+
("test_x", vec!["test_a"]), // should load right after test_a
302+
];
303+
let mut graph = Graph::from(nodes);
304+
let (order, _) = graph.get_load_order();
305+
assert_eq!(order, vec!["base", "a", "test_a", "test_x", "b"]);
306+
}
307+
308+
#[test]
309+
fn test_dependency_cycle_detection() {
310+
let nodes = vec![
311+
("base", vec![]),
312+
("a", vec!["b"]),
313+
("b", vec!["c"]),
314+
("c", vec!["a"]), // cycle here
315+
("d", vec!["base"]),
316+
];
317+
let mut graph = Graph::from(nodes);
318+
let (order, _) = graph.get_load_order();
319+
// a, b, c should be excluded due to cycle
320+
assert_eq!(order, vec!["base", "d"]);
321+
assert!(!graph.errors.is_empty());
322+
match &graph.errors[0] {
323+
ValidationError::DependencyCycle(cycle) => {
324+
// cycle should contain a, b, c (order might differ, due to hashmap iteration)
325+
let mut cycle = cycle.clone();
326+
cycle.sort();
327+
assert_eq!(cycle, vec!["a", "b", "c"]);
328+
}
329+
_ => panic!("Expected DependencyCycle error"),
330+
}
331+
}
332+
333+
#[test]
334+
fn test_missing_module_detection() {
335+
let nodes = vec![
336+
("base", vec![]),
337+
("a", vec!["b"]), // b is missing
338+
("c", vec!["base"]),
339+
];
340+
let mut graph = Graph::from(nodes);
341+
let (order, _) = graph.get_load_order();
342+
// a should be excluded due to missing dependency
343+
assert_eq!(order, vec!["base", "c"]);
344+
assert!(!graph.errors.is_empty());
345+
match &graph.errors[0] {
346+
ValidationError::MissingModule(missing) => {
347+
assert_eq!(*missing, "b");
348+
}
349+
_ => panic!("Expected MissingModule error"),
350+
}
351+
}
352+
}

0 commit comments

Comments
 (0)