Spaces:
Paused
Paused
Download tests/test_api.py from Cnass/sprite: direct link, hf CLI and curl.
- Browser
- Download file 8.71 kB
-
https://huggingface.co/spaces/Cnass/sprite/resolve/main/tests/test_api.py
- Command line
-
hf download hf://spaces/Cnass/sprite/tests/test_api.py
-
curl -L -o test_api.py https://huggingface.co/spaces/Cnass/sprite/resolve/main/tests/test_api.py
8.71 kB
| """ | |
| tests/test_api.py | |
| ------------------ | |
| HTTP-level tests for the endpoints that carry real logic rather than | |
| being a one-line pass-through: bulk metadata assignment, duplicate | |
| collapsing, and the animation frame layout. | |
| The DB path is redirected to a throwaway file BEFORE `app` is imported, | |
| so these never touch the Space's real data. | |
| Run with: pytest tests/test_api.py -v | |
| """ | |
| import importlib | |
| import sys | |
| import tempfile | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) | |
| _TMP = tempfile.TemporaryDirectory() | |
| import config # noqa: E402 | |
| config.DB_PATH = Path(_TMP.name) / "api-test.sqlite3" | |
| import database as db # noqa: E402 | |
| importlib.reload(db) | |
| import app as flask_app # noqa: E402 | |
| def reset_db(): | |
| if config.DB_PATH.exists(): | |
| config.DB_PATH.unlink() | |
| for suffix in ("-wal", "-shm"): | |
| extra = Path(str(config.DB_PATH) + suffix) | |
| if extra.exists(): | |
| extra.unlink() | |
| db.init_db() | |
| def client(): | |
| flask_app.app.config["TESTING"] = True | |
| return flask_app.app.test_client() | |
| def make_sprite(sprite_id, sheet_id="sheet_a", **kwargs): | |
| db.insert_sprite( | |
| db.SpriteRecord( | |
| id=sprite_id, | |
| sheet_id=sheet_id, | |
| source_file=f"{sheet_id}.png", | |
| sprite_index=kwargs.pop("sprite_index", 1), | |
| x=kwargs.pop("x", 0), y=kwargs.pop("y", 0), | |
| width=kwargs.pop("width", 16), height=kwargs.pop("height", 16), | |
| png_path=f"/tmp/{sprite_id}.png", | |
| png_path_transparent=None, | |
| sha256=sprite_id, phash="00", | |
| quality={"overall_quality_score": kwargs.pop("quality", 80.0)}, | |
| confidence=kwargs.pop("confidence", 95.0), | |
| confidence_reasons=[], | |
| component_count=1, | |
| category=kwargs.pop("category", "unknown"), | |
| style="pixel_art", | |
| tags=kwargs.pop("tags", []), | |
| status=kwargs.pop("status", "pending"), | |
| is_duplicate_of=kwargs.pop("is_duplicate_of", None), | |
| duplicate_similarity=kwargs.pop("duplicate_similarity", None), | |
| auto_tagged=kwargs.pop("auto_tagged", False), | |
| ) | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Bulk assignment | |
| # --------------------------------------------------------------------------- | |
| def test_bulk_assign_scoped_to_one_sheet_leaves_others_alone(): | |
| reset_db() | |
| make_sprite("a", sheet_id="s1") | |
| make_sprite("b", sheet_id="s1") | |
| make_sprite("c", sheet_id="s2") | |
| res = client().post("/api/batch/assign", json={ | |
| "scope": "sheet", "sheet_id": "s1", "category": "tree", "add_tags": ["forest"], | |
| }) | |
| assert res.status_code == 200 | |
| assert res.get_json()["updated_count"] == 2 | |
| assert db.get_sprite("a")["category"] == "tree" | |
| assert db.get_sprite("c")["category"] == "unknown" | |
| def test_bulk_assign_registers_a_brand_new_category(): | |
| """ | |
| Typing a category that doesn't exist yet must create it, otherwise the | |
| dropdown can never gain anything the built-in list didn't ship with. | |
| """ | |
| reset_db() | |
| make_sprite("a", sheet_id="s1") | |
| client().post("/api/batch/assign", json={ | |
| "scope": "sheet", "sheet_id": "s1", "category": "Siege Weapon", | |
| }) | |
| assert "siege_weapon" in db.list_categories("category") | |
| assert db.get_sprite("a")["category"] == "siege_weapon" | |
| def test_bulk_assign_rejects_an_unknown_scope(): | |
| reset_db() | |
| res = client().post("/api/batch/assign", json={"scope": "everything", "category": "x"}) | |
| assert res.status_code == 400 | |
| def test_bulk_assign_with_nothing_to_change_is_a_no_op(): | |
| reset_db() | |
| make_sprite("a", sheet_id="s1", tags=["keep"]) | |
| res = client().post("/api/batch/assign", json={"scope": "sheet", "sheet_id": "s1"}) | |
| assert res.status_code == 200 | |
| assert res.get_json()["updated_count"] == 0 | |
| # --------------------------------------------------------------------------- | |
| # Duplicate collapsing | |
| # --------------------------------------------------------------------------- | |
| def test_dedupe_keeps_the_best_member_of_each_cluster(): | |
| reset_db() | |
| make_sprite("low", quality=40, is_duplicate_of=None) | |
| make_sprite("high", quality=95, is_duplicate_of="low") | |
| make_sprite("mid", quality=70, is_duplicate_of="high") | |
| res = client().post("/api/batch/dedupe-keep-one").get_json() | |
| assert res["cluster_count"] == 1 | |
| assert res["kept_ids"] == ["high"] | |
| assert set(res["rejected_ids"]) == {"low", "mid"} | |
| assert db.get_sprite("high")["status"] == "pending" | |
| assert db.get_sprite("low")["status"] == "rejected" | |
| def test_dedupe_clears_the_flag_on_the_survivor(): | |
| """ | |
| The kept copy is the only one now, so leaving it flagged would keep it | |
| out of batch auto-accept for no reason. | |
| """ | |
| reset_db() | |
| make_sprite("first", quality=90) | |
| make_sprite("second", quality=50, is_duplicate_of="first") | |
| client().post("/api/batch/dedupe-keep-one") | |
| assert db.get_sprite("first")["is_duplicate_of"] is None | |
| def test_dedupe_does_nothing_without_duplicates(): | |
| reset_db() | |
| make_sprite("a") | |
| make_sprite("b") | |
| res = client().post("/api/batch/dedupe-keep-one").get_json() | |
| assert res["cluster_count"] == 0 | |
| assert res["rejected_count"] == 0 | |
| # --------------------------------------------------------------------------- | |
| # Animation frames | |
| # --------------------------------------------------------------------------- | |
| def test_animation_frames_are_aligned_to_a_shared_canvas(): | |
| """ | |
| Every sprite is cropped tight to its own content, so playing the crops | |
| back-to-back makes the animation wobble. Frames therefore come back | |
| with offsets into one canvas covering all of them. | |
| """ | |
| reset_db() | |
| make_sprite("f1", sheet_id="anim", sprite_index=1, x=10, y=20, width=8, height=8) | |
| make_sprite("f2", sheet_id="anim", sprite_index=2, x=40, y=20, width=12, height=16) | |
| data = client().get("/api/animation/frames?sheet_id=anim").get_json() | |
| assert [f["id"] for f in data["frames"]] == ["f1", "f2"] | |
| # Origin is the top-left of the union, so the first frame sits at 0,0. | |
| assert (data["frames"][0]["offset_x"], data["frames"][0]["offset_y"]) == (0, 0) | |
| assert (data["frames"][1]["offset_x"], data["frames"][1]["offset_y"]) == (30, 0) | |
| assert data["canvas"] == {"width": 42, "height": 16} | |
| def test_animation_frames_are_ordered_by_sprite_index(): | |
| reset_db() | |
| make_sprite("c", sheet_id="anim", sprite_index=3) | |
| make_sprite("a", sheet_id="anim", sprite_index=1) | |
| make_sprite("b", sheet_id="anim", sprite_index=2) | |
| data = client().get("/api/animation/frames?sheet_id=anim").get_json() | |
| assert [f["sprite_index"] for f in data["frames"]] == [1, 2, 3] | |
| def test_animation_skips_rejected_frames(): | |
| reset_db() | |
| make_sprite("keep", sheet_id="anim", sprite_index=1) | |
| make_sprite("gone", sheet_id="anim", sprite_index=2, status="rejected") | |
| data = client().get("/api/animation/frames?sheet_id=anim").get_json() | |
| assert [f["id"] for f in data["frames"]] == ["keep"] | |
| def test_animation_requires_a_sheet_id(): | |
| reset_db() | |
| assert client().get("/api/animation/frames").status_code == 400 | |
| def test_animation_on_an_empty_sheet_returns_no_frames(): | |
| reset_db() | |
| data = client().get("/api/animation/frames?sheet_id=nothing").get_json() | |
| assert data["frames"] == [] | |
| # --------------------------------------------------------------------------- | |
| # Reports | |
| # --------------------------------------------------------------------------- | |
| def test_report_endpoints_respond(): | |
| reset_db() | |
| make_sprite("a", status="accepted") | |
| c = client() | |
| for url in ("/api/dataset/balance", "/api/dataset/calibration", "/api/dataset/split-preview"): | |
| assert c.get(url).status_code == 200, url | |
| def test_split_preview_warns_when_a_partition_is_empty(): | |
| reset_db() | |
| for i in range(5): | |
| make_sprite(f"a{i}", sheet_id="one_sheet", sprite_index=i, status="accepted") | |
| data = client().get("/api/dataset/split-preview").get_json() | |
| assert data["total"] == 5 | |
| assert data["grouped_by_sheet"] is True | |
| assert data["warnings"], "a single sheet cannot fill three partitions" | |
| if __name__ == "__main__": | |
| import traceback | |
| tests = [(n, f) for n, f in list(globals().items()) if n.startswith("test_") and callable(f)] | |
| passed, failed = 0, 0 | |
| for name, fn in tests: | |
| try: | |
| fn() | |
| print(f"PASS {name}") | |
| passed += 1 | |
| except Exception: | |
| print(f"FAIL {name}") | |
| traceback.print_exc() | |
| failed += 1 | |
| print(f"\n{passed} passed, {failed} failed") | |
| sys.exit(1 if failed else 0) | |