Skip to content

Commit 78b129c

Browse files
committed
Implémentation de la comparaison des verrous par équivalence fonctionnelle
1 parent 4677822 commit 78b129c

4 files changed

Lines changed: 135 additions & 13 deletions

File tree

Core/Locking/__init__.py

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -254,13 +254,77 @@ def cache_rebuild_lock(workspace: Path, payload: dict[str, Any]) -> str:
254254
return str(target)
255255

256256

257-
def compare_lock_payloads(left: dict[str, Any], right: dict[str, Any]) -> bool:
258-
def comparable(payload: dict[str, Any]) -> dict[str, Any]:
259-
data = dict(payload)
260-
data.pop("build_id", None)
261-
return data
257+
def get_functional_snapshot(payload: dict[str, Any]) -> dict[str, Any]:
258+
"""
259+
Extract critical metadata for functional equivalence check.
260+
Ignores non-functional fields like build_id, git_branch, or resolved_command.
261+
"""
262+
if not isinstance(payload, dict):
263+
return {}
264+
265+
project = payload.get("project") or {}
266+
build = payload.get("build") or {}
267+
engine = payload.get("engine") or {}
268+
platform_info = payload.get("platform") or {}
269+
dependencies = payload.get("dependencies") or {}
270+
271+
return {
272+
"project": {
273+
"name": str(project.get("name") or ""),
274+
"version": str(project.get("version") or ""),
275+
"entry": str(project.get("entry") or ""),
276+
"git_commit": project.get("git_commit"),
277+
},
278+
"build": {
279+
"output": str(build.get("output") or ""),
280+
"data": list(build.get("data") or []),
281+
"exclude": list(build.get("exclude") or []),
282+
"icon": build.get("icon"),
283+
},
284+
"engine": {
285+
"name": str(engine.get("name") or ""),
286+
"version": str(engine.get("version") or ""),
287+
"config": engine.get("config") or {},
288+
},
289+
"platform": {
290+
"os": platform_info.get("os"),
291+
"arch": platform_info.get("arch"),
292+
"python_version": platform_info.get("python_version"),
293+
},
294+
"dependencies": dependencies,
295+
}
296+
297+
298+
def compare_lock_payloads(
299+
left: dict[str, Any], right: dict[str, Any], return_diff: bool = False
300+
) -> bool | tuple[bool, list[str]]:
301+
"""
302+
Compare two build lock payloads for functional equivalence.
303+
If return_diff is True, returns (is_equal, diff_list).
304+
"""
305+
snap_left = get_functional_snapshot(left)
306+
snap_right = get_functional_snapshot(right)
307+
308+
if not return_diff:
309+
return snap_left == snap_right
310+
311+
diffs = []
312+
313+
def _diff_dict(a: dict, b: dict, path: str):
314+
keys = set(a.keys()) | set(b.keys())
315+
for k in sorted(keys):
316+
val_a = a.get(k)
317+
val_b = b.get(k)
318+
cur_path = f"{path}.{k}" if path else k
319+
320+
if val_a != val_b:
321+
if isinstance(val_a, dict) and isinstance(val_b, dict):
322+
_diff_dict(val_a, val_b, cur_path)
323+
else:
324+
diffs.append(f"{cur_path}: {val_a} -> {val_b}")
262325

263-
return comparable(left) == comparable(right)
326+
_diff_dict(snap_left, snap_right, "")
327+
return (len(diffs) == 0, diffs)
264328

265329

266330
def default_lock_path(workspace: Path) -> Path:

Ui/Cli/app.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,9 +289,18 @@ def tr(self, fr, en):
289289
python_version=python_version,
290290
)
291291
rebuild_cache = cache_rebuild_lock(workspace, regenerated)
292-
comparison = compare_lock_payloads(lock_payload, regenerated)
293-
if not comparison:
292+
comparison_ok, diffs = compare_lock_payloads(lock_payload, regenerated, return_diff=True)
293+
comparison = comparison_ok
294+
if not comparison_ok:
294295
warnings.append("Lock mismatch")
296+
if not as_json and verbose:
297+
from .output import warn
298+
warn("Functional mismatch detected:")
299+
for d in diffs:
300+
info(f" - {d}")
301+
elif not as_json and verbose:
302+
from .output import success
303+
success("Lock integrity confirmed (Functional Equivalence).")
295304
except Exception as exc:
296305
warnings.append(f"Unable to regenerate comparison lock: {exc}")
297306

