Skip to content

Commit d2ce17f

Browse files
committed
tests(P0): couvrir le flux d'installation engines et l'analyseur deps
- Ajoute des tests de non-regression pour garantir l'ordre systeme puis Python dans EngineLoader/base.py, y compris le cas d'echec systeme. - Ajoute des tests heuristiques pour l'analyseur de dependances (racines internes via __init__, metadata projet, classification internal/third_party). - Met a jour TODO.md pour marquer les taches P0 de tests comme realisees.
1 parent d5e91a5 commit d2ce17f

3 files changed

Lines changed: 264 additions & 2 deletions

File tree

TODO.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
### Phase 2 - Securiser les flux critiques (P0)
99
- [x] Orchestrer les installs outils en sequence stricte: systeme puis Python.
1010
- [x] En cas d'echec systeme, journaliser proprement puis continuer la phase Python.
11-
- [ ] Ajouter des tests unitaires sur `EngineLoader/base.py` (ordre d'execution et cas d'echec).
12-
- [ ] Ajouter des tests de non-regression sur `Core/deps_analyser/analyser.py`.
11+
- [x] Ajouter des tests unitaires sur `EngineLoader/base.py` (ordre d'execution et cas d'echec).
12+
- [x] Ajouter des tests de non-regression sur `Core/deps_analyser/analyser.py`.
1313

