"""Compatibility table: variant clustering, the divergence asterisk, renderers, and /compat.""" from __future__ import annotations import json import pytest from ams.compat import ( Entry, compatibility_table, render_json, render_markdown, surface_signature, ) from ams.snapshot import Snapshot def _snap(name: str, methods: list[str], *, sha: str | None = None, events: list[str] | None = None): return Snapshot( { "binary": {"name": name, "engine": "Piklib8", "compiler": "MSVC6", "sha256": sha or ("sha-" + name + "-" + "".join(methods))}, "types": [{"script_name": "ANIMO"}], "events": [{"owner": "CMC_Animo", "name": e, "order": i} for i, e in enumerate(events or [])], "fields": [], "methods": [{"owner": "CMC_Animo", "name": m, "id": i} for i, m in enumerate(methods)], } ) # --- signature --------------------------------------------------------------------------------- def test_signature_ignores_identity_order_and_addresses(): a = _snap("X.dll", ["SHOW", "HIDE"]) b = _snap("X.dll", ["HIDE", "SHOW"]) # same surface, different list order assert surface_signature(a) == surface_signature(b) def test_signature_differs_on_surface_change(): a = _snap("X.dll", ["SHOW", "HIDE"]) b = _snap("X.dll", ["SHOW", "HIDE", "PAUSE"]) assert surface_signature(a) != surface_signature(b) # --- clustering / asterisk --------------------------------------------------------------------- def test_same_surface_collapses_to_one_variant(): # same library name, byte-different files, identical surface -> compatible, no asterisk t = compatibility_table([ Entry("Game A", _snap("PIKLIB8.dll", ["SHOW", "HIDE"], sha="aaa")), Entry("Game B", _snap("PIKLIB8.dll", ["SHOW", "HIDE"], sha="bbb")), ]) lib = t["libraries"][0] assert lib["name"] == "PIKLIB8.dll" assert lib["variant_count"] == 1 assert lib["diverges"] is False assert all(c["diverges"] is False for c in lib["cells"].values()) def test_divergent_surface_raises_asterisk(): t = compatibility_table([ Entry("Game A", _snap("PIKLIB8.dll", ["SHOW", "HIDE"])), Entry("Game B", _snap("PIKLIB8.dll", ["SHOW", "HIDE"])), # identical -> reference (bigger) Entry("Game C", _snap("PIKLIB8.dll", ["SHOW", "HIDE", "PAUSE"])), ]) lib = t["libraries"][0] assert lib["variant_count"] == 2 assert lib["diverges"] is True ref = next(v for v in lib["variants"] if v["reference"]) div = next(v for v in lib["variants"] if not v["reference"]) assert {m["game"] for m in ref["members"]} == {"Game A", "Game B"} # the majority cluster assert [m["game"] for m in div["members"]] == ["Game C"] assert div["vs_reference"]["axes"]["methods"] == {"added": 1, "removed": 0, "changed": 0} assert 0 < div["vs_reference"]["overall"] < 100 # named detail: *which* class gained *which* method by_owner = div["vs_reference"]["detail"]["methods"]["by_owner"] assert by_owner["CMC_Animo"]["added"] == ["PAUSE"] assert lib["cells"]["Game A"]["diverges"] is False assert lib["cells"]["Game C"]["diverges"] is True def test_hash_prefix_is_stripped_for_grouping(): # stored names carry a per-file _ prefix; copies must still group under one library row t = compatibility_table([ Entry("Game A", _snap("333ab0c4_PIKLIB8.dll", ["SHOW", "HIDE"])), Entry("Game B", _snap("8dc0165d_PIKLIB8.dll", ["SHOW", "HIDE"])), Entry("Game C", _snap("bba62d83_PIKLIB8.dll", ["SHOW", "HIDE", "PAUSE"])), ]) assert len(t["libraries"]) == 1 lib = t["libraries"][0] assert lib["name"] == "PIKLIB8.dll" # prefix gone from the display name assert lib["variant_count"] == 2 # A,B identical → reference; C diverges → asterisk assert lib["cells"]["Game C"]["diverges"] is True # the original stored filename is preserved per member for correlation files = {m["file"] for v in lib["variants"] for m in v["members"]} assert "333ab0c4_PIKLIB8.dll" in files def test_variant_carries_full_supported_api(): # the baseline: each variant lists *what is there*, grouped by class — not just the diff t = compatibility_table([ Entry("Game A", _snap("X.dll", ["SHOW", "HIDE"], events=["ONCLICK"])), ]) api = t["libraries"][0]["variants"][0]["api"] assert [m["name"] for m in api["classes"]["CMC_Animo"]["methods"]] == ["HIDE", "SHOW"] assert [e["name"] for e in api["classes"]["CMC_Animo"]["events"]] == ["ONCLICK"] assert [t_["script_name"] for t_ in api["types"]] == ["ANIMO"] def test_grouping_is_case_insensitive_keeps_common_spelling(): t = compatibility_table([ Entry("Game A", _snap("Piklib8.dll", ["SHOW"])), Entry("Game B", _snap("PIKLIB8.dll", ["SHOW"])), Entry("Game C", _snap("PIKLIB8.dll", ["SHOW"])), ]) assert len(t["libraries"]) == 1 assert t["libraries"][0]["name"] == "PIKLIB8.dll" # most common spelling # --- renderers --------------------------------------------------------------------------------- def test_markdown_has_matrix_and_asterisk(): t = compatibility_table([ Entry("Game A", _snap("PIKLIB8.dll", ["SHOW", "HIDE"])), Entry("Game B", _snap("PIKLIB8.dll", ["SHOW", "HIDE"])), Entry("Game C", _snap("PIKLIB8.dll", ["SHOW", "HIDE", "PAUSE"])), ]) md = render_markdown(t) assert "| Biblioteka |" in md assert "PIKLIB8.dll" in md assert "\\*" in md # the divergence marker survives to the table assert "vs **A**" in md # the footnote carries the delta assert "**CMC_Animo**" in md # named per-class breakdown assert "+PAUSE" in md # the concrete added method def test_json_round_trips(): t = compatibility_table([Entry("Game A", _snap("X.dll", ["SHOW"]))]) assert json.loads(render_json(t)) == t def test_empty_catalog(): t = compatibility_table([]) assert t["libraries"] == [] assert t["games"] == [] assert "katalog pusty" in render_markdown(t) # --- endpoint ---------------------------------------------------------------------------------- @pytest.fixture() def client(tmp_path): pytest.importorskip("fastapi") from fastapi.testclient import TestClient from ams.api.app import create_app return TestClient(create_app(database_url="sqlite:///{0}/compat.db".format(tmp_path))) def _raw(name: str, methods: list[str], sha: str) -> dict: return _snap(name, methods, sha=sha).raw def test_compat_endpoint_md_and_json(client): client.post("/snapshots", params={"game": "Reksio i UFO"}, json=_raw("PIKLIB8.dll", ["SHOW", "HIDE"], "s1")) client.post("/snapshots", params={"game": "Reksio i Nemo"}, json=_raw("PIKLIB8.dll", ["SHOW", "HIDE", "PAUSE"], "s2")) md = client.get("/compat", params={"format": "md"}) assert md.status_code == 200 assert "attachment" in md.headers["content-disposition"] assert "compat.md" in md.headers["content-disposition"] assert "PIKLIB8.dll" in md.text and "\\*" in md.text js = client.get("/compat", params={"format": "json"}) assert js.status_code == 200 assert "compat.json" in js.headers["content-disposition"] body = js.json() lib = body["libraries"][0] assert lib["name"] == "PIKLIB8.dll" assert lib["variant_count"] == 2 assert set(body["games"]) == {"Reksio i UFO", "Reksio i Nemo"}