166 lines
5.2 KiB
Python
166 lines
5.2 KiB
Python
"""End-to-end test over a pseudo-terminal, with no RFID hardware.
|
|
|
|
The test owns the master side of a PTY, points the CLI at the slave side and
|
|
writes real RDM6300 frames. This exercises the same code path a physical
|
|
reader would: serial open -> byte stream -> parse -> callback -> stdout.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import pty
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from rdm_card_reader.card import CONFIG_CARD_VALUE
|
|
|
|
from .test_protocol import encode_stream, encode_frame # noqa: F401
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
SRC = os.path.join(ROOT, "src")
|
|
|
|
TIMEOUT = 15.0
|
|
|
|
|
|
class ReaderProcess:
|
|
"""Run the CLI against a PTY and collect its stdout in the background."""
|
|
|
|
def __init__(self, extra_args=()):
|
|
self.master, slave = pty.openpty()
|
|
self.port = os.ttyname(slave)
|
|
env = dict(os.environ)
|
|
env["PYTHONPATH"] = SRC + os.pathsep + env.get("PYTHONPATH", "")
|
|
self.proc = subprocess.Popen(
|
|
[sys.executable, "-u", "-m", "rdm_card_reader",
|
|
"--port", self.port, "--heartbeat", "0.3", *extra_args],
|
|
cwd=ROOT, env=env, stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT, text=True, bufsize=1,
|
|
start_new_session=True,
|
|
)
|
|
self.lines = []
|
|
self._lock = threading.Lock()
|
|
self._thread = threading.Thread(target=self._pump, daemon=True)
|
|
self._thread.start()
|
|
|
|
def _pump(self):
|
|
for line in self.proc.stdout:
|
|
with self._lock:
|
|
self.lines.append(line.rstrip())
|
|
|
|
def output(self):
|
|
with self._lock:
|
|
return "\n".join(self.lines)
|
|
|
|
def send(self, frame):
|
|
os.write(self.master, b"\x02" + frame + b"\x03")
|
|
|
|
def wait_for(self, needle, timeout=TIMEOUT):
|
|
deadline = time.time() + timeout
|
|
while time.time() < deadline:
|
|
if needle in self.output():
|
|
return True
|
|
time.sleep(0.05)
|
|
return False
|
|
|
|
def __enter__(self):
|
|
self.wait_for("waiting for cards")
|
|
return self
|
|
|
|
def __exit__(self, *exc):
|
|
try:
|
|
os.killpg(os.getpgid(self.proc.pid), signal.SIGINT)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self.proc.wait(timeout=5)
|
|
except Exception:
|
|
self.proc.kill()
|
|
self._thread.join(timeout=3)
|
|
for fd in (self.master, self.proc.stdout):
|
|
try:
|
|
fd.close()
|
|
except Exception:
|
|
pass
|
|
return False
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform != "linux", reason="PTY test is Linux only")
|
|
def test_card_insert_and_remove_are_reported():
|
|
card_id = 0x00C4A0B5
|
|
frame = encode_frame(card_id)
|
|
|
|
with ReaderProcess() as rp:
|
|
deadline = time.time() + TIMEOUT
|
|
while time.time() < deadline and not rp.wait_for("CARD INSERTED", 0.2):
|
|
rp.send(frame)
|
|
|
|
assert rp.wait_for("CARD INSERTED"), rp.output()
|
|
assert rp.wait_for("CARD REMOVED")
|
|
|
|
text = rp.output()
|
|
assert rp.port in text
|
|
assert str(card_id) in text
|
|
assert "0x00C4A0B5" in text
|
|
assert "222" in text # checksum = 0xDE
|
|
assert "0x0F" in text # type
|
|
assert "True" in text # valid
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform != "linux", reason="PTY test is Linux only")
|
|
def test_corrupt_checksum_is_reported_as_invalid():
|
|
good = bytearray(encode_frame(0x1A2B3C4D))
|
|
good[-1] = ord("0") # break the checksum
|
|
bad = bytes(good)
|
|
|
|
with ReaderProcess() as rp:
|
|
deadline = time.time() + TIMEOUT
|
|
while time.time() < deadline and not rp.wait_for("INVALID CARD", 0.2):
|
|
rp.send(bad)
|
|
|
|
assert "INVALID CARD" in rp.output()
|
|
assert "checksum mismatch" in rp.output()
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform != "linux", reason="PTY test is Linux only")
|
|
def test_json_mode_emits_machine_readable_events():
|
|
with ReaderProcess(extra_args=("--json",)) as rp:
|
|
deadline = time.time() + TIMEOUT
|
|
while time.time() < deadline and not rp.wait_for('"event"', 0.2):
|
|
rp.send(encode_frame(0x00C4A0B5))
|
|
|
|
payloads = [
|
|
json.loads(line)
|
|
for line in rp.output().splitlines()
|
|
if line.strip().startswith("{")
|
|
]
|
|
assert payloads, rp.output()
|
|
assert any(p["event"] == "card_inserted" for p in payloads)
|
|
assert any(p["id"] == 0x00C4A0B5 for p in payloads)
|
|
assert all(p["port"] == rp.port for p in payloads)
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform != "linux", reason="PTY test is Linux only")
|
|
def test_config_card_is_annotated():
|
|
with ReaderProcess() as rp:
|
|
deadline = time.time() + TIMEOUT
|
|
while time.time() < deadline and not rp.wait_for("config card", 0.2):
|
|
rp.send(encode_frame(CONFIG_CARD_VALUE))
|
|
|
|
assert "config card" in rp.output()
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform != "linux", reason="PTY test is Linux only")
|
|
def test_removal_after_heartbeat_expires():
|
|
with ReaderProcess() as rp:
|
|
deadline = time.time() + TIMEOUT
|
|
while time.time() < deadline and not rp.wait_for("CARD INSERTED", 0.2):
|
|
rp.send(encode_frame(0x00C4A0B5))
|
|
# stop sending and let the 0.3 s heartbeat lapse
|
|
assert rp.wait_for("CARD REMOVED", timeout=5)
|
|
|
|
assert "CARD REMOVED" in rp.output()
|