1414
### Phase 3 - Fiabiliser l'analyseur de dependances (P0)
1515
- [x] Renforcer la classification `stdlib/internal/third_party/unknown`.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2026 Ague Samuel Amen
3+
4+
from __future__ import annotations
5+
6+
import sys
7+
from pathlib import Path
8+
from types import SimpleNamespace
9+
import types
10+
import importlib.util
11+
12+
import pytest
13+
14+
ROOT = Path(__file__).resolve().parents[1]
15+
if str(ROOT) not in sys.path:
16+
sys.path.insert(0, str(ROOT))
17+
18+
qtcore = types.ModuleType("PySide6.QtCore")
19+
qtwidgets = types.ModuleType("PySide6.QtWidgets")
20+
pyside6 = types.ModuleType("PySide6")
21+
core_pkg = types.ModuleType("Core")
22+
widgets_creator = types.ModuleType("Core.WidgetsCreator")
23+
i18n = types.ModuleType("Core.i18n")
24+
25+
26+
class _DummyQProcess:
27+
pass
28+
29+
30+
class _DummyApp:
31+
@staticmethod
32+
def processEvents():
33+
return None
34+
35+
36+
class _DummyMessageBox:
37+
Yes = 1
38+
No = 0
39+
ActionRole = 0
40+
AcceptRole = 1
41+
RejectRole = 2
42+
43+
44+
class _DummyProgressDialog:
45+
def __init__(self, *_args, **_kwargs):
46+
pass
47+
48+
49+
def _dummy_log(*_args, **_kwargs):
50+
return None
51+
52+
53+
qtcore.QProcess = _DummyQProcess
54+
qtwidgets.QApplication = _DummyApp
55+
qtwidgets.QMessageBox = _DummyMessageBox
56+
widgets_creator.ProgressDialog = _DummyProgressDialog
57+
i18n.log_with_level = _dummy_log
58+
pyside6.QtCore = qtcore
59+
pyside6.QtWidgets = qtwidgets
60+
core_pkg.WidgetsCreator = widgets_creator
61+
core_pkg.i18n = i18n
62+
63+
sys.modules.setdefault("PySide6", pyside6)
64+
sys.modules.setdefault("PySide6.QtCore", qtcore)
65+
sys.modules.setdefault("PySide6.QtWidgets", qtwidgets)
66+
sys.modules.setdefault("Core", core_pkg)
67+
sys.modules.setdefault("Core.WidgetsCreator", widgets_creator)
68+
sys.modules.setdefault("Core.i18n", i18n)
69+
70+
_ANALYSER_PATH = ROOT / "Core" / "deps_analyser" / "analyser.py"
71+
_SPEC = importlib.util.spec_from_file_location("ark_test_analyser", _ANALYSER_PATH)
72+
assert _SPEC and _SPEC.loader
73+
da = importlib.util.module_from_spec(_SPEC)
74+
_SPEC.loader.exec_module(da)
75+
76+
77+
@pytest.fixture(autouse=True)
78+
def _clear_caches() -> None:
79+
da._is_stdlib_module.cache_clear()
80+
da._classify_module_origin.cache_clear()
81+
82+
83+
def test_collect_workspace_module_roots_supports_init_chain_and_metadata(
84+
tmp_path: Path,
85+
) -> None:
86+
ws = tmp_path / "workspace"
87+
(ws / "src" / "acme_pkg").mkdir(parents=True)
88+
(ws / "src" / "acme_pkg" / "__init__.py").write_text("", encoding="utf-8")
89+
(ws / "src" / "acme_pkg" / "main.py").write_text("import os\n", encoding="utf-8")
90+
91+
(ws / "app" / "core").mkdir(parents=True)
92+
(ws / "app" / "core" / "__init__.py").write_text("", encoding="utf-8")
93+
94+
(ws / "pyproject.toml").write_text(
95+
"[project]\nname = 'ark-project'\n", encoding="utf-8"
96+
)
97+
(ws / "setup.cfg").write_text(
98+
"[metadata]\nname = awesome-lib\n", encoding="utf-8"
99+
)
100+
101+
filtered = [
102+
str(ws / "src" / "acme_pkg" / "__init__.py"),
103+
str(ws / "src" / "acme_pkg" / "main.py"),
104+
str(ws / "app" / "core" / "__init__.py"),
105+
]
106+
roots = da._collect_workspace_module_roots(filtered, str(ws))
107+
108+
assert "acme_pkg" in roots
109+
assert "app" in roots
110+
assert "ark_project" in roots
111+
assert "awesome_lib" in roots
112+
113+
114+
def test_classify_module_origin_internal_from_workspace_spec(
115+
tmp_path: Path, monkeypatch
116+
) -> None:
117+
ws = tmp_path / "workspace"
118+
(ws / "pkg").mkdir(parents=True)
119+
origin = str(ws / "pkg" / "__init__.py")
120+
(ws / "pkg" / "__init__.py").write_text("", encoding="utf-8")
121+
122+
monkeypatch.setattr(da, "_is_stdlib_module", lambda _name: False)
123+
monkeypatch.setattr(
124+
"importlib.util.find_spec",
125+
lambda _name: SimpleNamespace(origin=origin, submodule_search_locations=[]),
126+
)
127+
128+
assert da._classify_module_origin("pkg", str(ws)) == "internal"
129+
130+
131+
def test_classify_module_origin_third_party_from_site_packages(monkeypatch) -> None:
132+
monkeypatch.setattr(da, "_is_stdlib_module", lambda _name: False)
133+
monkeypatch.setattr(
134+
"importlib.util.find_spec",
135+
lambda _name: SimpleNamespace(
136+
origin="/tmp/venv/lib/python3.13/site-packages/requests/__init__.py",
137+
submodule_search_locations=[],
138+
),
139+
)
140+
141+
assert da._classify_module_origin("requests", "/tmp/workspace") == "third_party"

