"""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