"""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