v3.25.1: the run-ownership fix for the proactive auto-handling

A card that re-queues a command immediately after each accepted TR
(live 2026-10-11: the EP BAP card re-issued the same PROVIDE LOCAL
INFORMATION 1100 times) turned the background poll's FETCH/TR chain
into an unbounded loop that held _CARD_LOCK forever - every card
operation, a test run's start included, blocked behind it.

The chain now yields:
- _handle_proactive_chain aborts before the next FETCH while a test run
  owns the card (_TEST_RUNNING) - the pending command stays for the
  script - and takes max_rounds; the background poll passes
  _POLL_CHAIN_MAX = 10, so a chatty card cannot hold the lock.
- _do_status_poll re-arms on its run skip (a heartbeat that never
  fetches); _timer_expired and the BIP Data-available event wait during
  a run; _bip_flush_channel_events stays quiet too.
- _sim_resume_poll() resumes polling at every run end - the two worker
  bodies, their exception paths and the spawn failure.

Also the TR command-details qualifier echo (TS 102 223 6.8.1: the
object shall be identical to the command's): _build_tr,
testscript.build_tr and the interactive _menu_send_response echoed a
hardcoded 00 - harmless for the loop's qual-00 PLI, wrong for any
other qualifier.  The menu pendings now carry cmd_qual (parsed in the
on_fetch handler).

Tests: the chain bound/abort (a fake card re-queueing a command), the
poll/timer guards, the resume wiring, the qualifier echo in both
builders.  883 frontend + 865 Python; sw cache simple-v425.
This commit is contained in:
2026-10-11 12:23:12 +03:00
parent 87931afc41
commit af00932aca
10 changed files with 4743 additions and 32 deletions
+19 -4
View File
@@ -863,6 +863,20 @@ class TestProactiveDecode(unittest.TestCase):
tr = _build_tr(None, 1, 0x26, 0x81, 0x82, 0x00)
self.assertEqual(tr.hex(), '81030126008202828103010093055210011000')
def test_build_tr_echoes_the_command_qualifier(self):
# TS 102 223 6.8.1: the TR's command details shall be identical to
# the command's (including the qualifier); without one they stay 00
self.assertTrue(_build_tr(None, 1, 0x26, 0x81, 0x82, 0x06)
.hex().startswith('8103012606'))
self.assertTrue(_build_tr(None, 1, 0x03, 0x81, 0x82, None)
.hex().startswith('8103010300'))
# the runner's builder echoes the same way
from pysim_simple_server import testscript as T
self.assertTrue(T.build_tr(1, 0x26, 0x82, 0x81, {}, cmd_qual=0x06)
.hex().startswith('8103012606'))
self.assertTrue(T.build_tr(1, 0x26, 0x82, 0x81, {})
.hex().startswith('8103012600'))
def test_build_tr_poll_interval_duration_after_result(self):
import pysim_simple_server.server as srv
tr = _build_tr(None, 1, 0x03, 0x81, 0x82, None)
@@ -884,11 +898,12 @@ class TestProactiveDecode(unittest.TestCase):
tr_west = _build_tr(None, 1, 0x26, 0x81, 0x82, 0x03, west)
finally:
srv._PLI_DATA[0x03] = saved
# the command details echo the qualifier (TS 102 223 6.8.1)
self.assertEqual(tr_east.hex(),
'810301260082028281030100260762900312550021')
'810301260382028281030100260762900312550021')
# negative zone: the sign bit of the time zone byte (0x08)
self.assertEqual(tr_west.hex(),
'810301260082028281030100260762105080709009')
'810301260382028281030100260762105080709009')
def test_build_tr_pli_dict_entry_overrides_host_clock(self):
import pysim_simple_server.server as srv
@@ -900,7 +915,7 @@ class TestProactiveDecode(unittest.TestCase):
datetime(2026, 9, 30, 21, 55, tzinfo=timezone.utc))
finally:
srv._PLI_DATA[0x03] = saved
self.assertEqual(tr.hex(), '810301260082028281030100130752f01000ff0001')
self.assertEqual(tr.hex(), '810301260382028281030100130752f01000ff0001')
def test_build_tr_pli_empty_other_qualifier_sends_no_data(self):
# a qualifier with neither an override nor a built-in default (02
@@ -913,7 +928,7 @@ class TestProactiveDecode(unittest.TestCase):
data = srv._pli_data_hex(0x02)
finally:
srv._PLI_DATA[0x02] = saved
self.assertEqual(tr.hex(), '810301260082028281030100')
self.assertEqual(tr.hex(), '810301260282028281030100')
self.assertEqual(data, '')
def test_pli_defaults_cover_the_common_qualifiers(self):