tests/test_engine_install_flow.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2026 Ague Samuel Amen
3+
4+
from __future__ import annotations
5+
6+
import sys
7+
import types
8+
from pathlib import Path
9+
10+
ROOT = Path(__file__).resolve().parents[1]
11+
if str(ROOT) not in sys.path:
12+
sys.path.insert(0, str(ROOT))
13+
14+
from EngineLoader.base import CompilerEngine
15+
16+
17+
class _LogCapture:
18+
def __init__(self) -> None:
19+
self.lines: list[str] = []
20+
21+
def append(self, text: str) -> None:
22+
self.lines.append(text)
23+
24+
def __bool__(self) -> bool:
25+
return True
26+
27+
28+
class _FakeProc:
29+
def __init__(self, wait_ok: bool = True, exit_code: int = 0) -> None:
30+
self._wait_ok = wait_ok
31+
self._exit_code = exit_code
32+
33+
def waitForFinished(self, _timeout_ms: int) -> bool:
34+
return self._wait_ok
35+
36+
def exitCode(self) -> int:
37+
return self._exit_code
38+
39+
40+
class _FakeVenvManager:
41+
def __init__(self, events: list[str], missing_python: set[str]) -> None:
42+
self._events = events
43+
self._missing_python = missing_python
44+
45+
def is_tool_installed_system(self, tool: str) -> bool:
46+
return tool not in self._missing_python
47+
48+
def ensure_tools_installed_system(self, tools: list[str]) -> None:
49+
self._events.append(f"python_install:{','.join(tools)}")
50+
51+
def resolve_project_venv(self) -> str | None:
52+
return "/tmp/fake-venv"
53+
54+
def is_tool_installed(self, _venv: str, tool: str) -> bool:
55+
return tool not in self._missing_python
56+
57+
def ensure_tools_installed(self, _venv: str, tools: list[str]) -> None:
58+
self._events.append(f"python_install:{','.join(tools)}")
59+
60+
61+
class _FakeGui:
62+
def __init__(self, events: list[str], missing_python: set[str]) -> None:
63+
self.log = _LogCapture()
64+
self.venv_manager = _FakeVenvManager(events, missing_python)
65+
self.use_system_python = True
66+
self.sys_deps_manager = None
67+
68+
def tr(self, fr: str, en: str) -> str:
69+
return en
70+
71+
72+
class _TestEngine(CompilerEngine):
73+
@property
74+
def required_tools(self) -> dict[str, list[str]]:
75+
return {"python": ["pyinstaller"], "system": ["gcc"]}
76+
77+
78+
def _install_fake_sysdeps(
79+
monkeypatch, events: list[str], *, wait_ok: bool = True, exit_code: int = 0
80+
) -> None:
81+
fake_mod = types.ModuleType("Core.sys_deps")
82+
83+
def check_system_packages(_tools: list[str]) -> bool:
84+
return False
85+
86+
class SysDependencyManager:
87+
def __init__(self, _gui) -> None:
88+
pass
89+
90+
def install_packages_linux(self, tools: list[str]):
91+
events.append(f"system_install:{','.join(tools)}")
92+
return _FakeProc(wait_ok=wait_ok, exit_code=exit_code)
93+
94+
fake_mod.check_system_packages = check_system_packages
95+
fake_mod.SysDependencyManager = SysDependencyManager
96+
monkeypatch.setitem(sys.modules, "Core.sys_deps", fake_mod)
97+
monkeypatch.setattr("platform.system", lambda: "Linux")
98+
99+
100+
def test_install_order_runs_system_before_python(monkeypatch) -> None:
101+
events: list[str] = []
102+
_install_fake_sysdeps(monkeypatch, events, wait_ok=True, exit_code=0)
103+
gui = _FakeGui(events, missing_python={"pyinstaller"})
104+
engine = _TestEngine()
105+
106+
ok = engine.ensure_tools_installed(gui)
107+
108+
assert ok is True
109+
assert events == ["system_install:gcc", "python_install:pyinstaller"]
110+
111+
112+
def test_python_install_still_runs_when_system_install_fails(monkeypatch) -> None:
113+
events: list[str] = []
114+
_install_fake_sysdeps(monkeypatch, events, wait_ok=True, exit_code=1)
115+
gui = _FakeGui(events, missing_python={"pyinstaller"})
116+
engine = _TestEngine()
117+
118+
ok = engine.ensure_tools_installed(gui)
119+
120+
assert ok is True
121+
assert events == ["system_install:gcc", "python_install:pyinstaller"]

0 commit comments

Comments
 (0)