167 lines
6.6 KiB
Python
167 lines
6.6 KiB
Python
"""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_hash_prefix_is_stripped_for_grouping():
|
|
# stored names carry a per-file <sha8>_ 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_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"}
|