v3.22.1: rename the proactive action kind to proactive-drain

The kind string `proactive` read like an expectation of a proactive command
(`expect` is the step type for that) although the action consumes and answers
them.  Renamed everywhere:

- ACTION_KINDS / TEST_ACTION_KINDS (drift-tested), the normaliser branch and
  its error strings, the dispatch and the helper names (_test_run_drain,
  _drain_require_match/_label);
- the report label is DRAIN and the PWA summary reads "DRAIN x5 require ...
  TR ok";
- the editor/collect/summary/problem branches, docs/test-scripts.md
  (Proactive drain), docs/api.md, help EN/RU ("Очистка очереди proactive");
- the tests (TestRunnerProactiveDrain, the normalisation/data-check tests).

No stored scripts used the old kind (unpublished series), so there is no
alias and no migration.  sw cache simple-v404.
Tests: 839 frontend + 804 Python green.
This commit is contained in:
2026-10-06 01:41:53 +03:00
parent e12dfc7871
commit f0a836363c
11 changed files with 78 additions and 78 deletions
+13 -13
View File
@@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.22.0'
VERSION = '3.22.1'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -5853,7 +5853,7 @@ _TEST_KIND_LABELS = {
'menu-select': 'ENVELOPE(Menu Selection)',
'file-write': 'UPDATE FILE', 'file-read': 'READ FILE', 'apdu': 'APDU',
'scp80': 'SCP80', 'status': 'STATUS', 'cleanup': 'CLEANUP',
'proactive': 'PROACTIVE',
'proactive-drain': 'DRAIN',
}
# Card-touching endpoints refused while a script owns the card.
_TEST_BLOCKED_PATHS = frozenset([
@@ -6300,8 +6300,8 @@ def _test_run_status(scc, step):
return data or '', sw, ('STATUS x%d' % used if used > 1 else 'STATUS')
def _proactive_require_match(req, consumed):
"""Whether one consumed command satisfies a proactive action's `require`
def _drain_require_match(req, consumed):
"""Whether one consumed command satisfies a proactive-drain action's `require`
spec: the command type/name (ANY matches all) and an optional qualifier
(exact/mask)."""
if req.get('type') is not None and consumed.get('type_hex') != '%02X' % req['type']:
@@ -6312,7 +6312,7 @@ def _proactive_require_match(req, consumed):
return True
def _proactive_require_label(req):
def _drain_require_label(req):
"""The `require` spec as the report's expected string."""
if req.get('type') is not None:
name = req.get('name') or '0x%02X' % req['type']
@@ -6322,8 +6322,8 @@ def _proactive_require_label(req):
return name + ((' q=%s' % q['value']) if q else '')
def _test_run_proactive(server, step):
"""The proactive cleanup action: poll STATUS and consume every announced
def _test_run_drain(server, step):
"""The proactive drain action: poll STATUS and consume every announced
command - the TERMINAL RESPONSE is `first` for the first one, `respond`
for the rest - until the card answers 9000 (idle), a non-91XX SW appears
or the attempt budget is spent. The final SW is the step's SW check; an
@@ -6376,7 +6376,7 @@ def _test_run_proactive(server, step):
step['on_fail'])]
req = p.get('require')
if req:
matched = any(_proactive_require_match(req, c) for c in consumed)
matched = any(_drain_require_match(req, c) for c in consumed)
if consumed:
actual = ', '.join(
'%s%s' % (c['type_name'],
@@ -6385,7 +6385,7 @@ def _test_run_proactive(server, step):
else:
actual = '(nothing was pending)'
checks.append(_test_check_result('Require', matched,
_proactive_require_label(req), actual,
_drain_require_label(req), actual,
step['on_fail']))
status = testscript.combine_levels([c['level'] if not c['ok'] else 'ok'
for c in checks])
@@ -6646,8 +6646,8 @@ def _test_run_action(server, step, ctx):
(' RESP=%dB' % (len(data) // 2)) if data else ''))
elif kind == 'scp80':
data, sw, sent, por, counter = _test_run_scp80(server, step, ctx)
elif kind == 'proactive':
return _test_run_proactive(server, step)
elif kind == 'proactive-drain':
return _test_run_drain(server, step)
else:
raise testscript.ScriptError('unknown action kind %r' % kind)
checks = _test_action_checks(step, sw, data, por, kind)
@@ -6759,7 +6759,7 @@ def _test_run_expect(server, step, pending, ctx=None):
if not pending:
# Nothing to fetch: the step fails at its own on_fail level. The
# tolerant "consume a pending command if there is one" form is an
# expectation with on_fail "warning" (or the proactive cleanup action,
# expectation with on_fail "warning" (or the proactive-drain action,
# which tolerates an empty card by design). The UICC announces
# pending commands in the response to a command (TS 102 221 7.4.2.1).
want = step['command'].get('name')
@@ -6936,7 +6936,7 @@ def _test_drain_pending(scc, fetch_len, result=0x00, max_commands=3):
make the application emit another, so the drain keeps fetching while the
TERMINAL RESPONSE answers 91XX (bounded by `max_commands`). A script that
wants to test the refusal path answers explicitly (an expectation's
`respond`, or the proactive action's `first`)."""
`respond`, or the `proactive-drain` action's `first`)."""
answered = []
try:
while len(answered) < max_commands:
+12 -12
View File
@@ -33,7 +33,7 @@ __all__ = [
]
ACTION_KINDS = ('envelope', 'event', 'menu-select', 'file-write', 'file-read',
'apdu', 'scp80', 'status', 'proactive')
'apdu', 'scp80', 'status', 'proactive-drain')
FAIL_LEVELS = ('error', 'warning')
POR_CHECKS = ('none', 'ok', 'any')
@@ -357,28 +357,28 @@ def _normalise_params(kind, p, command_resolver=None):
interval = p.get('interval_ms')
interval = _int(200 if interval is None else interval, 'status interval_ms', 0, 10000)
return {'attempts': attempts, 'interval_ms': interval}
if kind == 'proactive':
# The proactive cleanup: consume whatever the card announces (the
if kind == 'proactive-drain':
# The proactive drain: consume whatever the card announces (the
# TERMINAL RESPONSE is `first` for the first command, `respond` for
# the rest) and confirm the card is idle; `require` asserts what was
# drained (at least one match - an empty drain fails it).
out = {'respond': normalise_respond(p.get('respond') or {}),
'attempts': _int(3 if p.get('attempts') is None else p.get('attempts'),
'proactive attempts', 1, 1000),
'proactive-drain attempts', 1, 1000),
'interval_ms': _int(200 if p.get('interval_ms') is None
else p.get('interval_ms'),
'proactive interval_ms', 0, 10000)}
'proactive-drain interval_ms', 0, 10000)}
if p.get('first') is not None:
out['first'] = normalise_respond(p['first'])
req = p.get('require')
if req is not None:
if not isinstance(req, dict):
raise ScriptError('proactive require must be an object')
ctype, cname = _resolve_command(req.get('command'), 'proactive require',
raise ScriptError('proactive-drain require must be an object')
ctype, cname = _resolve_command(req.get('command'), 'proactive-drain require',
command_resolver)
qualifier = _check_spec(req.get('qualifier'), 'proactive require.qualifier')
qualifier = _check_spec(req.get('qualifier'), 'proactive-drain require.qualifier')
if qualifier and '?' not in qualifier['value'] and len(qualifier['value']) != 2:
raise ScriptError('proactive require.qualifier must be one byte')
raise ScriptError('proactive-drain require.qualifier must be one byte')
out['require'] = {'type': ctype, 'name': cname, 'qualifier': qualifier}
return out
raise ScriptError('unknown action kind %r' % kind)
@@ -398,11 +398,11 @@ def _normalise_check(check, kind, params):
else:
sw = {'mode': 'exact', 'value': '9000'}
data = _check_spec(check.get('data'), 'check.data')
if kind == 'proactive' and data is not None:
# The proactive cleanup's result data is the drained command list, not
if kind == 'proactive-drain' and data is not None:
# The proactive drain's result data is the drained command list, not
# a single response: a `data` check would be silently ignored, so it
# is refused here (assert the drained commands with `require`).
raise ScriptError('check.data is not used by the proactive action - '
raise ScriptError('check.data is not used by the proactive drain action - '
'assert what was drained with require, or the final '
'status word with check.sw')
por = check.get('por')