Files
Aidem-Media-DLL-Analysis/tests/test_compat.py

172 lines
6.9 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
# 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 <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
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"}