import pytest from app.converter.compatibility import CompatibilityError, audit_ir, require_supported_scripts from app.converter.rpy_lexer import parse_tree from app.converter.rpy_parser import RpyParser, ScriptData HUD = ''' style hud_text: size 28 color "#fff" unknown_cosmetic_thing 3 transform pulse: zoom 1.0 linear 0.5 zoom 1.1 repeat screen hud(day=1): zorder 5 tag hud default open = False frame: xalign 0.0 yalign 0.0 background "#0008" vbox: spacing 4 text "Day [day]" style "hud_text" if money > 10: text "Rich" color "#0f0" else: text "Poor" for item in inventory: textbutton item action [SetVariable("held", item), Hide("hud")] at pulse key "K_h" action ToggleScreenVariable("open") timer 1.0 action NullAction() repeat True use footer("bye") screen footer(label): imagebutton auto "btn_%s.png" action Return(1) hover_sound "tick.ogg" label start: $ money = 20 $ inventory = ["key"] show screen hud(2) call screen footer("x") $ r = _return hide screen hud return ''' def parse(source): data = ScriptData() RpyParser(data, "script.rpy").parse(source) return data def test_screens_styles_and_calls_become_ir(): data = parse(HUD) assert not data.blockers, data.blockers hud = data.screens["hud"] assert hud["params"][0]["n"] == "day" assert hud["props"]["zorder"] == {"k": "const", "v": 5} and hud["props"]["tag"]["id"] == "hud" assert hud["defaults"] == [["open", {"k": "const", "v": False}]] frame = hud["body"][0] assert frame["w"] == "frame" and frame["props"]["background"]["v"] == "#0008" box = frame["body"][0] assert box["w"] == "vbox" and [n.get("w") or n["c"] for n in box["body"]] == ["text", "if", "for"] button = box["body"][2]["body"][0] assert button["w"] == "textbutton" and button["props"]["action"]["k"] == "list" assert button["props"]["at"] == [{"props": {"zoom": 1.0}, "name": "pulse", "atl": "pulse"}] assert [n.get("c") for n in hud["body"][1:]] == ["key", "timer", "use"] assert data.styles["hud_text"]["props"]["size"]["v"] == 28 assert "unknown_cosmetic_thing" not in data.styles["hud_text"]["props"] ops = [n["op"] for label in data.modules["script.rpy"] for n in label["body"] if n["op"].endswith("screen")] assert ops == ["show_screen", "call_screen", "hide_screen"] call = next(n for label in data.modules["script.rpy"] for n in label["body"] if n["op"] == "call_screen") assert call["name"] == "footer" and call["args"][0] == {"k": "const", "v": "x"} def test_shown_screens_are_audited_but_unshown_ones_are_not(): data = parse('screen odd():\n mousearea:\n pass\nlabel start:\n "Hi"\n') audit_ir(data) require_supported_scripts(data) # never shown: harmless data = parse('screen odd():\n mousearea:\n pass\nlabel start:\n call screen odd\n') audit_ir(data) with pytest.raises(CompatibilityError, match="screen"): require_supported_scripts(data) def test_unknown_actions_and_screens_block_the_import(): data = parse('screen s():\n textbutton "x" action Preference("fullscreen")\nlabel start:\n call screen s\n') audit_ir(data) with pytest.raises(CompatibilityError, match="Preference"): require_supported_scripts(data) data = parse('label start:\n call screen nowhere\n') audit_ir(data) with pytest.raises(CompatibilityError, match="screen"): require_supported_scripts(data) def test_screens_opened_from_python_and_by_actions_are_shipped(): from app.converter.pipeline import _used_screens data = parse( 'screen a():\n textbutton "x" action Show("b")\nscreen b():\n text "B"\nscreen c():\n text "C"\n' 'screen d():\n text "D"\nlabel start:\n $ renpy.show_screen("d")\n call screen a\n') audit_ir(data) require_supported_scripts(data) assert _used_screens(data) == {"a", "b", "d"} def test_touch_variants_are_dropped_and_later_definitions_win(): data = parse( 'screen s():\n text "one"\nscreen s():\n variant "touch"\n text "touch"\n' 'screen t():\n text "one"\nscreen t():\n text "two"\nlabel start:\n "x"\n') assert data.screens["s"]["body"][0]["args"][0]["v"] == "one" assert data.screens["t"]["body"][0]["args"][0]["v"] == "two" def test_lexer_keeps_screen_blocks_nested(): lines = parse_tree("screen s():\n vbox:\n text \"a\"\n") assert lines[0].block[0].block[0].text.strip().startswith("text") def test_decompiled_forms_operators_glued_properties_has_transclude_and_inline_atl(): data = parse( 'transform blink(delay, cycle=1.0):\n alpha 0.0\n pause delay\n linear cycle alpha 1.0\n' 'screen card(title):\n frame:\n has vbox\n text title\n transclude\n' 'screen deck(day):\n use card("T"):\n text "Day " + str(day) size 20\n' ' text "x" pos(5, 6) xysize(10,20)\n' ' add "bg.png":\n alpha 0.0\n linear 0.5 alpha 1.0\n' ' add "dot.png" at blink(0.2, 0.3)\n' ' on "show" action NullAction()\n' 'label start:\n call screen deck(1)\n') audit_ir(data) require_supported_scripts(data) deck = data.screens["deck"]["body"] use = deck[0] assert use["c"] == "use" and use["body"][0]["args"][0]["k"] == "bin" assert use["body"][0]["props"]["size"]["v"] == 20 card = data.screens["card"]["body"][0] assert card["w"] == "frame" and [n.get("w") or n["c"] for n in card["body"][0]["body"]] == ["text", "transclude"] assert deck[1]["props"]["pos"]["k"] == "tuple" and deck[1]["props"]["xysize"]["k"] == "tuple" assert deck[2]["props"]["alpha"] == {"k": "const", "v": 0.0} animated = deck[2]["props"]["at"][0] assert animated["atl"]["steps"][-1]["op"] == "move" and animated["atl"]["steps"][-1]["props"] == {"alpha": 1.0} called = deck[3]["props"]["at"][0] assert called["name"] == "blink" and called["atl"] == "blink" and len(called["args"]) == 2 blink = data.atl["blink"] assert [p["n"] for p in blink["params"]] == ["delay", "cycle"] assert blink["steps"][1] == {"op": "pause", "t": {"e": {"k": "name", "id": "delay"}}} assert deck[4] == {"c": "on", "event": "show", "action": {"k": "call", "f": {"k": "name", "id": "NullAction"}, "a": [], "kw": {}}} def test_computed_atl_values_make_a_transform_animated(): data = parse('transform shift(x):\n xoffset x\nlabel start:\n "Hi"\n') block = data.atl["shift"] assert block["params"][0]["n"] == "x" assert block["steps"][0]["props"]["xoffset"] == {"e": {"k": "name", "id": "x"}} assert data.transforms["shift"] == {}