Ui/Gui/Dialogs/CompilerDialog.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,15 +1119,18 @@ def handle_finished(self, return_code: int, info: dict) -> None:
11191119
)
11201120

11211121
rebuild_cache = cache_rebuild_lock(ws, regenerated)
1122-
comparison_ok = compare_lock_payloads(self._rebuild_lock_payload, regenerated)
1122+
comparison_ok, diffs = compare_lock_payloads(self._rebuild_lock_payload, regenerated, return_diff=True)
11231123

11241124
if not comparison_ok:
11251125
log_i18n_level(
11261126
self,
11271127
"warning",
1128-
"⚠️ Mismatch détecté : le verrou actuel diffère de celui utilisé pour le rebuild.",
1129-
"⚠️ Lock mismatch detected: the current configuration differs from the one in the lock file.",
1128+
"⚠️ Mismatch fonctionnel détecté : le verrou actuel diffère de celui utilisé pour le rebuild.",
1129+
"⚠️ Functional mismatch detected: the current configuration differs from the one in the lock file.",
11301130
)
1131+
for d in diffs:
1132+
log_with_level(self, "info", f" - {d}")
1133+
11311134
if rebuild_cache:
11321135
log_i18n_level(
11331136
self,
@@ -1139,8 +1142,8 @@ def handle_finished(self, return_code: int, info: dict) -> None:
11391142
log_i18n_level(
11401143
self,
11411144
"success",
1142-
"✅ Intégrité du verrou confirmée (Strict Alignment).",
1143-
"✅ Lock integrity confirmed (Strict Alignment).",
1145+
"✅ Intégrité du verrou confirmée (Équivalence Fonctionnelle).",
1146+
"✅ Lock integrity confirmed (Functional Equivalence).",
11441147
)
11451148
except Exception as exc:
11461149
log_i18n_level(

tests/test_lock_comparison.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import pytest
2+
from Core.Locking import compare_lock_payloads
3+
4+
def test_compare_lock_payloads_functional_equivalence():
5+
"""Test that comparison ignores build_id but detects critical changes."""
6+
lock_a = {
7+
"build_id": "ARK_2026_05_31_001",
8+
"project": {"name": "App", "version": "1.0.0", "entry": "main.py"},
9+
"build": {"output": "dist/", "exclude": [], "data": []},
10+
"engine": {"name": "nuitka", "version": "2.0", "config": {"standalone": True}},
11+
"platform": {"os": "linux", "python_version": "3.12"},
12+
"dependencies": {"requests": "2.31.0"}
13+
}
14+
15+
# Same functional metadata, different build_id
16+
lock_b = dict(lock_a)
17+
lock_b["build_id"] = "ARK_2026_05_31_002"
18+
19+
assert compare_lock_payloads(lock_a, lock_b) is True
20+
21+
# Different dependency version
22+
lock_c = dict(lock_a)
23+
lock_c["dependencies"] = {"requests": "2.32.0"}
24+
assert compare_lock_payloads(lock_a, lock_c) is False
25+
26+
# Different engine config
27+
lock_d = dict(lock_a)
28+
lock_d["engine"] = dict(lock_a["engine"])
29+
lock_d["engine"]["config"] = {"standalone": False}
30+
assert compare_lock_payloads(lock_a, lock_d) is False
31+
32+
def test_compare_lock_payloads_with_diff():
33+
"""Test that we can get a list of differences."""
34+
lock_a = {
35+
"project": {"name": "App", "version": "1.0.0", "entry": "main.py"},
36+
"dependencies": {"numpy": "1.24.0"}
37+
}
38+
lock_b = {
39+
"project": {"name": "App", "version": "1.1.0", "entry": "main.py"},
40+
"dependencies": {"numpy": "1.25.0"}
41+
}
42+
43+
ok, diffs = compare_lock_payloads(lock_a, lock_b, return_diff=True)
44+
assert ok is False
45+
assert any("project.version: 1.0.0 -> 1.1.0" in d for d in diffs)
46+
assert any("dependencies.numpy: 1.24.0 -> 1.25.0" in d for d in diffs)

0 commit comments

Comments
 (0)