OXYGENHUB
# 67065eaa1a13 retro-remote-debug-controller / conformance / test_conformance.py
3.9 KiB · Python · last changed Contract 0.2.0: input injection (POST /key, POST /reset) 3 months ago source blame history raw
8ac19619 Andy McCall ↶ 1
"""Contract conformance tests (SPEC.md 0.1.x). Platform-agnostic: framebuffer
2
dimensions come from the PPM header, addresses use $0000, and `bank` is
3
exercised but banking is not required."""
4
import urllib.error
5
import urllib.request
6
d44319b1 Andy McCall ↶ 7
import pytest
8
8ac19619 Andy McCall ↶ 9
from emu_control import parse_ppm_header
10
11
12
def _raw(emu, method, path):
13
    """Raw request returning (status_code, body) without the client's wrapping,
14
    so we can assert on 4xx status codes."""
15
    req = urllib.request.Request(emu.base + path, method=method)
16
    try:
17
        with urllib.request.urlopen(req, timeout=emu.timeout) as r:
18
            return r.status, r.read()
19
    except urllib.error.HTTPError as e:
20
        return e.code, e.read()
21
22
23
def test_status_contract(emu):
24
    st = emu.status()
25
    for k in ("contract", "emulator", "platform", "frame", "paused", "running"):
26
        assert k in st, f"missing /status.{k}"
27
    assert str(st["contract"]).split(".")[0] == "0", "unexpected contract MAJOR"
28
    assert isinstance(st["platform"], str) and st["platform"]
29
    assert isinstance(st["frame"], int)
30
    assert isinstance(st["paused"], bool)
31
32
33
def test_regs_object(emu):
34
    r = emu.regs()
35
    assert isinstance(r, dict) and r, "regs must be a non-empty JSON object"
36
    assert all(isinstance(v, int) for v in r.values()), "register values must be ints"
37
38
39
def test_mem_exact_length(emu):
40
    assert len(emu.mem(0x0000, 32)) == 32
41
    assert len(emu.mem(0x0000, 256)) == 256
42
    assert len(emu.mem(0x0000, 0)) == 0
43
44
45
def test_mem_bank_param_returns_length(emu):
46
    # bank is optional (ignored on flat platforms) but must still honour len.
47
    assert len(emu.mem(0x0000, 8, bank=0)) == 8
48
49
50
def test_screenshot_ppm(emu):
51
    data = emu.screenshot_ppm()
52
    w, h, maxval, off = parse_ppm_header(data)
53
    assert w > 0 and h > 0, f"bad dimensions {w}x{h}"
54
    assert maxval == 255
55
    assert len(data) == off + 3 * w * h, "PPM body size mismatches dimensions"
56
57
58
def test_step_is_deterministic(emu):
59
    emu.pause()
60
    try:
61
        a = emu.frame()
62
        b = emu.step(2); c = emu.frame()
63
        assert b == c, "/step must return the halted frame counter"
64
        d = emu.step(2); e = emu.frame()
65
        assert d == e
66
        assert c - a > 0, "/step must advance the frame counter"
67
        assert (c - a) == (e - c), "equal steps must advance by equal amounts"
68
    finally:
69
        emu.resume()
70
71
72
def test_pause_resume_toggle(emu):
73
    emu.pause()
74
    assert emu.status()["paused"] is True
75
    emu.resume()
76
    assert emu.status()["paused"] is False
77
78
79
def test_unknown_path_404(emu):
80
    code, _ = _raw(emu, "GET", "/no-such-endpoint")
81
    assert code == 404
82
83
84
def test_step_requires_post(emu):
85
    code, _ = _raw(emu, "GET", "/step?frames=1")
d44319b1 Andy McCall ↶ 86
    assert code == 405
87
88
89
def _contract_minor(emu) -> int:
90
    return int(str(emu.status()["contract"]).split(".")[1])
91
92
93
# -- contract 0.2: input injection (skipped on 0.1 servers) ------------------
94
95
def test_key_injection(emu):
96
    """/key accepts text= and code= with an optional down=, and rejects a
97
    request with neither. The key's *effect* is platform/program-specific and
98
    tested per-port; here we assert only the contract surface."""
99
    if _contract_minor(emu) < 2:
100
        pytest.skip("contract < 0.2 (no input injection)")
101
    emu.key(text="a", down=True)
102
    emu.key(text="a", down=False)
103
    emu.key(text="a")                    # tap (press then release)
104
    emu.key(code=32, down=True)
105
    emu.key(code=32, down=False)
106
    code, _ = _raw(emu, "POST", "/key")  # neither text nor code
107
    assert code == 400
108
109
110
def test_key_requires_post(emu):
111
    if _contract_minor(emu) < 2:
112
        pytest.skip("contract < 0.2")
113
    code, _ = _raw(emu, "GET", "/key?text=a")
114
    assert code == 405
115
116
117
def test_reset(emu):
118
    """/reset responds and is POST-only. Runs last (it restarts the machine)."""
119
    if _contract_minor(emu) < 2:
120
        pytest.skip("contract < 0.2")
121
    code, _ = _raw(emu, "GET", "/reset")
8ac19619 Andy McCall ↶ 122
    assert code == 405
d44319b1 Andy McCall ↶ 123
    emu.reset()