Arrêt de génération et reclassification optionnelle depuis l'interface

- POST /api/stop : SIGTERM au pipeline (groupe isolé via start_new_session),
  SIGKILL du groupe après 15 s ; forwardé au worker en mode deux machines
- « Régénérer » n'implique plus la reclassification : le DTM en cache est
  réutilisé tant que la méthode ne change pas ; nouvelle case « Reclasser le
  sol » (--force-classification) et rappel de la classification existante
- Tâche marquée « Interrompu » (returncode négatif ou nul) au lieu d'Échec
This commit is contained in:
Antoine Jacquin
2026-09-12 20:27:52 +02:00
parent bde2025714
commit 8e55fc5f98
3 changed files with 188 additions and 22 deletions

View File

@ -222,10 +222,13 @@ def test_generate_all_missing_empty(tmp_path, monkeypatch):
def test_build_command_regenerate():
"""regenerate=True ajoute --force --force-classification à la commande."""
"""regenerate --force seul (classification conservée) ; reclassify ajoute --force-classification."""
from lidar_pipeline.webapp import _build_command
cmd = " ".join(_build_command([(1054, 6882)], regenerate=True))
assert "--force" in cmd
assert "--force-classification" not in cmd
cmd = " ".join(_build_command([(1054, 6882)], regenerate=True, reclassify=True))
assert "--force" in cmd
assert "--force-classification" in cmd
cmd = " ".join(_build_command([(1054, 6882)]))
assert "--force" not in cmd
@ -239,14 +242,56 @@ def test_build_command_ground_classification():
cmd = _build_command([(1054, 6882)])
i = cmd.index("--ground-classification")
assert cmd[i + 1] == "ign"
# Chaque méthode valide est transmise telle quelle, avec ou sans régénération
# Chaque méthode valide est transmise telle quelle ; --force suit
# regenerate, --force-classification suit reclassify (indépendants)
for method in GROUND_CLASS_METHODS:
for regenerate in (False, True):
cmd = _build_command([(1054, 6882)], regenerate=regenerate, ground_class=method)
i = cmd.index("--ground-classification")
assert cmd[i + 1] == method
assert ("--force" in cmd) == regenerate
assert ("--force-classification" in cmd) == regenerate
for reclassify in (False, True):
cmd = _build_command([(1054, 6882)], regenerate=regenerate,
ground_class=method, reclassify=reclassify)
i = cmd.index("--ground-classification")
assert cmd[i + 1] == method
assert ("--force" in cmd) == regenerate
assert ("--force-classification" in cmd) == reclassify
def test_stop_generation(monkeypatch):
"""/api/stop termine le processus en cours (SIGTERM) ou refuse (409)."""
import time
from fastapi import HTTPException
import lidar_pipeline.webapp as webapp
terminated = []
class FakeProc:
pid = 4242
def poll(self):
return None # en cours
def terminate(self):
terminated.append("SIGTERM")
def wait(self, timeout=None):
terminated.append("wait")
saved = {k: webapp._job.get(k) for k in ("proc", "started", "returncode", "cmd", "finished")}
try:
webapp._job.update({"proc": FakeProc(), "started": time.time(),
"returncode": None, "cmd": ["x"], "finished": None})
assert webapp.stop_generation() == {"arrêt": "demandé"}
assert "SIGTERM" in terminated
# Le fil d'escalade peut mettre un instant avant son wait : laisser
# la file de threads vider avant de restaurer l'état du job.
for _ in range(50):
if "wait" in terminated:
break
time.sleep(0.02)
# Sans job en cours : refus propre
webapp._job.update({"proc": None})
try:
webapp.stop_generation()
assert False, "une HTTPException était attendue"
except HTTPException as e:
assert e.status_code == 409
finally:
webapp._job.update(saved)
def test_build_command_default_viz_aspect():
@ -574,6 +619,7 @@ def test_generate_forwards_to_remote(monkeypatch):
assert result["demarré"] is True and result["distant"] is True
assert calls == [("POST", "/api/generate",
{"tiles": [[1054, 6882]], "regenerate": False,
"reclassify": False,
"ground_class": "ign", "ign_classes": "sol",
"bare_earth": False, "viz": ["aspect"],
"all_missing": False})]