Rendre la carte autonome, télécharger la pyramide en fond, refermer les trous de zoom et borner les workers GPU par la VRAM
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@ -571,3 +571,119 @@ def test_unstored_level_rendered_on_the_fly_without_disk(tmp_path, monkeypatch):
|
||||
assert data and data[:4] == b"\x89PNG"
|
||||
assert not tiles.tile_cache_path(tmp_path, "aspect", z, x, y, 1, "png").exists()
|
||||
assert tiles.get_tile(tmp_path, "aspect", z, x, y, 1, "png") == data and len(calls) == 1
|
||||
|
||||
|
||||
def test_remote_payload_failure_not_retried_each_call(monkeypatch):
|
||||
"""Amont injoignable sans inventaire connu : l'échec est mémorisé (TTL).
|
||||
|
||||
Sinon chaque appel (plusieurs par /api/map/meta) repaie le délai de
|
||||
connexion : l'interface ne se chargeait plus quand le worker était éteint.
|
||||
"""
|
||||
import urllib.request
|
||||
from lidar_pipeline import tiles
|
||||
monkeypatch.setattr(tiles, "REMOTE_SOURCE_URL", "http://amont:8973")
|
||||
monkeypatch.setattr(tiles, "_remote_cache",
|
||||
{"payload": None, "at": 0.0, "index": None, "root": None})
|
||||
calls = []
|
||||
|
||||
def boom(*a, **k):
|
||||
calls.append(1)
|
||||
raise OSError("hôte injoignable")
|
||||
|
||||
monkeypatch.setattr(urllib.request, "urlopen", boom)
|
||||
for _ in range(3):
|
||||
assert tiles._remote_payload() is None
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_fetch_source_offline_breaker(tmp_path, monkeypatch):
|
||||
"""Source amont en échec : les suivantes échouent sans attendre le délai
|
||||
réseau pendant la suspension (la maintenance passe au rendu local)."""
|
||||
import urllib.request
|
||||
from lidar_pipeline import tiles
|
||||
monkeypatch.setattr(tiles, "_SOURCE_OFFLINE", {"until": 0.0})
|
||||
calls = []
|
||||
|
||||
def boom(*a, **k):
|
||||
calls.append(1)
|
||||
raise OSError("hôte injoignable")
|
||||
|
||||
monkeypatch.setattr(urllib.request, "urlopen", boom)
|
||||
assert tiles._fetch_source("http://amont/a", tmp_path / "a.avif") is False
|
||||
assert tiles._fetch_source("http://amont/b", tmp_path / "b.avif") is False
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_fetched_source_dated_to_upstream_version(tmp_path, monkeypatch):
|
||||
"""Une source rapatriée porte la date de version amont : quand l'amont
|
||||
s'éteint, l'index local retrouve les mêmes dates et les tuiles déjà
|
||||
faites restent fraîches (pas de pyramide entière à refaire)."""
|
||||
from lidar_pipeline import tiles
|
||||
|
||||
def fake_fetch(url, dest):
|
||||
dest.write_bytes(b"x")
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(tiles, "_fetch_source", fake_fetch)
|
||||
src = tiles._Source(tmp_path / "q.avif", (0, 0, 1, 1), 0.2,
|
||||
url="http://amont/q.avif", version=1700000000000)
|
||||
assert src.ensure() is True
|
||||
assert (tmp_path / "q.avif").stat().st_mtime == 1700000000.0
|
||||
|
||||
|
||||
def test_tile_refreshed_when_older_dalle_appears(tmp_path):
|
||||
"""Dalle entrée dans l'inventaire APRÈS le rendu d'une tuile qui la couvre,
|
||||
mais avec une date de version plus ancienne (écrite avant, inventoriée
|
||||
après) : la tuile doit être périmée — sinon trou permanent à ce niveau,
|
||||
sur le disque, en mémoire et dans le navigateur (stamp inchangé)."""
|
||||
import io
|
||||
import os
|
||||
from PIL import Image
|
||||
from lidar_pipeline import tiles
|
||||
tiles.clear_source_cache()
|
||||
tiles._mem_tiles.clear()
|
||||
z = 10
|
||||
assert _tile_of_cell(1054, 6882, z) == _tile_of_cell(1055, 6882, z)
|
||||
x, y = _tile_of_cell(1054, 6882, z)
|
||||
_make_dalle(tmp_path, 1054, 6882, ["slope"], color=(200, 30, 30))
|
||||
tiles.source_index(tmp_path, force=True)
|
||||
first = tiles.get_tile(tmp_path, "slope", z, x, y)
|
||||
assert first is not None
|
||||
stamp = tiles.tiles_stamp(tmp_path)
|
||||
|
||||
_make_dalle(tmp_path, 1055, 6882, ["slope"], color=(30, 200, 30))
|
||||
for f in tmp_path.rglob("*1055_6882*"):
|
||||
os.utime(f, (1_000_000, 1_000_000)) # version antérieure à la tuile
|
||||
tiles.source_index(tmp_path, force=True)
|
||||
assert tiles.cached_tile(tmp_path, "slope", z, x, y)[1] == "pending"
|
||||
second = tiles.get_tile(tmp_path, "slope", z, x, y)
|
||||
img = Image.open(io.BytesIO(second)).convert("RGBA")
|
||||
assert any(g > 150 and r < 80 and a == 255
|
||||
for r, g, b, a in img.getdata()), "nouvelle dalle absente de la tuile"
|
||||
assert tiles.tiles_stamp(tmp_path) > stamp
|
||||
# Registre persistant : un redémarrage ne réinvalide rien
|
||||
tiles._index_cache.clear()
|
||||
tiles.source_index(tmp_path, force=True)
|
||||
assert tiles.cached_tile(tmp_path, "slope", z, x, y)[1] == "fresh"
|
||||
tiles.clear_source_cache()
|
||||
|
||||
|
||||
def test_existing_cache_without_registry_is_refreshed_once(tmp_path):
|
||||
"""Mise à jour : cache de tuiles présent mais pas de registre — les
|
||||
tuiles existantes (peut-être trouées) sont périmées une seule fois."""
|
||||
from lidar_pipeline import tiles
|
||||
tiles._seen_cache.clear()
|
||||
tiles._mem_tiles.clear()
|
||||
z = 10
|
||||
x, y = _tile_of_cell(1054, 6882, z)
|
||||
_make_dalle(tmp_path, 1054, 6882, ["slope"])
|
||||
tiles.source_index(tmp_path, force=True)
|
||||
assert tiles.get_tile(tmp_path, "slope", z, x, y) is not None
|
||||
(tmp_path / tiles.TILE_DIRNAME / tiles._SEEN_FILE).unlink() # version précédente
|
||||
tiles._seen_cache.clear()
|
||||
tiles.source_index(tmp_path, force=True)
|
||||
assert tiles.cached_tile(tmp_path, "slope", z, x, y)[1] == "pending"
|
||||
tiles.get_tile(tmp_path, "slope", z, x, y)
|
||||
tiles._seen_cache.clear()
|
||||
tiles.source_index(tmp_path, force=True)
|
||||
assert tiles.cached_tile(tmp_path, "slope", z, x, y)[1] == "fresh"
|
||||
|
||||
Reference in New Issue
Block a user