nfcscript 0.0.16__tar.gz → 0.0.18__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {nfcscript-0.0.16 → nfcscript-0.0.18}/PKG-INFO +1 -1
  2. {nfcscript-0.0.16 → nfcscript-0.0.18}/pyproject.toml +2 -5
  3. {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/nfc_cli.py +3 -3
  4. {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/reader.py +205 -64
  5. {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfcscript.egg-info/PKG-INFO +1 -1
  6. nfcscript-0.0.18/tests/test_reader.py +338 -0
  7. nfcscript-0.0.16/tests/test_reader.py +0 -275
  8. {nfcscript-0.0.16 → nfcscript-0.0.18}/README.md +0 -0
  9. {nfcscript-0.0.16 → nfcscript-0.0.18}/setup.cfg +0 -0
  10. {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/__init__.py +0 -0
  11. {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/assertions.py +0 -0
  12. {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/bits.py +0 -0
  13. {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/checksum.py +0 -0
  14. {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/cli.py +0 -0
  15. {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/delay.py +0 -0
  16. {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/hex_util.py +0 -0
  17. {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/trace.py +0 -0
  18. {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfcscript.egg-info/SOURCES.txt +0 -0
  19. {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfcscript.egg-info/dependency_links.txt +0 -0
  20. {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfcscript.egg-info/entry_points.txt +0 -0
  21. {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfcscript.egg-info/requires.txt +0 -0
  22. {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfcscript.egg-info/top_level.txt +0 -0
  23. {nfcscript-0.0.16 → nfcscript-0.0.18}/tests/test_assertions.py +0 -0
  24. {nfcscript-0.0.16 → nfcscript-0.0.18}/tests/test_bits.py +0 -0
  25. {nfcscript-0.0.16 → nfcscript-0.0.18}/tests/test_checksum.py +0 -0
  26. {nfcscript-0.0.16 → nfcscript-0.0.18}/tests/test_cli.py +0 -0
  27. {nfcscript-0.0.16 → nfcscript-0.0.18}/tests/test_cli_collect.py +0 -0
  28. {nfcscript-0.0.16 → nfcscript-0.0.18}/tests/test_delay.py +0 -0
  29. {nfcscript-0.0.16 → nfcscript-0.0.18}/tests/test_hex_util.py +0 -0
  30. {nfcscript-0.0.16 → nfcscript-0.0.18}/tests/test_trace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nfcscript
3
- Version: 0.0.16
3
+ Version: 0.0.18
4
4
  Summary: A plugin-based core engine for DSL-style NFC testing automation.
5
5
  Author: crthu
6
6
  Requires-Python: >=3.14
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nfcscript"
3
- version = "0.0.16"
3
+ version = "0.0.18"
4
4
  description = "A plugin-based core engine for DSL-style NFC testing automation."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -24,9 +24,6 @@ testpaths = ["tests"]
24
24
  [tool.uv]
25
25
  package = true
26
26
 
27
- [tool.uv.sources]
28
- nfctester = { workspace = true }
29
-
30
27
  [dependency-groups]
31
28
  dev = [
32
29
  "pytest",
@@ -34,7 +31,7 @@ dev = [
34
31
  ]
35
32
 
36
33
  [tool.bumpversion]
37
- current_version = "0.0.16"
34
+ current_version = "0.0.18"
38
35
  commit = true
39
36
  tag = true
40
37
  allow_dirty = true
@@ -172,13 +172,13 @@ def main():
172
172
  parser.error("需要指定卡片类型 (-c/--card) 或使用 --list-cards 查看可用类型")
173
173
 
174
174
  try:
175
- connect(port=args.port, reader_type=args.reader)
175
+ reader.connect(port=args.port, reader_type=args.reader)
176
176
  except Exception as e:
177
177
  print(f"连接读卡器失败: {e}")
178
178
  sys.exit(1)
179
179
 
180
180
  try:
181
- card = CardRegistry.create(args.card, reader=get_reader())
181
+ card = CardRegistry.create(args.card, reader=reader.target)
182
182
  methods = _get_methods(card)
183
183
 
184
184
  print(f"已连接卡片: {args.card}")
@@ -220,7 +220,7 @@ def main():
220
220
  print(f"错误: {e}")
221
221
 
222
222
  finally:
223
- close()
223
+ reader.close()
224
224
  print("已断开连接")
225
225
 
226
226
 
@@ -1,5 +1,10 @@
1
+ import atexit
1
2
  import os
3
+ import sys
2
4
  from contextlib import contextmanager
5
+ from typing import Any
6
+
7
+ from dotenv import load_dotenv
3
8
  from nfctester.drivers.card_reader import CardInfo, TransceiveBits
4
9
  from nfctester.registry import CardReaderRegistry
5
10
  from nfctester.trace import trace as _trace
@@ -7,44 +12,104 @@ from .assertions import ASSERT_EQUAL, ASSERT_IS_NOT_NONE, ASSERT_LEN
7
12
  from .checksum import GET_BCC
8
13
  from .hex_util import FORMAT_HEX
9
14
 
15
+ _DOTENV_LOADED = False
16
+
17
+
18
+ def _find_dotenv_all(start_dir: str) -> list[str]:
19
+ """从起始目录逐级向上搜索所有 .env 文件"""
20
+ chain = []
21
+ path = start_dir
22
+ while True:
23
+ env_path = os.path.join(path, ".env")
24
+ if os.path.exists(env_path):
25
+ chain.append(env_path)
26
+ parent = os.path.dirname(path)
27
+ if parent == path:
28
+ break
29
+ path = parent
30
+ chain.reverse()
31
+ return chain
32
+
33
+
34
+ def _load_env_configs() -> None:
35
+ """按需加载当前目录及上级目录的 .env 文件"""
36
+ global _DOTENV_LOADED
37
+ if not _DOTENV_LOADED:
38
+ try:
39
+ cwd = os.getcwd()
40
+ for env_path in _find_dotenv_all(cwd):
41
+ load_dotenv(env_path, override=False)
42
+ except Exception:
43
+ pass
44
+ _DOTENV_LOADED = True
45
+
46
+
47
+ # ─── 轻量读卡器门面单例 ───────────────────────────────────────────────────
10
48
 
11
- class _NFCState:
12
- """单例状态管理,封装读卡器生命周期"""
49
+ class _ReaderFacade:
50
+ """
51
+ 读卡器单例门面对象 (`reader`)。
52
+
53
+ 统一维护读卡器连接状态,提供 `reader.rf = True / False` 属性控制,
54
+ 并将所有属性与方法调用优先路由到当前模块顶层函数,或透传到底层 CardReader 实例。
55
+ """
56
+
57
+ def __init__(self) -> None:
58
+ self._target: Any = None
59
+ self._connected: bool = False
60
+
61
+ @property
62
+ def rf(self) -> bool:
63
+ """获取射频场状态。"""
64
+ return getattr(get_reader(), "rf_field", False)
65
+
66
+ @rf.setter
67
+ def rf(self, value: bool) -> None:
68
+ """设置射频场开关 (True / False)。"""
69
+ get_reader().rf_field = bool(value)
13
70
 
14
- _instance = None
71
+ @property
72
+ def rf_field(self) -> bool:
73
+ """获取射频场状态 (rf 别名)。"""
74
+ return self.rf
15
75
 
16
- def __new__(cls):
17
- if cls._instance is None:
18
- cls._instance = super().__new__(cls)
19
- cls._instance._reader = None
20
- cls._instance._connected = False
21
- return cls._instance
76
+ @rf_field.setter
77
+ def rf_field(self, value: bool) -> None:
78
+ """设置射频场开关 (rf 别名)。"""
79
+ self.rf = value
22
80
 
23
81
  @property
24
- def reader(self):
25
- return self._reader
82
+ def mf_crypto(self) -> bool:
83
+ """获取 Mifare 硬件加密状态。"""
84
+ return getattr(get_reader(), "mf_crypto", False)
26
85
 
27
- def connect(self, port: str = "COM20", reader_type: str = "pn532"):
28
- self._reader = CardReaderRegistry.create(reader_type, transport="serial", port=port)
29
- self._reader.open()
30
- self._connected = True
31
- _trace.log(f"Connected to {reader_type.upper()} on port {port}", layer="app")
86
+ @property
87
+ def target(self) -> Any:
88
+ """获取当前连接的底层 CardReader 实例。"""
89
+ return get_reader()
32
90
 
33
- def disconnect(self):
34
- if self._connected and self._reader:
35
- self._reader.close()
36
- self._connected = False
37
- self._reader = None
38
- _trace.log("Disconnected", layer="app")
91
+ def __getattr__(self, name: str) -> Any:
92
+ # 1. 优先路由到当前模块的顶层函数(如 reader.active -> active)
93
+ mod = sys.modules[__name__]
94
+ if hasattr(mod, name) and not name.startswith("_"):
95
+ return getattr(mod, name)
96
+ # 2. 其次透传给底层 CardReader 驱动实例(如 reader.mf_auth -> _target.mf_auth)
97
+ return getattr(self.target, name)
39
98
 
40
- def ensure_connected(self):
41
- ASSERT_IS_NOT_NONE(self._reader, msg="NFC driver not connected. Call connect() first.")
99
+ def __enter__(self):
100
+ get_reader()
101
+ return self
42
102
 
103
+ def __exit__(self, exc_type, exc_val, exc_tb):
104
+ pass
43
105
 
44
- _state = _NFCState()
45
106
 
107
+ reader = _ReaderFacade()
46
108
 
47
- def connect(port: str = None, reader_type: str = None):
109
+
110
+ # ─── 模块级顶层函数(单一事实来源) ──────────────────────────────────────────
111
+
112
+ def connect(port: str = None, reader_type: str = None) -> None:
48
113
  """
49
114
  连接并初始化读卡器。
50
115
 
@@ -55,6 +120,7 @@ def connect(port: str = None, reader_type: str = None):
55
120
  Raises:
56
121
  ValueError: 未提供 port 或 reader_type 且环境变量未设置。
57
122
  """
123
+ _load_env_configs()
58
124
  if port is None:
59
125
  port = os.environ.get("NFC_PORT")
60
126
  if reader_type is None:
@@ -63,18 +129,53 @@ def connect(port: str = None, reader_type: str = None):
63
129
  raise ValueError("未指定串口,请通过 -p 参数、connect(port=...) 或 NFC_PORT 环境变量配置")
64
130
  if reader_type is None:
65
131
  raise ValueError("未指定读卡器类型,请通过 -r 参数、connect(reader_type=...) 或 NFC_READER 环境变量配置")
66
- _state.connect(port, reader_type)
132
+
133
+ if reader._connected and reader._target is not None:
134
+ disconnect()
135
+
136
+ reader._target = CardReaderRegistry.create(reader_type, transport="serial", port=port)
137
+ reader._target.open()
138
+ reader._connected = True
139
+ _trace.log(f"Connected to {reader_type.upper()} on port {port}", layer="app")
140
+
141
+
142
+ def disconnect() -> None:
143
+ """断开与读卡器的连接并释放资源。"""
144
+ if reader._connected and reader._target is not None:
145
+ try:
146
+ reader._target.rf_field = False
147
+ except Exception:
148
+ pass
149
+ try:
150
+ reader._target.close()
151
+ except Exception:
152
+ pass
153
+ reader._connected = False
154
+ reader._target = None
155
+ _trace.log("Disconnected", layer="app")
156
+
157
+
158
+ def close() -> None:
159
+ """断开与读卡器的连接 (disconnect 别名)。"""
160
+ disconnect()
67
161
 
68
162
 
69
- def get_reader():
163
+ def get_reader() -> Any:
70
164
  """
71
- 获取当前连接的读卡器实例,供 Card 类使用。
165
+ 获取当前连接的读卡器实例,若未连接则尝试从环境变量自动连接。
72
166
 
73
167
  Returns:
74
168
  reader: 当前连接的读卡器实例。
75
169
  """
76
- _state.ensure_connected()
77
- return _state.reader
170
+ if not reader._connected or reader._target is None:
171
+ _load_env_configs()
172
+ port = os.environ.get("NFC_PORT")
173
+ reader_type = os.environ.get("NFC_READER")
174
+ if port and reader_type:
175
+ connect(port, reader_type)
176
+ else:
177
+ ASSERT_IS_NOT_NONE(reader._target, msg="NFC driver not connected. Call connect() first.")
178
+ return reader._target
78
179
 
79
180
 
80
181
  def active(low_layer: bool = False, ignore_error: bool = False, reqa_cmd: int = 0x26) -> CardInfo | None:
@@ -94,11 +195,10 @@ def active(low_layer: bool = False, ignore_error: bool = False, reqa_cmd: int =
94
195
  - 'atq' (list[int]): 卡片的 ATQ (Answer To Request)。
95
196
  - 'sak' (int): 卡片的 SAK (Select Acknowledge)。
96
197
  """
97
- _state.ensure_connected()
98
- reader = _state.reader
198
+ rdr = get_reader()
99
199
 
100
200
  if not low_layer:
101
- return reader.active()
201
+ return rdr.active()
102
202
 
103
203
  # 手动底层寻卡流程
104
204
  # 1. REQA
@@ -109,6 +209,7 @@ def active(low_layer: bool = False, ignore_error: bool = False, reqa_cmd: int =
109
209
 
110
210
  # 2. 抗冲突与选择
111
211
  full_uid = []
212
+ sak = 0
112
213
  for cl in [1, 2, 3]:
113
214
  res = anticoll(cl_level=cl, nvb=0x20)
114
215
  if not res.data:
@@ -153,40 +254,53 @@ def active(low_layer: bool = False, ignore_error: bool = False, reqa_cmd: int =
153
254
  return info
154
255
 
155
256
 
156
- def transceive(data: list[int], tx_crc: bool = True, rx_crc: bool = True) -> list[int]:
257
+ def transceive(
258
+ data: list[int] | bytes,
259
+ tx_crc: bool = True,
260
+ rx_crc: bool = True,
261
+ timeout: float = 0.5,
262
+ ) -> list[int]:
157
263
  """
158
264
  使用底层帧交互方式发送数据。
159
265
 
160
266
  Args:
161
- data: 要发送的字节列表。
267
+ data: 要发送的字节列表或 bytes。
162
268
  tx_crc: 发送时是否自动添加 CRC。False 则原样发送。
163
269
  rx_crc: 接收时是否自动校验 CRC。False 则原样接收不校验。
270
+ timeout: 接收超时时间 (秒),默认 0.5s。
164
271
 
165
272
  Returns:
166
273
  list[int]: 接收到的数据字节列表。
167
274
  """
168
- _state.ensure_connected()
169
- reader = _state.reader
170
- res = reader.transceive(data, tx_crc=tx_crc, rx_crc=rx_crc)
275
+ rdr = get_reader()
276
+ raw_list = list(data) if isinstance(data, (bytes, bytearray)) else data
277
+ res = rdr.transceive(raw_list, tx_crc=tx_crc, rx_crc=rx_crc, timeout=timeout)
171
278
  return res.data if res and res.data else []
172
279
 
173
280
 
174
- def transceive_bits(data: list[int], last_tx_bits: int = 0, tx_crc: bool = True, rx_crc: bool = True) -> TransceiveBits:
281
+ def transceive_bits(
282
+ data: list[int] | bytes,
283
+ last_tx_bits: int = 0,
284
+ tx_crc: bool = True,
285
+ rx_crc: bool = True,
286
+ timeout: float = 0.5,
287
+ ) -> TransceiveBits:
175
288
  """
176
289
  使用底层帧交互方式发送数据,并支持对最后一个字节进行位控制。
177
290
 
178
291
  Args:
179
- data: 要发送的字节列表。
292
+ data: 要发送的字节列表或 bytes。
180
293
  last_tx_bits: 最后一个字节实际发送的有效位数 (1-7),0 表示发送完整字节。
181
294
  tx_crc: 发送时是否自动添加 CRC。False 则原样发送。
182
295
  rx_crc: 接收时是否自动校验 CRC。False 则原样接收不校验。
296
+ timeout: 接收超时时间 (秒),默认 0.5s。
183
297
 
184
298
  Returns:
185
299
  TransceiveBits: 包含 data 和 bits 属性,失败返回空的 TransceiveBits(data=[], bits=0)。
186
300
  """
187
- _state.ensure_connected()
188
- reader = _state.reader
189
- res = reader.transceive(data, last_tx_bits=last_tx_bits, tx_crc=tx_crc, rx_crc=rx_crc)
301
+ rdr = get_reader()
302
+ raw_list = list(data) if isinstance(data, (bytes, bytearray)) else data
303
+ res = rdr.transceive(raw_list, last_tx_bits=last_tx_bits, tx_crc=tx_crc, rx_crc=rx_crc, timeout=timeout)
190
304
  return res if res else TransceiveBits(data=[], bits=0)
191
305
 
192
306
 
@@ -273,33 +387,24 @@ def select(cl_level: int, uid: list[int]) -> list[int]:
273
387
  return transceive(cmd, tx_crc=True, rx_crc=True)
274
388
 
275
389
 
276
- def field_on():
390
+ def field_on() -> None:
277
391
  """
278
- 开启 PN532 的 RF 场。
392
+ 开启读卡器的 RF 场。
279
393
 
280
394
  Raises:
281
395
  AssertionError: 未连接读卡器时抛出。
282
396
  """
283
- _state.ensure_connected()
284
- _state.reader.rf_field = True
397
+ get_reader().rf_field = True
285
398
 
286
399
 
287
- def field_off():
400
+ def field_off() -> None:
288
401
  """
289
- 关闭 PN532 的 RF 场。
402
+ 关闭读卡器的 RF 场。
290
403
 
291
404
  Raises:
292
405
  AssertionError: 未连接读卡器时抛出。
293
406
  """
294
- _state.ensure_connected()
295
- _state.reader.rf_field = False
296
-
297
-
298
- def close():
299
- """
300
- 断开与 PN532 读卡器的连接。
301
- """
302
- _state.disconnect()
407
+ get_reader().rf_field = False
303
408
 
304
409
 
305
410
  @contextmanager
@@ -308,8 +413,8 @@ def session(port: str = None, reader_type: str = None):
308
413
  上下文管理器,自动管理连接生命周期。
309
414
 
310
415
  用法:
311
- with session("COM20") as reader:
312
- card_info = reader.active()
416
+ with session("COM20") as r:
417
+ card_info = r.active()
313
418
  """
314
419
  if port is None:
315
420
  port = os.environ.get("NFC_PORT")
@@ -319,8 +424,44 @@ def session(port: str = None, reader_type: str = None):
319
424
  raise ValueError("未指定串口,请通过 session(port=...) 或 NFC_PORT 环境变量配置")
320
425
  if reader_type is None:
321
426
  raise ValueError("未指定读卡器类型,请通过 session(reader_type=...) 或 NFC_READER 环境变量配置")
322
- _state.connect(port, reader_type)
427
+ connect(port, reader_type)
323
428
  try:
324
- yield _state.reader
429
+ yield reader
325
430
  finally:
326
- _state.disconnect()
431
+ disconnect()
432
+
433
+
434
+ def _atexit_cleanup() -> None:
435
+ """进程退出时的 atexit 安全清理钩子:先关闭物理 RF 场,再关闭串口。"""
436
+ if reader._connected and reader._target is not None:
437
+ try:
438
+ reader._target.rf_field = False
439
+ except Exception:
440
+ pass
441
+ try:
442
+ reader._target.close()
443
+ except Exception:
444
+ pass
445
+
446
+
447
+ atexit.register(_atexit_cleanup)
448
+
449
+
450
+ __all__ = [
451
+ "reader",
452
+ "connect",
453
+ "disconnect",
454
+ "get_reader",
455
+ "active",
456
+ "transceive",
457
+ "transceive_bits",
458
+ "reqa",
459
+ "wupa",
460
+ "halt",
461
+ "anticoll",
462
+ "select",
463
+ "field_on",
464
+ "field_off",
465
+ "close",
466
+ "session",
467
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nfcscript
3
- Version: 0.0.16
3
+ Version: 0.0.18
4
4
  Summary: A plugin-based core engine for DSL-style NFC testing automation.
5
5
  Author: crthu
6
6
  Requires-Python: >=3.14
@@ -0,0 +1,338 @@
1
+ """reader 模块测试 (connect / get_reader / active / transceive / session 等)"""
2
+ import os
3
+ import sys
4
+ import pytest
5
+ from unittest.mock import MagicMock, patch
6
+ import nfc
7
+ from nfc import reader
8
+ from nfctester.drivers.card_reader import CardInfo, TransceiveBits
9
+ from nfctester.registry import CardReaderRegistry
10
+
11
+
12
+ @pytest.fixture(autouse=True)
13
+ def reset_state():
14
+ """每个测试前重置全局状态"""
15
+ reader._target = None
16
+ reader._connected = False
17
+ yield
18
+ reader._target = None
19
+ reader._connected = False
20
+
21
+
22
+ def _mock_reader():
23
+ mock = MagicMock()
24
+ mock.transceive.return_value = TransceiveBits(data=[0xAA, 0xBB], bits=8)
25
+ mock.active.return_value = CardInfo(uid=[0x01, 0x02, 0x03, 0x04], atq=[0x44, 0x00], sak=0x00)
26
+ mock.rf_field = False
27
+ mock.mf_crypto = False
28
+ return mock
29
+
30
+
31
+ class TestConnect:
32
+
33
+ @patch.object(CardReaderRegistry, "create")
34
+ def test_connect_with_params(self, mock_create):
35
+ mock_rdr = _mock_reader()
36
+ mock_create.return_value = mock_rdr
37
+ nfc.connect(port="COM99", reader_type="pn532")
38
+ mock_create.assert_called_once_with("pn532", transport="serial", port="COM99")
39
+ mock_rdr.open.assert_called_once()
40
+ assert reader._connected is True
41
+
42
+ @patch.object(CardReaderRegistry, "create")
43
+ def test_connect_from_env(self, mock_create, monkeypatch):
44
+ monkeypatch.setenv("NFC_PORT", "COM10")
45
+ monkeypatch.setenv("NFC_READER", "pn532")
46
+ mock_create.return_value = _mock_reader()
47
+ nfc.connect()
48
+ mock_create.assert_called_once_with("pn532", transport="serial", port="COM10")
49
+
50
+ def test_connect_no_port_raises(self, monkeypatch):
51
+ monkeypatch.delenv("NFC_PORT", raising=False)
52
+ monkeypatch.delenv("NFC_READER", raising=False)
53
+ with pytest.raises(ValueError, match="未指定串口"):
54
+ nfc.connect(reader_type="pn532")
55
+
56
+ def test_connect_no_reader_type_raises(self, monkeypatch):
57
+ monkeypatch.delenv("NFC_PORT", raising=False)
58
+ monkeypatch.delenv("NFC_READER", raising=False)
59
+ with pytest.raises(ValueError, match="未指定读卡器类型"):
60
+ nfc.connect(port="COM99")
61
+
62
+ @patch.object(CardReaderRegistry, "create")
63
+ def test_connect_overrides_env(self, mock_create, monkeypatch):
64
+ monkeypatch.setenv("NFC_PORT", "COM10")
65
+ monkeypatch.setenv("NFC_READER", "clrc663")
66
+ mock_create.return_value = _mock_reader()
67
+ nfc.connect(port="COM99", reader_type="pn532")
68
+ mock_create.assert_called_once_with("pn532", transport="serial", port="COM99")
69
+
70
+
71
+ class TestGetReader:
72
+
73
+ def test_get_reader_when_connected(self):
74
+ mock = _mock_reader()
75
+ reader._target = mock
76
+ reader._connected = True
77
+ assert nfc.get_reader() is mock
78
+ assert reader.target is mock
79
+
80
+ def test_get_reader_when_not_connected(self, monkeypatch):
81
+ monkeypatch.delenv("NFC_PORT", raising=False)
82
+ monkeypatch.delenv("NFC_READER", raising=False)
83
+ with pytest.raises(AssertionError):
84
+ nfc.get_reader()
85
+
86
+
87
+ class TestTransceive:
88
+
89
+ def test_transceive_returns_data(self):
90
+ mock = _mock_reader()
91
+ reader._target = mock
92
+ reader._connected = True
93
+ result = nfc.transceive([0x30, 0x00])
94
+ assert result == [0xAA, 0xBB]
95
+ mock.transceive.assert_called_once_with([0x30, 0x00], tx_crc=True, rx_crc=True, timeout=0.5)
96
+
97
+ def test_transceive_no_crc(self):
98
+ mock = _mock_reader()
99
+ reader._target = mock
100
+ reader._connected = True
101
+ nfc.transceive([0x30], tx_crc=False, rx_crc=False)
102
+ mock.transceive.assert_called_once_with([0x30], tx_crc=False, rx_crc=False, timeout=0.5)
103
+
104
+ def test_transceive_with_timeout(self):
105
+ mock = _mock_reader()
106
+ reader._target = mock
107
+ reader._connected = True
108
+ nfc.transceive([0x30], timeout=1.2)
109
+ mock.transceive.assert_called_once_with([0x30], tx_crc=True, rx_crc=True, timeout=1.2)
110
+
111
+ def test_transceive_empty_result(self):
112
+ mock = _mock_reader()
113
+ mock.transceive.return_value = None
114
+ reader._target = mock
115
+ reader._connected = True
116
+ result = nfc.transceive([0x30])
117
+ assert result == []
118
+
119
+ def test_transceive_not_connected(self, monkeypatch):
120
+ monkeypatch.delenv("NFC_PORT", raising=False)
121
+ monkeypatch.delenv("NFC_READER", raising=False)
122
+ with pytest.raises(AssertionError):
123
+ nfc.transceive([0x30])
124
+
125
+
126
+ class TestTransceiveBits:
127
+
128
+ def test_transceive_bits_returns_result(self):
129
+ mock = _mock_reader()
130
+ reader._target = mock
131
+ reader._connected = True
132
+ result = nfc.transceive_bits([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
133
+ assert result is not None
134
+ assert result.data == [0xAA, 0xBB]
135
+
136
+ def test_transceive_bits_passes_params(self):
137
+ mock = _mock_reader()
138
+ reader._target = mock
139
+ reader._connected = True
140
+ nfc.transceive_bits([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
141
+ mock.transceive.assert_called_once_with([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False, timeout=0.5)
142
+
143
+
144
+ class TestReqa:
145
+
146
+ @patch("nfc.reader.transceive_bits")
147
+ def test_reqa_default(self, mock_tb):
148
+ mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
149
+ result = nfc.reqa()
150
+ mock_tb.assert_called_once_with([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
151
+ assert result is not None
152
+
153
+ @patch("nfc.reader.transceive_bits")
154
+ def test_reqa_custom_cmd(self, mock_tb):
155
+ mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
156
+ nfc.reqa(cmd=0x20)
157
+ mock_tb.assert_called_once_with([0x20], last_tx_bits=7, tx_crc=False, rx_crc=False)
158
+
159
+
160
+ class TestWupa:
161
+
162
+ @patch("nfc.reader.transceive_bits")
163
+ def test_wupa(self, mock_tb):
164
+ mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
165
+ result = nfc.wupa()
166
+ mock_tb.assert_called_once_with([0x52], last_tx_bits=7, tx_crc=False, rx_crc=False)
167
+ assert result is not None
168
+
169
+
170
+ class TestHalt:
171
+
172
+ @patch("nfc.reader.transceive")
173
+ def test_halt(self, mock_ts):
174
+ mock_ts.return_value = []
175
+ result = nfc.halt()
176
+ mock_ts.assert_called_once_with([0x50, 0x00], tx_crc=True, rx_crc=True)
177
+
178
+
179
+ class TestSelect:
180
+
181
+ @patch("nfc.reader.transceive")
182
+ def test_select_cl1(self, mock_ts):
183
+ mock_ts.return_value = [0x08]
184
+ result = nfc.select(cl_level=1, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
185
+ mock_ts.assert_called_once_with(
186
+ [0x93, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
187
+ tx_crc=True, rx_crc=True
188
+ )
189
+ assert result == [0x08]
190
+
191
+ @patch("nfc.reader.transceive")
192
+ def test_select_cl2(self, mock_ts):
193
+ mock_ts.return_value = [0x08]
194
+ nfc.select(cl_level=2, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
195
+ mock_ts.assert_called_once_with(
196
+ [0x95, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
197
+ tx_crc=True, rx_crc=True
198
+ )
199
+
200
+ @patch("nfc.reader.transceive")
201
+ def test_select_cl3(self, mock_ts):
202
+ mock_ts.return_value = [0x08]
203
+ nfc.select(cl_level=3, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
204
+ mock_ts.assert_called_once_with(
205
+ [0x97, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
206
+ tx_crc=True, rx_crc=True
207
+ )
208
+
209
+ def test_select_invalid_cl(self):
210
+ with pytest.raises(ValueError, match="Invalid CL level"):
211
+ nfc.select(cl_level=4, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
212
+
213
+
214
+ class TestAnticoll:
215
+
216
+ @patch("nfc.reader.transceive_bits")
217
+ def test_anticoll_cl1(self, mock_tb):
218
+ mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
219
+ result = nfc.anticoll(cl_level=1)
220
+ mock_tb.assert_called_once_with([0x93, 0x20], last_tx_bits=0, tx_crc=False, rx_crc=False)
221
+ assert result is not None
222
+
223
+ @patch("nfc.reader.transceive_bits")
224
+ def test_anticoll_cl2(self, mock_tb):
225
+ mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
226
+ nfc.anticoll(cl_level=2)
227
+ mock_tb.assert_called_once_with([0x95, 0x20], last_tx_bits=0, tx_crc=False, rx_crc=False)
228
+
229
+ @patch("nfc.reader.transceive_bits")
230
+ def test_anticoll_with_uid_prefix(self, mock_tb):
231
+ mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
232
+ nfc.anticoll(cl_level=1, uid_prefix=[0x04, 0x12, 0x34, 0x56])
233
+ mock_tb.assert_called_once_with(
234
+ [0x93, 0x20, 0x04, 0x12, 0x34, 0x56],
235
+ last_tx_bits=0, tx_crc=False, rx_crc=False
236
+ )
237
+
238
+
239
+ class TestFieldControl:
240
+
241
+ def test_field_on(self):
242
+ mock = _mock_reader()
243
+ reader._target = mock
244
+ reader._connected = True
245
+ nfc.field_on()
246
+ assert mock.rf_field is True
247
+
248
+ def test_field_off(self):
249
+ mock = _mock_reader()
250
+ reader._target = mock
251
+ reader._connected = True
252
+ nfc.field_off()
253
+ assert mock.rf_field is False
254
+
255
+
256
+ class TestSession:
257
+
258
+ @patch("nfc.reader.disconnect")
259
+ @patch("nfc.reader.connect")
260
+ def test_session_yields_reader(self, mock_connect, mock_disconnect):
261
+ mock_rdr = _mock_reader()
262
+
263
+ def fake_connect(port="COM20", reader_type="pn532"):
264
+ reader._target = mock_rdr
265
+ reader._connected = True
266
+
267
+ mock_connect.side_effect = fake_connect
268
+
269
+ with nfc.session(port="COM99", reader_type="pn532") as r:
270
+ assert r is reader
271
+ mock_disconnect.assert_called_once()
272
+
273
+ def test_session_no_port_raises(self, monkeypatch):
274
+ monkeypatch.delenv("NFC_PORT", raising=False)
275
+ monkeypatch.delenv("NFC_READER", raising=False)
276
+ with pytest.raises(ValueError, match="未指定串口"):
277
+ with nfc.session(reader_type="pn532"):
278
+ pass
279
+
280
+ def test_session_no_reader_type_raises(self, monkeypatch):
281
+ monkeypatch.delenv("NFC_PORT", raising=False)
282
+ monkeypatch.delenv("NFC_READER", raising=False)
283
+ with pytest.raises(ValueError, match="未指定读卡器类型"):
284
+ with nfc.session(port="COM99"):
285
+ pass
286
+
287
+
288
+ class TestGetSelCmd:
289
+
290
+ def test_cl1(self):
291
+ from nfc.reader import _get_sel_cmd
292
+ assert _get_sel_cmd(1) == 0x93
293
+
294
+ def test_cl2(self):
295
+ from nfc.reader import _get_sel_cmd
296
+ assert _get_sel_cmd(2) == 0x95
297
+
298
+ def test_cl3(self):
299
+ from nfc.reader import _get_sel_cmd
300
+ assert _get_sel_cmd(3) == 0x97
301
+
302
+ def test_invalid(self):
303
+ from nfc.reader import _get_sel_cmd
304
+ with pytest.raises(ValueError):
305
+ _get_sel_cmd(4)
306
+
307
+
308
+ class TestReaderSingletonProxy:
309
+
310
+ @patch.object(CardReaderRegistry, "create")
311
+ def test_reader_rf_property_and_lazy_connect(self, mock_create, monkeypatch):
312
+ monkeypatch.setenv("NFC_PORT", "COM10")
313
+ monkeypatch.setenv("NFC_READER", "pn532")
314
+ mock_rdr = _mock_reader()
315
+ mock_create.return_value = mock_rdr
316
+
317
+ # reader.rf triggers auto-connect
318
+ reader.rf = True
319
+ assert mock_rdr.rf_field is True
320
+ assert reader.rf is True
321
+ assert reader.rf_field is True
322
+ assert reader.mf_crypto is False
323
+
324
+ @patch.object(CardReaderRegistry, "create")
325
+ def test_reader_delegates_to_functions_and_target(self, mock_create, monkeypatch):
326
+ monkeypatch.setenv("NFC_PORT", "COM10")
327
+ monkeypatch.setenv("NFC_READER", "pn532")
328
+ mock_rdr = _mock_reader()
329
+ mock_create.return_value = mock_rdr
330
+
331
+ # reader.active calls active()
332
+ card = reader.active()
333
+ assert card is not None
334
+ assert card.uid == [0x01, 0x02, 0x03, 0x04]
335
+
336
+ # reader.custom_method delegates to mock_rdr
337
+ mock_rdr.custom_method = MagicMock(return_value="OK")
338
+ assert reader.custom_method() == "OK"
@@ -1,275 +0,0 @@
1
- """reader 模块测试 (connect / get_reader / active / transceive / session 等)"""
2
- import pytest
3
- from unittest.mock import MagicMock, patch, PropertyMock
4
- from nfc import reader
5
- from nfctester.drivers.card_reader import CardInfo, TransceiveBits
6
-
7
-
8
- @pytest.fixture(autouse=True)
9
- def reset_state():
10
- """每个测试前重置全局状态"""
11
- reader._state._reader = None
12
- reader._state._connected = False
13
- yield
14
- reader._state._reader = None
15
- reader._state._connected = False
16
-
17
-
18
- def _mock_reader():
19
- mock = MagicMock()
20
- mock.transceive.return_value = TransceiveBits(data=[0xAA, 0xBB], bits=8)
21
- mock.active.return_value = CardInfo(uid=[0x01, 0x02, 0x03, 0x04], atq=[0x44, 0x00], sak=0x00)
22
- return mock
23
-
24
-
25
- class TestConnect:
26
-
27
- @patch.object(reader.CardReaderRegistry, "create")
28
- def test_connect_with_params(self, mock_create):
29
- mock_reader = _mock_reader()
30
- mock_create.return_value = mock_reader
31
- reader.connect(port="COM99", reader_type="pn532")
32
- mock_create.assert_called_once_with("pn532", transport="serial", port="COM99")
33
- mock_reader.open.assert_called_once()
34
- assert reader._state._connected is True
35
-
36
- @patch.object(reader.CardReaderRegistry, "create")
37
- def test_connect_from_env(self, mock_create, monkeypatch):
38
- monkeypatch.setenv("NFC_PORT", "COM10")
39
- monkeypatch.setenv("NFC_READER", "pn532")
40
- mock_create.return_value = _mock_reader()
41
- reader.connect()
42
- mock_create.assert_called_once_with("pn532", transport="serial", port="COM10")
43
-
44
- def test_connect_no_port_raises(self):
45
- with pytest.raises(ValueError, match="未指定串口"):
46
- reader.connect(reader_type="pn532")
47
-
48
- def test_connect_no_reader_type_raises(self):
49
- with pytest.raises(ValueError, match="未指定读卡器类型"):
50
- reader.connect(port="COM99")
51
-
52
- @patch.object(reader.CardReaderRegistry, "create")
53
- def test_connect_overrides_env(self, mock_create, monkeypatch):
54
- monkeypatch.setenv("NFC_PORT", "COM10")
55
- monkeypatch.setenv("NFC_READER", "clrc663")
56
- mock_create.return_value = _mock_reader()
57
- reader.connect(port="COM99", reader_type="pn532")
58
- mock_create.assert_called_once_with("pn532", transport="serial", port="COM99")
59
-
60
-
61
- class TestGetReader:
62
-
63
- def test_get_reader_when_connected(self):
64
- mock = _mock_reader()
65
- reader._state._reader = mock
66
- reader._state._connected = True
67
- assert reader.get_reader() is mock
68
-
69
- def test_get_reader_when_not_connected(self):
70
- with pytest.raises(AssertionError):
71
- reader.get_reader()
72
-
73
-
74
- class TestTransceive:
75
-
76
- def test_transceive_returns_data(self):
77
- mock = _mock_reader()
78
- reader._state._reader = mock
79
- reader._state._connected = True
80
- result = reader.transceive([0x30, 0x00])
81
- assert result == [0xAA, 0xBB]
82
- mock.transceive.assert_called_once_with([0x30, 0x00], tx_crc=True, rx_crc=True)
83
-
84
- def test_transceive_no_crc(self):
85
- mock = _mock_reader()
86
- reader._state._reader = mock
87
- reader._state._connected = True
88
- reader.transceive([0x30], tx_crc=False, rx_crc=False)
89
- mock.transceive.assert_called_once_with([0x30], tx_crc=False, rx_crc=False)
90
-
91
- def test_transceive_empty_result(self):
92
- mock = _mock_reader()
93
- mock.transceive.return_value = None
94
- reader._state._reader = mock
95
- reader._state._connected = True
96
- result = reader.transceive([0x30])
97
- assert result == []
98
-
99
- def test_transceive_not_connected(self):
100
- with pytest.raises(AssertionError):
101
- reader.transceive([0x30])
102
-
103
-
104
- class TestTransceiveBits:
105
-
106
- def test_transceive_bits_returns_result(self):
107
- mock = _mock_reader()
108
- reader._state._reader = mock
109
- reader._state._connected = True
110
- result = reader.transceive_bits([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
111
- assert result is not None
112
- assert result.data == [0xAA, 0xBB]
113
-
114
- def test_transceive_bits_passes_params(self):
115
- mock = _mock_reader()
116
- reader._state._reader = mock
117
- reader._state._connected = True
118
- reader.transceive_bits([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
119
- mock.transceive.assert_called_once_with([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
120
-
121
-
122
- class TestReqa:
123
-
124
- @patch.object(reader, "transceive_bits")
125
- def test_reqa_default(self, mock_tb):
126
- mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
127
- result = reader.reqa()
128
- mock_tb.assert_called_once_with([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
129
- assert result is not None
130
-
131
- @patch.object(reader, "transceive_bits")
132
- def test_reqa_custom_cmd(self, mock_tb):
133
- mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
134
- reader.reqa(cmd=0x20)
135
- mock_tb.assert_called_once_with([0x20], last_tx_bits=7, tx_crc=False, rx_crc=False)
136
-
137
-
138
- class TestWupa:
139
-
140
- @patch.object(reader, "transceive_bits")
141
- def test_wupa(self, mock_tb):
142
- mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
143
- result = reader.wupa()
144
- mock_tb.assert_called_once_with([0x52], last_tx_bits=7, tx_crc=False, rx_crc=False)
145
- assert result is not None
146
-
147
-
148
- class TestHalt:
149
-
150
- @patch.object(reader, "transceive")
151
- def test_halt(self, mock_ts):
152
- mock_ts.return_value = []
153
- result = reader.halt()
154
- mock_ts.assert_called_once_with([0x50, 0x00], tx_crc=True, rx_crc=True)
155
-
156
-
157
- class TestSelect:
158
-
159
- @patch.object(reader, "transceive")
160
- def test_select_cl1(self, mock_ts):
161
- mock_ts.return_value = [0x08]
162
- result = reader.select(cl_level=1, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
163
- mock_ts.assert_called_once_with(
164
- [0x93, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
165
- tx_crc=True, rx_crc=True
166
- )
167
- assert result == [0x08]
168
-
169
- @patch.object(reader, "transceive")
170
- def test_select_cl2(self, mock_ts):
171
- mock_ts.return_value = [0x08]
172
- reader.select(cl_level=2, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
173
- mock_ts.assert_called_once_with(
174
- [0x95, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
175
- tx_crc=True, rx_crc=True
176
- )
177
-
178
- @patch.object(reader, "transceive")
179
- def test_select_cl3(self, mock_ts):
180
- mock_ts.return_value = [0x08]
181
- reader.select(cl_level=3, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
182
- mock_ts.assert_called_once_with(
183
- [0x97, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
184
- tx_crc=True, rx_crc=True
185
- )
186
-
187
- def test_select_invalid_cl(self):
188
- with pytest.raises(ValueError, match="Invalid CL level"):
189
- reader.select(cl_level=4, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
190
-
191
-
192
- class TestAnticoll:
193
-
194
- @patch.object(reader, "transceive_bits")
195
- def test_anticoll_cl1(self, mock_tb):
196
- mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
197
- result = reader.anticoll(cl_level=1)
198
- mock_tb.assert_called_once_with([0x93, 0x20], last_tx_bits=0, tx_crc=False, rx_crc=False)
199
- assert result is not None
200
-
201
- @patch.object(reader, "transceive_bits")
202
- def test_anticoll_cl2(self, mock_tb):
203
- mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
204
- reader.anticoll(cl_level=2)
205
- mock_tb.assert_called_once_with([0x95, 0x20], last_tx_bits=0, tx_crc=False, rx_crc=False)
206
-
207
- @patch.object(reader, "transceive_bits")
208
- def test_anticoll_with_uid_prefix(self, mock_tb):
209
- mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
210
- reader.anticoll(cl_level=1, uid_prefix=[0x04, 0x12, 0x34, 0x56])
211
- mock_tb.assert_called_once_with(
212
- [0x93, 0x20, 0x04, 0x12, 0x34, 0x56],
213
- last_tx_bits=0, tx_crc=False, rx_crc=False
214
- )
215
-
216
-
217
- class TestFieldControl:
218
-
219
- def test_field_on(self):
220
- mock = _mock_reader()
221
- reader._state._reader = mock
222
- reader._state._connected = True
223
- reader.field_on()
224
- assert mock.rf_field is True
225
-
226
- def test_field_off(self):
227
- mock = _mock_reader()
228
- reader._state._reader = mock
229
- reader._state._connected = True
230
- reader.field_off()
231
- assert mock.rf_field is False
232
-
233
-
234
- class TestSession:
235
-
236
- @patch.object(reader._state, "disconnect")
237
- @patch.object(reader._state, "connect")
238
- def test_session_yields_reader(self, mock_connect, mock_disconnect):
239
- mock_rdr = _mock_reader()
240
-
241
- def fake_connect(port="COM20", reader_type="pn532"):
242
- reader._state._reader = mock_rdr
243
- reader._state._connected = True
244
-
245
- mock_connect.side_effect = fake_connect
246
-
247
- with reader.session(port="COM99", reader_type="pn532") as r:
248
- assert r is mock_rdr
249
- mock_disconnect.assert_called_once()
250
-
251
- def test_session_no_port_raises(self):
252
- with pytest.raises(ValueError, match="未指定串口"):
253
- with reader.session(reader_type="pn532"):
254
- pass
255
-
256
- def test_session_no_reader_type_raises(self):
257
- with pytest.raises(ValueError, match="未指定读卡器类型"):
258
- with reader.session(port="COM99"):
259
- pass
260
-
261
-
262
- class TestGetSelCmd:
263
-
264
- def test_cl1(self):
265
- assert reader._get_sel_cmd(1) == 0x93
266
-
267
- def test_cl2(self):
268
- assert reader._get_sel_cmd(2) == 0x95
269
-
270
- def test_cl3(self):
271
- assert reader._get_sel_cmd(3) == 0x97
272
-
273
- def test_invalid(self):
274
- with pytest.raises(ValueError):
275
- reader._get_sel_cmd(4)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes