feat: add compatibility table endpoint and rendering for game libraries
This commit is contained in:
149
tests/test_compat.py
Normal file
149
tests/test_compat.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""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
|
||||
|
||||
assert lib["cells"]["Game A"]["diverges"] is False
|
||||
assert lib["cells"]["Game C"]["diverges"] is True
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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"}
|
||||
Reference in New Issue
Block a user