Spaces:
Running
Running
Download tests/test_sky_sync.py from RDS777/TARGET_SPACE: direct link, hf CLI and curl.
- Browser
- Download file 7.26 kB
-
https://huggingface.co/spaces/RDS777/TARGET_SPACE/resolve/main/tests/test_sky_sync.py
- Command line
-
hf download hf://spaces/RDS777/TARGET_SPACE/tests/test_sky_sync.py
-
curl -L -o test_sky_sync.py https://huggingface.co/spaces/RDS777/TARGET_SPACE/resolve/main/tests/test_sky_sync.py
7.26 kB
| """Synchronisation de la file sky-trajectories avec le Hub : envoi, archivage, échecs sans perte, secrets protégés.""" | |
| import json | |
| import pytest | |
| from fastapi import FastAPI | |
| from fastapi.testclient import TestClient | |
| from mouh.geometry import sync | |
| from mouh.geometry.api import make_router | |
| from mouh.geometry.mesh import STAGING_NAME, export_trajectory | |
| CUBE_V = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0), (0, 0, 1), (1, 0, 1), (1, 1, 1), (0, 1, 1)] | |
| CUBE_F = [(0, 2, 1), (0, 3, 2), (4, 5, 6), (4, 6, 7), (0, 1, 5), (0, 5, 4), | |
| (3, 6, 2), (3, 7, 6), (0, 4, 7), (0, 7, 3), (1, 2, 6), (1, 6, 5)] | |
| HEADERS = {"x-admin-key": "secret"} | |
| SECRET = "hf_TEST_SECRET_NE_PAS_AFFICHER" | |
| class FakeApi: | |
| def __init__(self, fail: Exception | None = None): | |
| self.fail = fail | |
| self.created = [] | |
| self.uploads = [] | |
| def create_repo(self, **kwargs): | |
| self.created.append(kwargs) | |
| def upload_file(self, **kwargs): | |
| if self.fail is not None: | |
| raise self.fail | |
| self.uploads.append(kwargs) | |
| def queue_with_one_geometry(tmp_path): | |
| queue = tmp_path / "sky-trajectories" | |
| rep = export_trajectory({"vertices": CUBE_V, "faces": CUBE_F, "instruction": "Cube unitaire"}, queue) | |
| assert rep.ok and rep.stats["written"] | |
| return queue | |
| def test_empty_queue_sends_nothing(tmp_path): | |
| api = FakeApi() | |
| result = sync.push_pending(tmp_path / "vide", api, "RDS777/sky-trajectories") | |
| assert result["sent"] == 0 and api.uploads == [] and api.created == [] | |
| def test_successful_push_creates_a_private_dataset_and_archives_the_lines(tmp_path): | |
| queue = queue_with_one_geometry(tmp_path) | |
| api = FakeApi() | |
| result = sync.push_pending(queue, api, "RDS777/sky-trajectories") | |
| assert result["sent"] == 1 and result["pending"] == 0 | |
| assert api.created[0]["private"] is True and api.created[0]["repo_type"] == "dataset" | |
| assert api.created[0]["exist_ok"] is True | |
| upload = api.uploads[0] | |
| assert upload["path_in_repo"].startswith("geometry/") and upload["path_in_repo"].endswith(".jsonl") | |
| sent_line = json.loads(upload["path_or_fileobj"].decode("utf-8").splitlines()[0]) | |
| assert sent_line["instruction"] == "Cube unitaire" and len(sent_line["id"]) == 64 | |
| assert (queue / STAGING_NAME).read_text(encoding="utf-8") == "" | |
| assert sync.sent_batches(queue) == 1 | |
| def test_a_failed_upload_leaves_the_queue_untouched(tmp_path): | |
| queue = queue_with_one_geometry(tmp_path) | |
| before = (queue / STAGING_NAME).read_text(encoding="utf-8") | |
| with pytest.raises(RuntimeError): | |
| sync.push_pending(queue, FakeApi(fail=RuntimeError("réseau coupé")), "RDS777/sky-trajectories") | |
| assert (queue / STAGING_NAME).read_text(encoding="utf-8") == before | |
| assert sync.sent_batches(queue) == 0 | |
| def test_dataset_name_comes_from_the_environment_with_a_default(monkeypatch): | |
| monkeypatch.delenv("SKY_DATASET_REPO", raising=False) | |
| assert sync.dataset_repo() == "RDS777/sky-trajectories" | |
| monkeypatch.setenv("SKY_DATASET_REPO", "autre/depot") | |
| assert sync.dataset_repo() == "autre/depot" | |
| def make_client(tmp_path, admin="secret", sync_fn=None): | |
| app = FastAPI() | |
| app.include_router(make_router(tmp_path / "q", lambda key: key == admin, sync=sync_fn)) | |
| return TestClient(app) | |
| def test_sync_route_requires_the_admin_key(tmp_path): | |
| client = make_client(tmp_path, sync_fn=lambda: {"sent": 0}) | |
| assert client.post("/api/symbioza/sync").status_code == 401 | |
| assert client.post("/api/symbioza/sync", headers={"x-admin-key": "faux"}).status_code == 401 | |
| def test_sync_route_reports_503_when_no_token_is_configured(tmp_path): | |
| def unavailable(): | |
| raise sync.SyncUnavailable("HF_TOKEN absent : synchronisation désactivée") | |
| client = make_client(tmp_path, sync_fn=unavailable) | |
| r = client.post("/api/symbioza/sync", headers=HEADERS) | |
| assert r.status_code == 503 and "HF_TOKEN absent" in r.json()["error"] | |
| def test_sync_route_never_echoes_a_secret_from_an_upstream_error(tmp_path): | |
| def leaky(): | |
| raise RuntimeError(f"401 Unauthorized pour {SECRET}") | |
| client = make_client(tmp_path, sync_fn=leaky) | |
| r = client.post("/api/symbioza/sync", headers=HEADERS) | |
| assert r.status_code == 502 | |
| assert SECRET not in r.text | |
| assert r.json()["type"] == "RuntimeError" | |
| def test_sync_route_without_a_configured_callable_is_503(tmp_path): | |
| client = make_client(tmp_path, sync_fn=None) | |
| assert client.post("/api/symbioza/sync", headers=HEADERS).status_code == 503 | |
| def test_status_route_counts_without_exposing_content(tmp_path): | |
| client = make_client(tmp_path) | |
| r = client.get("/api/symbioza/status") | |
| assert r.status_code == 200 and r.json() == {"pending": 0, "sent_batches": 0} | |
| def scaled_cube(k): | |
| return [(x * k, y * k, z * k) for x, y, z in CUBE_V] | |
| def export_named(queue, k, name): | |
| rep = export_trajectory({"vertices": scaled_cube(k), "faces": CUBE_F, "instruction": name}, queue) | |
| assert rep.ok and rep.stats["written"] | |
| return rep | |
| def test_geometries_route_is_empty_on_an_empty_queue(tmp_path): | |
| client = make_client(tmp_path) | |
| assert client.get("/api/symbioza/geometries").json() == {"geometries": []} | |
| def test_geometries_route_lists_newest_first_within_the_limit(tmp_path): | |
| queue = tmp_path / "q" | |
| for k, name in ((1, "A"), (2, "B"), (3, "C")): | |
| export_named(queue, k, name) | |
| client = make_client(tmp_path) | |
| got = client.get("/api/symbioza/geometries", params={"limit": 2}).json()["geometries"] | |
| assert [g["instruction"] for g in got] == ["C", "B"] | |
| assert got[0]["checks"]["self_intersections"] == 0 and len(got[0]["vertices"]) == 8 | |
| def test_geometries_keep_listing_after_a_sync_from_the_archive(tmp_path): | |
| queue = tmp_path / "q" | |
| export_named(queue, 1, "A") | |
| sync.push_pending(queue, FakeApi(), "RDS777/sky-trajectories") | |
| export_named(queue, 2, "B") | |
| assert [g["instruction"] for g in sync.recent_entries(queue)] == ["B", "A"] | |
| def test_recent_entries_ignores_unreadable_lines(tmp_path): | |
| queue = tmp_path / "q" | |
| export_named(queue, 1, "A") | |
| with (queue / STAGING_NAME).open("a", encoding="utf-8") as fh: | |
| fh.write("ceci n'est pas du JSON\n") | |
| assert [g["instruction"] for g in sync.recent_entries(queue)] == ["A"] | |
| def test_recent_entries_stops_at_the_face_budget(tmp_path): | |
| queue = tmp_path / "q" | |
| for k, name in ((1, "A"), (2, "B")): | |
| export_named(queue, k, name) | |
| # Chaque cube compte 12 faces : un budget de 12 n'en laisse passer qu'une seule. | |
| assert [g["instruction"] for g in sync.recent_entries(queue, limit=5, face_budget=12)] == ["B"] | |
| # Un budget inférieur au plus petit maillage ne laisse rien passer. | |
| assert sync.recent_entries(queue, limit=5, face_budget=11) == [] | |
| def test_geometries_route_caps_the_limit(tmp_path): | |
| queue = tmp_path / "q" | |
| for k in range(1, 6): | |
| export_named(queue, k, f"G{k}") | |
| client = make_client(tmp_path) | |
| assert len(client.get("/api/symbioza/geometries", params={"limit": 3}).json()["geometries"]) == 3 | |
| assert len(client.get("/api/symbioza/geometries", params={"limit": 0}).json()["geometries"]) == 1 | |
| assert len(client.get("/api/symbioza/geometries", params={"limit": 999}).json()["geometries"]) == 5 | |