Files
lidar_rendu/lidar_pipeline/tests/test_webapp.py
Antoine Jacquin ed3e90ea89 Add visualization picker to zone generation and unify tile colors
- Web map: multi-select picker in the generation bar (aspect, slope,
  positive openness, anisotropic openness, wavelet) passed to the API;
  the layer panel is restricted to the same shortlist (PANEL_VIZ) and
  a refresh button rebuilds the index when new layers appear on disk;
  jobs started outside the UI are now adopted into the visible queue
- Uniform colors across tiles: openness/anisotropic/sailore now store
  local z-scores, and all renderers use fixed ranges (0-3 sigma, SVF
  0-1 physical, slope 0-30 deg) instead of per-tile percentile stretches
- Ray-tracing falls back to CPU when VRAM is exhausted so openness and
  SVF no longer fail silently on shared GPUs
- build_index merges visualizations available at only one resolution
  into the displayed tile so in-progress layers stay visible
- 11 new tests (142 passing)
2026-08-31 19:02:14 +02:00

176 lines
7.2 KiB
Python

"""Tests du serveur web de génération de zones (webapp)."""
def test_bbox_to_cells_single_km_cell():
"""Une bbox couvrant ~1 km² retourne la cellule L93 correspondante."""
from lidar_pipeline.webapp import bbox_to_cells
# Cellule 1054,6882 : X∈[1054000,1055000], Y∈[6881000,6882000] (L93)
from rasterio.warp import transform as warp_transform
lons, lats = warp_transform('EPSG:2154', 'EPSG:4326',
[1054100, 1054900], [6881100, 6881900])
cells = bbox_to_cells(min(lons), min(lats), max(lons), max(lats))
assert (1054, 6882) in cells
# La sélection reste locale : pas de cellule lointaine
for (c, r) in cells:
assert abs(c - 1054) <= 1 and abs(r - 6882) <= 1
def test_bbox_to_cells_empty_for_tiny_bbox():
"""Une bbox quasi ponctuelle ne sélectionne rien (rétrécie sous 1 m)."""
from lidar_pipeline.webapp import bbox_to_cells
assert bbox_to_cells(7.850000, 48.930000, 7.850001, 48.930001) == []
def test_processed_cells(tmp_path):
"""processed_cells lit les dossiers de visualisations."""
from lidar_pipeline.webapp import processed_cells
vis = tmp_path / "visualisations" / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69_r0p2"
vis.mkdir(parents=True)
(vis / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69_r0p2_aspect.avif").write_bytes(b"x")
assert processed_cells(tmp_path) == {(1054, 6882)}
def test_missing_cells_filters_processed(tmp_path):
"""Les cellules déjà traitées sont exclues, les autres gardent leurs coins."""
from lidar_pipeline.webapp import missing_cells_with_corners
vis = tmp_path / "visualisations" / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69"
vis.mkdir(parents=True)
(vis / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69_aspect.avif").write_bytes(b"x")
todo = missing_cells_with_corners([(1054, 6882), (1055, 6882)], tmp_path)
assert len(todo) == 1
assert todo[0]['col'] == 1055 and todo[0]['row'] == 6882
assert len(todo[0]['corners']) == 4 # SW, SE, NE, NW
def test_missing_cells_include_done(tmp_path):
"""include_done=True conserve les cellules déjà traitées (régénération)."""
from lidar_pipeline.webapp import missing_cells_with_corners
vis = tmp_path / "visualisations" / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69"
vis.mkdir(parents=True)
(vis / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69_aspect.avif").write_bytes(b"x")
todo = missing_cells_with_corners([(1054, 6882), (1055, 6882)], tmp_path,
include_done=True)
assert {(t['col'], t['row']) for t in todo} == {(1054, 6882), (1055, 6882)}
def test_build_command_regenerate():
"""regenerate=True ajoute --force --force-classification à la commande."""
from lidar_pipeline.webapp import _build_command
cmd = " ".join(_build_command([(1054, 6882)], regenerate=True))
assert "--force" in cmd
assert "--force-classification" in cmd
cmd = " ".join(_build_command([(1054, 6882)]))
assert "--force" not in cmd
assert "--force-classification" not in cmd
def test_build_command_ground_classification():
"""La commande utilise la méthode de classification demandée (défaut : ign)."""
from lidar_pipeline.webapp import _build_command, GROUND_CLASS_METHODS
# Défaut : ign (pré-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
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
def test_build_command_default_viz_aspect():
"""Sans choix de visualisation, la commande génère uniquement aspect."""
from lidar_pipeline.webapp import _build_command
cmd = _build_command([(1054, 6882)])
i = cmd.index("--only")
assert cmd[i + 1] == "aspect"
def test_build_command_custom_viz():
"""Les visualisations demandées sont passées à --only dans l'ordre."""
from lidar_pipeline.webapp import _build_command
cmd = _build_command([(1054, 6882)], viz=["aspect", "wavelet", "aniso_open"])
i = cmd.index("--only")
assert cmd[i + 1:i + 4] == ["aspect", "wavelet", "aniso_open"]
def test_viz_step_names_match_pipeline():
"""Les noms acceptés par l'API sont exactement les étapes VIZ_STEPS du pipeline."""
from lidar_pipeline.webapp import _viz_step_names
from lidar_pipeline.pipeline import VIZ_STEPS
assert _viz_step_names() == [n for n, _ in VIZ_STEPS]
def test_generate_rejects_unknown_viz():
"""L'API refuse un nom de visualisation inconnu (HTTPException 400)."""
from fastapi import HTTPException
from lidar_pipeline.webapp import generate, GenerateRequest
req = GenerateRequest(tiles=[[1054, 6882]], viz=["wavelet", "inconnu"])
try:
generate(req)
assert False, "une HTTPException était attendue"
except HTTPException as e:
assert e.status_code == 400
assert "inconnu" in e.detail
def test_generate_accepts_wavelet_and_aniso():
"""L'API accepte wavelet + aniso_open et les transmet à --only."""
import subprocess
import lidar_pipeline.webapp as webapp
captured = {}
def fake_popen(cmd, **kwargs):
captured["cmd"] = cmd
class _P:
def wait(self):
return 0
def poll(self):
return 0
return _P()
webapp.subprocess.Popen = fake_popen
try:
req = webapp.GenerateRequest(tiles=[[1054, 6882]],
viz=["wavelet", "aniso_open"])
result = webapp.generate(req)
assert result["demarré"] is True
i = captured["cmd"].index("--only")
assert captured["cmd"][i + 1:i + 3] == ["wavelet", "aniso_open"]
finally:
webapp.subprocess.Popen = subprocess.Popen
def test_available_layers_scans_disk(tmp_path, monkeypatch):
"""/api/layers retourne les couches présentes (clé → label)."""
import lidar_pipeline.webapp as webapp
monkeypatch.setattr(webapp, "OUTPUT_DIR", tmp_path)
vis = tmp_path / "visualisations" / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69"
vis.mkdir(parents=True)
(vis / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69_wavelet.avif").write_bytes(b"x")
layers = webapp.available_layers()
assert layers.get("wavelet")
assert "aspect" not in layers
def test_rebuild_index_background(tmp_path, monkeypatch):
"""POST /api/rebuild lance build_index en arrière-plan puis termine."""
import time as _time
import lidar_pipeline.webapp as webapp
import lidar_pipeline.index as index_mod
calls = []
monkeypatch.setattr(index_mod, "build_index", lambda out: calls.append(out))
assert webapp.rebuild_index()["demarré"] is True
for _ in range(100):
if not webapp.rebuild_status()["running"]:
break
_time.sleep(0.05)
assert calls # build_index a bien été appelé
assert webapp.rebuild_status()["running"] is False
assert webapp.rebuild_status()["done"] is not None