v3.22.0 (review fixes): the suite members share the SCP80 counter; drain/PWA nits
The v3.22.0 review found that a suite's SCP80 counter state was rebuilt from the preset snapshot for every member: a second SCP80 script sent the counter the first had consumed (the store persists the advance, the in-memory preset copy does not move) and the card answered cntr_low. Reproduced with a two-member suite (both packets carried CNTR=0000000010). - runner: `_test_run_script` takes a `shared` counter state (created once per suite, passed to every member; a single script passes None) and copies the no-keyset counter back into it, so a suite behaves like one script for counter bookkeeping. Pinned by a two-member test asserting the second packet's counter is the first's + 1 and the final counters. - `_test_drain_pending`: the unknown-length FETCH uses 0xff - `'%02x' % 0x100` emitted an odd-length APDU (a malformed fetch) when a 9100 SW (length 0) reached the drain. - PWA: the suite save adopts the stored suite (the server normalises the setup-first/teardown-last order); the role rows' up/down buttons are disabled (only members move); deleting a suite cancels its pending name debounce (no late 404); `refreshDynamicI18n` redraws the test views, so a language switch updates the suites list/report immediately. - tests: the cross-member counter test (Python) and the save-adoption / move-guard / i18n wiring tests (Node). Tests: 835 frontend + 795 Python; sw cache simple-v398.
This commit is contained in:
@@ -6873,8 +6873,10 @@ def _test_drain_pending(scc, fetch_len, result=0x00, max_commands=3):
|
||||
answered = []
|
||||
try:
|
||||
while len(answered) < max_commands:
|
||||
# fetch_len 0 (a 9100 SW) means "length unknown": fetch the full
|
||||
# 255 bytes - `%02x` of 0x100 would emit an odd-length APDU
|
||||
fdata, sw = scc._tp.send_apdu('%s120000%02x' % (scc.cat_cla,
|
||||
fetch_len or 0x100))
|
||||
fetch_len or 0xff))
|
||||
if not fdata:
|
||||
break
|
||||
raw = bytes.fromhex(fdata)
|
||||
@@ -6904,16 +6906,24 @@ def _test_drain_pending(scc, fetch_len, result=0x00, max_commands=3):
|
||||
for a in answered)}
|
||||
|
||||
|
||||
def _test_run_script(server, script, preset):
|
||||
def _test_run_script(server, script, preset, shared=None):
|
||||
"""One script's steps - the single-script run or one suite member. The
|
||||
live steps land in `_TEST_RUN['steps']` (the snapshot the PWA renders),
|
||||
the semantic lines in `_TEST_RUN['log']` (the report's excerpt). Returns
|
||||
``(ctx, stopped)``: the per-script context (preset, per-keyset counters)
|
||||
and whether the stop request cut the script short."""
|
||||
and whether the stop request cut the script short.
|
||||
|
||||
`shared` carries the counter state across a suite's members (the SCP80
|
||||
counter is per keyset and per card session: a second member must start
|
||||
from what the first consumed - the server persisted it, but the in-memory
|
||||
preset copy is the suite-start snapshot). The dict is created once per
|
||||
suite and reused; a single script passes None."""
|
||||
shared = shared if shared is not None else {}
|
||||
scc = server.scc
|
||||
ctx = {'preset': dict(preset or {}),
|
||||
'counter': str((preset or {}).get('counter') or (preset or {}).get('cntr') or '').upper(),
|
||||
'counters': {},
|
||||
'counter': (shared.get('counter')
|
||||
or str((preset or {}).get('counter') or (preset or {}).get('cntr') or '').upper()),
|
||||
'counters': shared.setdefault('counters', {}),
|
||||
'uses_scp80': any(s['type'] == 'action' and s['kind'] == 'scp80'
|
||||
for s in script['steps'])}
|
||||
if ctx['uses_scp80']:
|
||||
@@ -6984,6 +6994,9 @@ def _test_run_script(server, script, preset):
|
||||
with _TEST_LOCK:
|
||||
_TEST_RUN['scp80_counter'] = ctx['counter']
|
||||
_TEST_RUN['scp80_counters'] = dict(ctx.get('counters') or {})
|
||||
# The no-keyset counter is a plain value in the ctx: carry it into the
|
||||
# shared state so the next suite member starts where this one ended.
|
||||
shared['counter'] = ctx.get('counter') or ''
|
||||
return ctx, stopped
|
||||
|
||||
|
||||
@@ -7180,6 +7193,11 @@ def _test_suite_execute(server, suite, members, preset, preset_id):
|
||||
teardown_i = next((i for i, (e, _s) in enumerate(members)
|
||||
if e['role'] == 'teardown'), None)
|
||||
member_idx = [i for i, (e, _s) in enumerate(members) if e['role'] == 'member']
|
||||
# The counter state is per card session: one shared dict for the whole
|
||||
# suite, so a second SCP80 script starts from what the first consumed
|
||||
# (the store persists each packet's counter; the preset dict here is the
|
||||
# suite-start snapshot).
|
||||
shared = {}
|
||||
|
||||
def run_member(i):
|
||||
entry, script = members[i]
|
||||
@@ -7192,7 +7210,7 @@ def _test_suite_execute(server, suite, members, preset, preset_id):
|
||||
_TEST_RUN['total'] = len(script['steps'])
|
||||
_TEST_RUN['log_prefix'] = _test_member_prefix(i, member)
|
||||
log_from = len(_TEST_RUN['log'])
|
||||
ctx, member_stopped = _test_run_script(server, script, preset)
|
||||
ctx, member_stopped = _test_run_script(server, script, preset, shared=shared)
|
||||
with _TEST_LOCK:
|
||||
steps = json.loads(json.dumps(_TEST_RUN['steps']))
|
||||
log = list(_TEST_RUN['log'][log_from:])
|
||||
|
||||
Reference in New Issue
Block a user