"""Chaîne complète sur un petit voyage fabriqué à la volée.
Sources « mal rangées » exprès : une trace de deux jours au nom quelconque,
des photos dans un autre dossier (dont une HEIC et une sans GPS), une vidéo.
"""
import os
import shutil
import subprocess
from datetime import datetime, timedelta, timezone
import pytest
from rtg import config, site, sources
pytestmark = pytest.mark.skipif(not (shutil.which("exiftool") and shutil.which("ffmpeg")),
reason="exiftool et ffmpeg nécessaires")
UTC = timezone.utc
def _gpx(chemin):
"""Deux journées (Paris, UTC+2 en mai) ; un point à 00 h 30 locale doit
rester sur la veille (coupure à 4 h)."""
pts = []
t0 = datetime(2026, 5, 1, 6, 0, tzinfo=UTC) # 8 h locale
for i in range(60):
pts.append((48.80 + i * 0.002, 2.30 + i * 0.003, t0 + timedelta(minutes=2 * i)))
pts.append((48.95, 2.50, datetime(2026, 5, 1, 22, 30, tzinfo=UTC))) # 00 h 30 le 2 mai
t1 = datetime(2026, 5, 2, 7, 0, tzinfo=UTC)
for i in range(40):
pts.append((49.00 + i * 0.002, 2.60 + i * 0.002, t1 + timedelta(minutes=3 * i)))
seg = "\n".join(f''
for la, lo, t in pts)
with open(chemin, "w") as f:
f.write(f'test{seg}'
f'')
def _photo(chemin, date_locale, gps=None):
from PIL import Image
im = Image.new("RGB", (3000, 2000), (90, 140, 200))
im.save(chemin, "JPEG")
args = ["exiftool", "-q", "-overwrite_original", f"-DateTimeOriginal={date_locale}"]
if gps:
args += [f"-GPSLatitude={gps[0]}", "-GPSLatitudeRef=N", f"-GPSLongitude={gps[1]}", "-GPSLongitudeRef=E"]
subprocess.run(args + [chemin], check=True)
def _heic(chemin, jpeg):
from PIL import Image
import pillow_heif
pillow_heif.register_heif_opener()
with Image.open(jpeg) as im:
im.save(chemin, format="HEIF", exif=im.info.get("exif"))
@pytest.fixture
def site_test(tmp_path):
racine = tmp_path / "site"
voyage = racine / "Balades" / "Test-05-2026"
sources_dir = tmp_path / "carte_sd"
for d in (voyage, sources_dir / "photos", sources_dir / "gopro"):
d.mkdir(parents=True)
_gpx(sources_dir / "export_68degres_quelconque.gpx")
_photo(str(sources_dir / "photos" / "IMG_0001.jpg"), "2026:05:01 09:30:00", gps=(48.85, 2.35))
# Sans GPS, prise à 9 h 00 locale le 1er mai = 7 h 00 UTC : 30e point de la trace
_photo(str(sources_dir / "photos" / "IMG_0002.jpg"), "2026:05:01 09:00:00")
_photo(str(tmp_path / "base.jpg"), "2026:05:02 10:00:00", gps=(49.01, 2.61))
_heic(str(sources_dir / "photos" / "IMG_0003.HEIC"), str(tmp_path / "base.jpg"))
subprocess.run(["ffmpeg", "-loglevel", "error", "-f", "lavfi", "-i", "testsrc=size=1280x720:rate=30",
"-f", "lavfi", "-i", "sine=frequency=440", "-t", "2", "-c:v", "libx264", "-c:a", "aac",
"-shortest", str(sources_dir / "gopro" / "GX010001.MP4")], check=True)
config.ecrire_toml(str(racine / "site.toml"), {"titre": "Site de test",
"video": {"preset": "ultrafast"}})
config.ecrire_toml(str(voyage / "voyage.toml"), {
"titre": "Test", "moto": "Moto d'essai",
"sources": {"traces": [str(sources_dir / "export_68degres_quelconque.gpx")],
"videos": [str(sources_dir / "gopro")],
"photos": [str(sources_dir / "photos")]}})
return racine, voyage
def test_decoupage_et_coupure(site_test):
racine, voyage = site_test
cfg = config.reglages_voyage(str(voyage), config.reglages_site(str(racine)))
a = sources.analyser(str(voyage), cfg)
assert sorted(j.isoformat() for j in a.journees) == ["2026-05-01", "2026-05-02"]
# Le point de 00 h 30 (heure de Paris) appartient au 1er mai
derniers = [p for t in a.journees[min(a.journees)] for s in t.segments for p in s.points]
assert derniers[-1].latitude == 48.95
def test_chaine_complete(site_test):
racine, voyage = site_test
assert site.generer(str(racine))
# Traces rangées par jour
assert sorted(os.listdir(voyage / "routes")) == ["2026-05-01.gpx", "2026-05-02.gpx"]
# Photos copiées, HEIC convertie en JPEG
assert sorted(os.listdir(voyage / "photos")) == ["IMG_0001.jpg", "IMG_0002.jpg", "IMG_0003.jpeg"]
csv = (voyage / "export_photos.csv").read_text()
assert "IMG_0001.jpg" in csv and "IMG_0003.jpeg" in csv
ligne2 = [l for l in csv.splitlines() if "IMG_0002" in l][0]
lat = float(ligne2.split(",")[2])
assert abs(lat - (48.80 + 30 * 0.002)) < 0.003 # position déduite de la trace
# Vidéo compressée en deux paliers lisibles
assert (voyage / "videos" / "GX010001.mp4").exists()
assert (voyage / "videos" / "GX010001_mobile.mp4").exists()
# Pages
assert sorted(os.listdir(voyage / "html")) == ["carte_2026-05-01.html", "carte_2026-05-02.html"]
assert (voyage / "index.html").exists() and (voyage / "photos_web" / "IMG_0001.jpg").exists()
accueil = (racine / "index.html").read_text()
assert "Site de test" in accueil and "Balades/Test-05-2026/index.html" in accueil
def test_deuxieme_passage_ne_refait_rien(site_test, capsys):
racine, voyage = site_test
site.generer(str(racine))
capsys.readouterr()
site.generer(str(racine))
sortie = capsys.readouterr().out
assert "Aucun nouvel encodage" in sortie
assert "0 réduite(s)" in sortie