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