server: add --fast-init to skip redundant card resets
pysim's init_card() resets the card once per profile candidate in CardProfile.pick(), once in RuntimeState.__init__ and again in PysimApp.equip(); on typical readers each reconnect costs ~1.3s and a normal init/equip does 7-8 of them (measured: equip 9.85s, startup ~14.8s). fastinit.py mirrors pySim.app.init_card() with the resets removed: pick_profile_no_reset() runs all profile probes back-to-back on one connection, FastRuntimeState.reset() is a software reset (select MF, clear selected_adf/scp, ATR from the transport) and the equip/reset commands are routed through do_equip_fast (one reconnect via wait_for_card) and do_reset_fast (always a physical reset). Enabled with --fast-init; stock behavior remains the default. Tests for the reset-free pick, the soft reset and the explicit reset paths. Docs updated; SW cache v88 -> v89.
This commit is contained in:
@@ -626,7 +626,9 @@ pysim-otaman-server --http-port 8080
|
|||||||
| `--log-requests` | Log request/response payloads to stderr |
|
| `--log-requests` | Log request/response payloads to stderr |
|
||||||
| `--sms-oa` / `--sms-sm-sc` | SMS-DELIVER originating address / SM-SC for PoR-in-submit |
|
| `--sms-oa` / `--sms-sm-sc` | SMS-DELIVER originating address / SM-SC for PoR-in-submit |
|
||||||
| `--terminal-profile` | TERMINAL PROFILE payload hex (default 10-byte GSM profile) |
|
| `--terminal-profile` | TERMINAL PROFILE payload hex (default 10-byte GSM profile) |
|
||||||
| `--poll-interval` | Idle interval before automatic STATUS polling (default 30s) |
|
| `--poll-interval` | Idle interval before automatic STATUS polling (default 30s; `0` disables polling) |
|
||||||
|
| `--fast-init` | Init/equip without redundant card resets (only explicit equip/reset resets the card) |
|
||||||
|
| `--timing` | Log phase durations, card resets and APDU counters with elapsed timestamps |
|
||||||
|
|
||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -606,7 +606,9 @@ pysim-otaman-server --http-port 8080
|
|||||||
| `--no-card-init` | Пропустить инициализацию карты (сохранить CAT-сессию) |
|
| `--no-card-init` | Пропустить инициализацию карты (сохранить CAT-сессию) |
|
||||||
| `--apdu-trace` | Лог APDU-трафика в stderr |
|
| `--apdu-trace` | Лог APDU-трафика в stderr |
|
||||||
| `--log-requests` | Лог запросов/ответов в stderr |
|
| `--log-requests` | Лог запросов/ответов в stderr |
|
||||||
| `--poll-interval` | Интервал автоопроса STATUS (по умолчанию 30с) |
|
| `--poll-interval` | Интервал автоопроса STATUS (по умолчанию 30с; `0` отключает опрос) |
|
||||||
|
| `--fast-init` | Инициализация/equip без лишних сбросов карты (сброс только по явным equip/reset) |
|
||||||
|
| `--timing` | Лог длительности фаз, сбросов карты и счётчиков APDU с отметками времени |
|
||||||
|
|
||||||
### Устранение неполадок
|
### Устранение неполадок
|
||||||
|
|
||||||
|
|||||||
@@ -362,7 +362,7 @@
|
|||||||
<p class="text-sm mb-3">Значения хранятся на сервере до перезапуска. Когда карта выдаёт PLI, сервер вставляет значения словаря в TERMINAL RESPONSE.</p>
|
<p class="text-sm mb-3">Значения хранятся на сервере до перезапуска. Когда карта выдаёт PLI, сервер вставляет значения словаря в TERMINAL RESPONSE.</p>
|
||||||
|
|
||||||
<h4 id="status-polling" class="font-medium mb-1">5.5.5 Опрос STATUS</h4>
|
<h4 id="status-polling" class="font-medium mb-1">5.5.5 Опрос STATUS</h4>
|
||||||
<p class="text-sm mb-3">Кнопка <strong>Отправить STATUS</strong> отправляет STATUS (F2) вручную. Переключатель <strong>Опрос</strong> включает фоновый опрос: после настраиваемого интервала бездействия (аргумент сервера <code class="font-mono text-sm">--poll-interval</code>, 1–255 с, по умолчанию 30 с) сервер отправляет STATUS и обрабатывает любую ожидающую проактивную команду. При извлечении карты опрос останавливается, а состояние карты сбрасывается.</p>
|
<p class="text-sm mb-3">Кнопка <strong>Отправить STATUS</strong> отправляет STATUS (F2) вручную. Переключатель <strong>Опрос</strong> включает фоновый опрос: после настраиваемого интервала бездействия (аргумент сервера <code class="font-mono text-sm">--poll-interval</code>, 1–255 с, по умолчанию 30 с, <code class="font-mono text-sm">0</code> отключает опрос) сервер отправляет STATUS и обрабатывает любую ожидающую проактивную команду. При извлечении карты опрос останавливается, а состояние карты сбрасывается.</p>
|
||||||
|
|
||||||
<h3 id="profiler" class="text-lg font-medium mb-2">5.6 Профайлер</h3>
|
<h3 id="profiler" class="text-lg font-medium mb-2">5.6 Профайлер</h3>
|
||||||
<p class="text-sm mb-2">Проверяет соответствие карты именованному <strong>профилю</strong> — упорядоченному набору правил, описывающих ожидаемую файловую систему и (опционально) содержимое файлов. Профили хранятся в <code class="font-mono text-sm">localStorage</code>.</p>
|
<p class="text-sm mb-2">Проверяет соответствие карты именованному <strong>профилю</strong> — упорядоченному набору правил, описывающих ожидаемую файловую систему и (опционально) содержимое файлов. Профили хранятся в <code class="font-mono text-sm">localStorage</code>.</p>
|
||||||
|
|||||||
+1
-1
@@ -362,7 +362,7 @@
|
|||||||
<p class="text-sm mb-3">Values persist server-side until restart. When the card issues PLI, the server injects the dictionary values into the TERMINAL RESPONSE.</p>
|
<p class="text-sm mb-3">Values persist server-side until restart. When the card issues PLI, the server injects the dictionary values into the TERMINAL RESPONSE.</p>
|
||||||
|
|
||||||
<h4 id="status-polling" class="font-medium mb-1">5.5.5 STATUS polling</h4>
|
<h4 id="status-polling" class="font-medium mb-1">5.5.5 STATUS polling</h4>
|
||||||
<p class="text-sm mb-3">A <strong>Send STATUS</strong> button issues a manual STATUS (F2). A <strong>Polling</strong> toggle enables background polling: after a configurable idle interval (server CLI <code class="font-mono text-sm">--poll-interval</code>, 1–255 s, default 30 s) the server sends STATUS and handles any pending proactive command. Polling stops and card state resets if the card is removed.</p>
|
<p class="text-sm mb-3">A <strong>Send STATUS</strong> button issues a manual STATUS (F2). A <strong>Polling</strong> toggle enables background polling: after a configurable idle interval (server CLI <code class="font-mono text-sm">--poll-interval</code>, 1–255 s, default 30 s, <code class="font-mono text-sm">0</code> disables polling) the server sends STATUS and handles any pending proactive command. Polling stops and card state resets if the card is removed.</p>
|
||||||
|
|
||||||
<h3 id="profiler" class="text-lg font-medium mb-2">5.6 Profiler</h3>
|
<h3 id="profiler" class="text-lg font-medium mb-2">5.6 Profiler</h3>
|
||||||
<p class="text-sm mb-2">Verifies that a card matches a named <strong>profile</strong> — an ordered set of rules describing the expected file system and (optionally) file contents. Profiles are stored in <code class="font-mono text-sm">localStorage</code>.</p>
|
<p class="text-sm mb-2">Verifies that a card matches a named <strong>profile</strong> — an ordered set of rules describing the expected file system and (optionally) file contents. Profiles are stored in <code class="font-mono text-sm">localStorage</code>.</p>
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'otaman-v88';
|
const CACHE = 'otaman-v89';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from pySim.log import PySimLogger
|
|||||||
from pySim.cards import UiccCardBase
|
from pySim.cards import UiccCardBase
|
||||||
|
|
||||||
from .shell import load_pysim_app
|
from .shell import load_pysim_app
|
||||||
|
from . import fastinit
|
||||||
from .server import PysimHandler, StderrApduTracer, _LoggingApduTracer, VERSION, _send_terminal_profile, _DefaultProactiveHandler, _handle_proactive_chain, _send_status, _init_proactive_session, _timing_on, _tlog
|
from .server import PysimHandler, StderrApduTracer, _LoggingApduTracer, VERSION, _send_terminal_profile, _DefaultProactiveHandler, _handle_proactive_chain, _send_status, _init_proactive_session, _timing_on, _tlog
|
||||||
|
|
||||||
|
|
||||||
@@ -49,6 +50,8 @@ def main():
|
|||||||
help='Skip pysim card initialization (preserve CAT session — no file manager)')
|
help='Skip pysim card initialization (preserve CAT session — no file manager)')
|
||||||
parser.add_argument('--timing', action='store_true', default=False,
|
parser.add_argument('--timing', action='store_true', default=False,
|
||||||
help='Log phase durations, card resets and APDU counters with elapsed timestamps')
|
help='Log phase durations, card resets and APDU counters with elapsed timestamps')
|
||||||
|
parser.add_argument('--fast-init', action='store_true', default=False,
|
||||||
|
help='Init/equip without redundant card resets (reset only on explicit equip/reset)')
|
||||||
|
|
||||||
opts = parser.parse_args()
|
opts = parser.parse_args()
|
||||||
opts.skip_card_init = opts.no_card_init
|
opts.skip_card_init = opts.no_card_init
|
||||||
@@ -88,6 +91,9 @@ def main():
|
|||||||
scc.cat_cla = '80' # UICC CLA default; overridden for SIM after init_card
|
scc.cat_cla = '80' # UICC CLA default; overridden for SIM after init_card
|
||||||
scc._tp.proactive_handler = _DefaultProactiveHandler()
|
scc._tp.proactive_handler = _DefaultProactiveHandler()
|
||||||
t_phase = time.time()
|
t_phase = time.time()
|
||||||
|
if opts.fast_init:
|
||||||
|
rs, card = fastinit.init_card_fast(sl, opts.skip_card_init, wait=True)
|
||||||
|
else:
|
||||||
sl.wait_for_card(3)
|
sl.wait_for_card(3)
|
||||||
rs, card = mod.init_card(sl, opts.skip_card_init)
|
rs, card = mod.init_card(sl, opts.skip_card_init)
|
||||||
_tlog('card_init: %.0fms' % ((time.time() - t_phase) * 1000))
|
_tlog('card_init: %.0fms' % ((time.time() - t_phase) * 1000))
|
||||||
@@ -104,6 +110,8 @@ def main():
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
app = None
|
app = None
|
||||||
_tlog('pysim_app: %.0fms' % ((time.time() - t_phase) * 1000))
|
_tlog('pysim_app: %.0fms' % ((time.time() - t_phase) * 1000))
|
||||||
|
if app is not None and opts.fast_init:
|
||||||
|
fastinit.install(app)
|
||||||
if scc and hasattr(scc, '_tp'):
|
if scc and hasattr(scc, '_tp'):
|
||||||
scc._tp.apdu_tracer = _LoggingApduTracer()
|
scc._tp.apdu_tracer = _LoggingApduTracer()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
"""Fast card initialization for pysim-otaman-server.
|
||||||
|
|
||||||
|
pySim's ``init_card()`` performs several physical card resets: one per profile
|
||||||
|
candidate tried by ``CardProfile.pick()`` plus one at the end of
|
||||||
|
``RuntimeState.__init__``, and ``PysimApp.equip()`` resets yet again. On common
|
||||||
|
readers each disconnect/connect costs around a second, so the stock path spends
|
||||||
|
most of its time re-establishing a clean state (MF selected) that can also be
|
||||||
|
restored in software.
|
||||||
|
|
||||||
|
This module mirrors ``pySim.app.init_card()`` with those resets removed: all
|
||||||
|
profile probes run back-to-back on the same connection and the runtime state
|
||||||
|
uses a software reset. The explicit ``equip`` and ``reset`` commands keep a
|
||||||
|
real reconnect/physical reset.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import operator
|
||||||
|
|
||||||
|
from pySim.cards import CardBase, SimCardBase, UiccCardBase, card_detect
|
||||||
|
from pySim.commands import SimCardCommands
|
||||||
|
from pySim.exceptions import SwMatchError
|
||||||
|
from pySim.filesystem import CardApplication, CardModel
|
||||||
|
from pySim.profile import CardProfile
|
||||||
|
from pySim.runtime import RuntimeState
|
||||||
|
from pySim.ts_102_221 import CardProfileUICC
|
||||||
|
from pySim.utils import all_subclasses
|
||||||
|
|
||||||
|
import pySim.euicc
|
||||||
|
|
||||||
|
from .server import _tlog
|
||||||
|
|
||||||
|
|
||||||
|
class FastRuntimeState(RuntimeState):
|
||||||
|
"""RuntimeState whose reset() restores software state (selects MF) instead
|
||||||
|
of power-cycling the card. Use hard_reset() for an explicit reset."""
|
||||||
|
|
||||||
|
def reset(self, cmd_app=None):
|
||||||
|
return self.soft_reset(cmd_app)
|
||||||
|
|
||||||
|
def soft_reset(self, cmd_app=None):
|
||||||
|
for lchan_nr in list(self.lchan.keys()):
|
||||||
|
self.lchan[lchan_nr].scc.scp = None
|
||||||
|
if lchan_nr == 0:
|
||||||
|
continue
|
||||||
|
del self.lchan[lchan_nr]
|
||||||
|
self.adm_verified = False
|
||||||
|
try:
|
||||||
|
atr = self.card._scc.get_atr()
|
||||||
|
except Exception:
|
||||||
|
atr = None
|
||||||
|
if cmd_app:
|
||||||
|
cmd_app.lchan = self.lchan[0]
|
||||||
|
self.lchan[0].select('MF', cmd_app)
|
||||||
|
self.lchan[0].selected_adf = None
|
||||||
|
self.identity['ATR'] = atr
|
||||||
|
return atr
|
||||||
|
|
||||||
|
def hard_reset(self, cmd_app=None):
|
||||||
|
return super().reset(cmd_app)
|
||||||
|
|
||||||
|
|
||||||
|
def pick_profile_no_reset(scc):
|
||||||
|
"""Like CardProfile.pick(), but without a physical reset between
|
||||||
|
candidates. Each probe selects its own discriminating file, so a reset only
|
||||||
|
costs a reconnect without changing the outcome."""
|
||||||
|
original_reset = scc.reset_card
|
||||||
|
scc.reset_card = lambda: None
|
||||||
|
try:
|
||||||
|
profiles = sorted(all_subclasses(CardProfile), key=operator.attrgetter('ORDER'))
|
||||||
|
for p in profiles:
|
||||||
|
if p.match_with_card(scc):
|
||||||
|
return p()
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
scc.reset_card = original_reset
|
||||||
|
|
||||||
|
|
||||||
|
def init_card_fast(sl, skip_card_init=False, wait=True):
|
||||||
|
"""Replacement for pySim.app.init_card() that avoids redundant resets.
|
||||||
|
|
||||||
|
``wait`` performs the single disconnect/connect of this init (explicit
|
||||||
|
equip passes True; startup already connects via wait_for_card)."""
|
||||||
|
scc = SimCardCommands(transport=sl)
|
||||||
|
if wait:
|
||||||
|
sl.wait_for_card(3)
|
||||||
|
if skip_card_init:
|
||||||
|
return None, CardBase(scc)
|
||||||
|
|
||||||
|
generic_card = False
|
||||||
|
card = card_detect(scc)
|
||||||
|
if card is None:
|
||||||
|
card = SimCardBase(scc)
|
||||||
|
generic_card = True
|
||||||
|
|
||||||
|
profile = pick_profile_no_reset(scc)
|
||||||
|
if profile is None:
|
||||||
|
return None, card
|
||||||
|
|
||||||
|
if generic_card and isinstance(profile, CardProfileUICC):
|
||||||
|
card._adm_chv_num = 0x0A
|
||||||
|
|
||||||
|
if isinstance(profile, CardProfileUICC):
|
||||||
|
for app_cls in all_subclasses(CardApplication):
|
||||||
|
if hasattr(app_cls, '_' + app_cls.__name__ + '__intermediate'):
|
||||||
|
continue
|
||||||
|
profile.add_application(app_cls())
|
||||||
|
if generic_card:
|
||||||
|
card = UiccCardBase(scc)
|
||||||
|
|
||||||
|
rs = FastRuntimeState(card, profile)
|
||||||
|
|
||||||
|
CardModel.apply_matching_models(scc, rs)
|
||||||
|
|
||||||
|
sl.set_sw_interpreter(rs)
|
||||||
|
|
||||||
|
isd_r = rs.mf.applications.get(pySim.euicc.AID_ISD_R.lower(), None)
|
||||||
|
if isd_r:
|
||||||
|
rs.lchan[0].select_file(isd_r)
|
||||||
|
try:
|
||||||
|
rs.identity['EID'] = pySim.euicc.CardApplicationISDR.get_eid(scc)
|
||||||
|
except SwMatchError:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
rs.soft_reset()
|
||||||
|
|
||||||
|
return rs, card
|
||||||
|
|
||||||
|
|
||||||
|
def do_equip_fast(app):
|
||||||
|
"""Explicit equip: one real reconnect (wait_for_card) then reset-free init."""
|
||||||
|
if app.rs and app.rs.profile:
|
||||||
|
for cmd_set in app.rs.profile.shell_cmdsets:
|
||||||
|
app.unregister_command_set(cmd_set)
|
||||||
|
rs, card = init_card_fast(app.sl, wait=True)
|
||||||
|
app.equip(card, rs)
|
||||||
|
|
||||||
|
|
||||||
|
def do_reset_fast(app):
|
||||||
|
"""Explicit reset: always a physical card reset."""
|
||||||
|
if app.rs is None:
|
||||||
|
app.card._scc.reset_card()
|
||||||
|
atr = app.card._scc.get_atr()
|
||||||
|
else:
|
||||||
|
atr = app.rs.hard_reset(app)
|
||||||
|
app.poutput('Card ATR: %s' % atr)
|
||||||
|
|
||||||
|
|
||||||
|
def install(app):
|
||||||
|
"""Route the pySim-shell equip/reset commands through the fast paths."""
|
||||||
|
def _do_equip(statement):
|
||||||
|
_tlog('do_equip_fast: start')
|
||||||
|
do_equip_fast(app)
|
||||||
|
_tlog('do_equip_fast: done')
|
||||||
|
|
||||||
|
def _do_reset(statement):
|
||||||
|
_tlog('do_reset_fast: start')
|
||||||
|
do_reset_fast(app)
|
||||||
|
_tlog('do_reset_fast: done')
|
||||||
|
|
||||||
|
app.do_equip = _do_equip
|
||||||
|
app.do_reset = _do_reset
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Tests for the reset-free fast initialization helpers."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PROJECTS = Path(__file__).resolve().parents[2]
|
||||||
|
PY_SIM = PROJECTS / 'pysim'
|
||||||
|
if str(PY_SIM) not in sys.path:
|
||||||
|
sys.path.insert(0, str(PY_SIM))
|
||||||
|
|
||||||
|
from pySim.exceptions import SwMatchError
|
||||||
|
from pySim.ts_102_221 import CardProfileUICC
|
||||||
|
|
||||||
|
from pysim_otaman_server.fastinit import (
|
||||||
|
FastRuntimeState,
|
||||||
|
do_reset_fast,
|
||||||
|
pick_profile_no_reset,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeScc:
|
||||||
|
def __init__(self):
|
||||||
|
self.sel_ctrl = '0004'
|
||||||
|
self.cla_byte = '00'
|
||||||
|
self.resets = 0
|
||||||
|
self.selected = []
|
||||||
|
|
||||||
|
def reset_card(self):
|
||||||
|
self.resets += 1
|
||||||
|
|
||||||
|
def select_file(self, fid):
|
||||||
|
self.selected.append(fid)
|
||||||
|
return ('', '9000')
|
||||||
|
|
||||||
|
def select_adf(self, aid):
|
||||||
|
raise SwMatchError('6a82', '9000')
|
||||||
|
|
||||||
|
|
||||||
|
class TestPickProfileNoReset(unittest.TestCase):
|
||||||
|
def test_uicc_selected_without_any_reset(self):
|
||||||
|
scc = FakeScc()
|
||||||
|
profile = pick_profile_no_reset(scc)
|
||||||
|
self.assertIsInstance(profile, CardProfileUICC)
|
||||||
|
self.assertEqual(scc.resets, 0)
|
||||||
|
self.assertIn('3f00', scc.selected)
|
||||||
|
|
||||||
|
def test_reset_card_restored_after_pick(self):
|
||||||
|
scc = FakeScc()
|
||||||
|
pick_profile_no_reset(scc)
|
||||||
|
scc.reset_card()
|
||||||
|
self.assertEqual(scc.resets, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeLchan:
|
||||||
|
def __init__(self):
|
||||||
|
self.scc = types.SimpleNamespace(scp=object())
|
||||||
|
self.selected_adf = 'SOMETHING'
|
||||||
|
self.selected = []
|
||||||
|
|
||||||
|
def select(self, path, cmd_app=None):
|
||||||
|
self.selected.append(path)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFastRuntimeStateSoftReset(unittest.TestCase):
|
||||||
|
def make_rs(self):
|
||||||
|
rs = FastRuntimeState.__new__(FastRuntimeState)
|
||||||
|
rs.lchan = {0: FakeLchan(), 1: FakeLchan()}
|
||||||
|
rs.adm_verified = True
|
||||||
|
rs.card = types.SimpleNamespace(_scc=types.SimpleNamespace(get_atr=lambda: 'AABB'))
|
||||||
|
rs.identity = {}
|
||||||
|
return rs
|
||||||
|
|
||||||
|
def test_soft_reset_selects_mf_without_physical_reset(self):
|
||||||
|
rs = self.make_rs()
|
||||||
|
atr = rs.soft_reset()
|
||||||
|
self.assertEqual(atr, 'AABB')
|
||||||
|
self.assertEqual(rs.identity['ATR'], 'AABB')
|
||||||
|
self.assertEqual(rs.lchan[0].selected, ['MF'])
|
||||||
|
self.assertIsNone(rs.lchan[0].selected_adf)
|
||||||
|
self.assertFalse(rs.adm_verified)
|
||||||
|
self.assertNotIn(1, rs.lchan)
|
||||||
|
|
||||||
|
def test_reset_is_soft(self):
|
||||||
|
rs = self.make_rs()
|
||||||
|
rs.card = types.SimpleNamespace(_scc=types.SimpleNamespace(get_atr=lambda: 'EEFF'))
|
||||||
|
self.assertEqual(rs.reset(), 'EEFF')
|
||||||
|
self.assertEqual(rs.lchan[0].selected, ['MF'])
|
||||||
|
|
||||||
|
|
||||||
|
class FakeCardScc:
|
||||||
|
def __init__(self):
|
||||||
|
self.resets = 0
|
||||||
|
|
||||||
|
def reset_card(self):
|
||||||
|
self.resets += 1
|
||||||
|
return 'ATR'
|
||||||
|
|
||||||
|
def get_atr(self):
|
||||||
|
return 'AABB'
|
||||||
|
|
||||||
|
|
||||||
|
class TestDoResetFast(unittest.TestCase):
|
||||||
|
def test_explicit_reset_is_physical(self):
|
||||||
|
scc = FakeCardScc()
|
||||||
|
out = []
|
||||||
|
app = types.SimpleNamespace(rs=None, card=types.SimpleNamespace(_scc=scc), poutput=out.append)
|
||||||
|
do_reset_fast(app)
|
||||||
|
self.assertEqual(scc.resets, 1)
|
||||||
|
self.assertEqual(out, ['Card ATR: AABB'])
|
||||||
|
|
||||||
|
def test_explicit_reset_uses_hard_reset_with_runtime_state(self):
|
||||||
|
calls = []
|
||||||
|
rs = types.SimpleNamespace(hard_reset=lambda cmd_app=None: calls.append(cmd_app) or 'CCDD')
|
||||||
|
out = []
|
||||||
|
app = types.SimpleNamespace(rs=rs, card=None, poutput=out.append)
|
||||||
|
do_reset_fast(app)
|
||||||
|
self.assertEqual(calls, [app])
|
||||||
|
self.assertEqual(out, ['Card ATR: CCDD'])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user