v3.21.0: the test-runner tolerance batch + the proactive cleanup action

Five reported issues, plus the action that makes an explicit drain possible:

- on_fail is honoured by every step failure: an expectation with no pending
  command (or a FETCH that returns nothing) fails as a Command/Fetch check at
  the step's level instead of raising past it; the default still stops the
  run, on_fail "warning" continues (the tolerant consume-if-pending form).
- the run cleanup answers ok (0x00) and keeps draining while the TR answers
  91XX (bounded); a cancel is only sent when a script asks for it - it tells
  the card the user aborted the session and pushes assertive applets onto an
  error path.
- new `proactive` action: STATUS rounds consume every announced command
  (respond/first TERMINAL RESPONSE results, attempts/interval_ms bound) until
  the card answers 9000; an optional require asserts at least one drained
  command (an empty drain fails it), the final SW is the step's SW check and
  a still-pending command is handed to the next step.  PWA editor + summary +
  validation, EN/RU help, docs, action-kind drift test.
- the PoR response form follows the command's TAR: _tar_is_rm (preset role
  entries + the TS 101 220 Annex D ISD/RFM allocations) decides; a non-RM
  TAR's reply is always raw (TS 102 226 4) - no fabricated status word even
  when the bytes happen to parse (01 00 0B 91 ... used to read as SW 000B).
- a secured data too short for the compact structure (a 2-byte applet reply)
  is recovered by _por_header_parse (the decode minus the compact parse; a
  malformed packet or a failed RC/CC/DS check still yields no por) and
  reported raw, so short applet replies are assertable via por.data.

Docs: test-scripts.md, api.md, help EN/RU, repo AGENTS.  sw cache simple-v396.
Tests: 825 frontend + 755 Python green.
This commit is contained in:
2026-10-05 23:36:47 +03:00
parent 82c5733906
commit e6ced47323
12 changed files with 788 additions and 100 deletions
+54
View File
@@ -25,6 +25,7 @@ from pysim_simple_server.server import (
_build_tr,
_decode_cmd,
_decode_por,
_tar_is_rm,
_decode_tr,
_log_proactive,
_ota_reference,
@@ -535,6 +536,59 @@ class TestDecodePor(unittest.TestCase):
self.assertEqual(r['decoded']['last_status_word'], '6a86')
self.assertEqual(r['secured_data'], '016A86')
def test_non_rm_tar_is_application_data(self):
# v3.21.0: the response form follows the command's TAR. A non-RM TAR
# answers with application data (TS 102 226 4); `01 00 0B 91 ...`
# happens to parse as a compact response (count 1, SW 000B) and must
# not be split into a fabricated status word (live 2026-10-05).
raw = '027100000e0aaf4d010000000001000001000B919733525088F4'
r = _decode_por('16', '01', '15', '15', '0000000001', K, K, raw, rm=False)
self.assertEqual(r['response_type'], 'raw')
self.assertEqual(r['response_status'], 'por_ok')
self.assertEqual(r['secured_data'], '01000B919733525088F4')
self.assertEqual(r['decoded']['last_response_data'], '01000B919733525088F4')
self.assertEqual(r['decoded']['last_status_word'], '')
self.assertIsNone(r['decoded']['number_of_commands'])
# without the TAR context the same bytes still parse as compact
r = _decode_por('16', '01', '15', '15', '0000000001', K, K, raw)
self.assertEqual(r['response_type'], 'compact')
self.assertEqual(r['decoded']['last_status_word'], '000b')
def test_short_applet_reply_falls_back_to_raw(self):
# a 2-byte secured data cannot be a compact response (TS 102 226 5.1.2
# Table 5.1 needs a count plus two status bytes): pySim's decode raises
# and the packet is reported raw instead of "undecodable"
raw = '027100000e0aaf4d01000000000100000400'
r = _decode_por('16', '01', '15', '15', '0000000001', K, K, raw)
self.assertEqual(r['response_type'], 'raw')
self.assertEqual(r['response_status'], 'por_ok')
self.assertEqual(r['secured_data'], '0400')
self.assertEqual(r['decoded']['last_response_data'], '0400')
class TestTarIsRm(unittest.TestCase):
"""v3.21.0: the response form follows the command's TAR - the preset's
role entries plus the standard ISD/RFM allocations of TS 101 220 Annex D."""
def test_standard_allocations(self):
for tar in ('000000', 'B00000', 'B00001', 'B00010', 'B20100',
'B00120', 'B00130', 'B00140'):
self.assertTrue(_tar_is_rm(None, tar), tar)
def test_application_allocations_are_not_rm(self):
# the Annex D application TARs (USAT interpreter, multiplexing, CASD,
# OMA) and any custom TAR: application-specific data (TS 102 226 4)
for tar in ('AF4D01', 'B20000', 'B20200', 'B20201', 'B20202',
'B20203', 'B00200'):
self.assertFalse(_tar_is_rm(None, tar), tar)
def test_preset_roles_win(self):
preset = {'tars': [{'role': 'isd', 'tar': 'A11300', 'msl': '16'},
{'role': '', 'tar': 'AF4D01', 'msl': '16'}]}
self.assertTrue(_tar_is_rm(preset, 'A11300'))
self.assertFalse(_tar_is_rm(preset, 'AF4D01'))
self.assertIsNone(_tar_is_rm(preset, ''))
class TestPresetCounterSeed(unittest.TestCase):
"""v3.20.0: the effective counter of an operation - explicit `cntr` wins,