- parser.py: classify a line into control / function-call / raw payload; parse loose hex/binary with intermixed 0x/0b tokens (whitespace- insensitive, defaulting to the session entry mode) - entry.py: byte_space() byte-group formatting + key bindings — Ctrl-/ toggles hex/binary entry mode (breadcrumb updates), digits auto-space into byte groups as you type - app.py: dispatch() drives the loop via parse_line (testable without a live prompt); identify/call are stubbed for Phases 3/4 14 new tests (parser tokens/intermix/errors, byte_space, dispatch). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
141 lines
4.9 KiB
Python
141 lines
4.9 KiB
Python
"""rawcli Phase 1 — session state, breadcrumb, trace rendering, CLI dispatch. Hardware-free."""
|
|
import re
|
|
|
|
import pytest
|
|
|
|
from pm3py.cli.main import build_parser, main
|
|
from pm3py.cli.rawcli.session import RawSession
|
|
from pm3py.cli.rawcli.trace_view import render_exchange
|
|
from pm3py.cli.rawcli.parser import parse_token, parse_bytes, parse_line
|
|
from pm3py.cli.rawcli.entry import byte_space
|
|
from pm3py.cli.rawcli.app import dispatch
|
|
|
|
_ANSI = re.compile(r"\x1b\[[0-9;]*m")
|
|
|
|
|
|
def _plain(s: str) -> str:
|
|
return _ANSI.sub("", s)
|
|
|
|
|
|
class TestBreadcrumb:
|
|
def test_default(self):
|
|
assert RawSession().breadcrumb() == "[raw / 0x]"
|
|
|
|
def test_toggle_to_binary(self):
|
|
s = RawSession()
|
|
assert s.toggle_entry_mode() == "bin"
|
|
assert s.entry_prefix == "0b"
|
|
assert s.breadcrumb() == "[raw / 0b]"
|
|
assert s.toggle_entry_mode() == "hex"
|
|
assert s.breadcrumb() == "[raw / 0x]"
|
|
|
|
def test_identified_fills_segments(self):
|
|
s = RawSession()
|
|
s.field = "hf"
|
|
s.transponder = "NTAG215"
|
|
assert s.breadcrumb() == "[raw / hf / NTAG215 / 0x]"
|
|
|
|
def test_field_only(self):
|
|
s = RawSession()
|
|
s.field = "lf"
|
|
assert s.breadcrumb() == "[raw / lf / 0x]"
|
|
|
|
|
|
class TestTraceView:
|
|
def test_command_and_response_lines(self):
|
|
out = _plain(render_exchange(bytes([0x30, 0x04]),
|
|
bytes([0x00, 0x01, 0x02, 0x03]),
|
|
protocol="hf14a", is_tty=False))
|
|
assert "Reader → Tag" in out
|
|
assert "Tag → Reader" in out
|
|
assert "30 04" in out # command bytes shown
|
|
|
|
def test_command_only_when_no_response(self):
|
|
out = _plain(render_exchange(bytes([0x26]), None, protocol="hf14a", is_tty=False))
|
|
assert "Reader → Tag" in out
|
|
assert "Tag → Reader" not in out
|
|
|
|
def test_annotation_present_for_known_command(self):
|
|
# 0x30 = 14a READ; the decoder should annotate it (exact text may vary)
|
|
out = _plain(render_exchange(bytes([0x30, 0x04]), None, protocol="hf14a", is_tty=False))
|
|
assert "READ" in out.upper()
|
|
|
|
|
|
class TestCliDispatch:
|
|
def test_rawcli_subcommand_parsed(self):
|
|
args = build_parser().parse_args(["rawcli", "--port", "/dev/ttyACM0"])
|
|
assert args.command == "rawcli" and args.port == "/dev/ttyACM0"
|
|
|
|
def test_no_command_prints_help(self, capsys):
|
|
rc = main([])
|
|
assert rc == 1
|
|
assert "rawcli" in capsys.readouterr().out
|
|
|
|
|
|
class TestParser:
|
|
def test_hex_tokens(self):
|
|
assert parse_token("30") == b"\x30"
|
|
assert parse_token("0x3004") == b"\x30\x04"
|
|
assert parse_bytes("30 04") == b"\x30\x04"
|
|
assert parse_bytes("3004") == b"\x30\x04" # default hex mode
|
|
|
|
def test_binary_tokens(self):
|
|
assert parse_token("0b00110000") == b"\x30"
|
|
assert parse_token("00110000", "bin") == b"\x30"
|
|
|
|
def test_intermixed(self):
|
|
assert parse_bytes("0b00110000 0x04") == b"\x30\x04"
|
|
assert parse_bytes("0x30 00000100", "bin") == b"\x30\x04" # bare token uses mode
|
|
|
|
def test_bad_tokens(self):
|
|
with pytest.raises(ValueError):
|
|
parse_token("303") # odd hex
|
|
with pytest.raises(ValueError):
|
|
parse_token("0b0011") # not a whole byte
|
|
|
|
def test_classify_control(self):
|
|
c = parse_line("identify")
|
|
assert c.kind == "control" and c.name == "identify"
|
|
assert parse_line("help ntag215").kind == "control"
|
|
|
|
def test_classify_call(self):
|
|
c = parse_line("READ(4)")
|
|
assert c.kind == "call" and c.name == "READ" and c.args == ["4"]
|
|
c = parse_line("WRITE(4, 0x00112233)")
|
|
assert c.args == ["4", "0x00112233"]
|
|
assert parse_line("GET_DATA()").kind == "call"
|
|
|
|
def test_classify_raw(self):
|
|
c = parse_line("30 04")
|
|
assert c.kind == "raw" and c.payload == b"\x30\x04"
|
|
|
|
|
|
class TestByteSpace:
|
|
def test_hex_grouping(self):
|
|
assert byte_space("3004", "hex") == "30 04"
|
|
assert byte_space("300", "hex") == "30 0"
|
|
assert byte_space("30 04", "hex") == "30 04" # idempotent
|
|
|
|
def test_binary_grouping(self):
|
|
assert byte_space("0011000000000100", "bin") == "00110000 00000100"
|
|
|
|
def test_prefixed_left_alone(self):
|
|
assert byte_space("0x3004", "hex") == "0x3004" # explicit prefix not mangled
|
|
|
|
|
|
class TestDispatch:
|
|
def test_quit_returns_false(self):
|
|
assert dispatch(RawSession(), "quit") is False
|
|
|
|
def test_help_prints(self, capsys):
|
|
assert dispatch(RawSession(), "help") is True
|
|
assert "rawcli" in capsys.readouterr().out
|
|
|
|
def test_raw_renders_without_device(self, capsys):
|
|
assert dispatch(RawSession(), "30 04") is True
|
|
assert "30 04" in _plain(capsys.readouterr().out)
|
|
|
|
def test_bad_hex_reports(self, capsys):
|
|
assert dispatch(RawSession(), "0b0011") is True
|
|
assert "whole number of bytes" in capsys.readouterr().out
|