startup: cardless start is a normal state, not an init failure (v2.2.17)
- __main__ catches NoCardError from the startup init (fast and stock paths, and the stock fallback) and reports it with one line - 'INIT: no card in the reader - server ready; insert a card or press Equip' - without a traceback and without the stock-pysim fallback. Any other init failure still falls back with its traceback. - The cardless PysimApp construction no longer prints pySim's 'Waiting for card...' or pySim-shell's 'pySim-shell not equipped!': a new _LineFilter (server.py) drops whole matching lines while proxying encoding/isatty/ fileno (the cmd2 Rich console probes them on its stdout). - Removed the now-duplicate 'INIT: card not initialized' tail line and the cat_cla assignment when no card was detected (scc exists but is cardless). - Tests: tests/test_startup_cardless.py (line filter semantics incl. partial lines, one-line cardless report, no fallback call, real failures keep the fallback + traceback, muted PysimApp construction). Live-verified against an empty SCR 3310: two init lines, no traceback, API serves 2.2.17. - Docs: READMEs quick start, help EN+RU (Card reader), AGENTS (equip flow). SW cache otaman-v184.
This commit is contained in:
@@ -17,7 +17,7 @@ cd otaman
|
|||||||
./start.sh # or start.bat — starts the server (it serves the PWA too)
|
./start.sh # or start.bat — starts the server (it serves the PWA too)
|
||||||
```
|
```
|
||||||
|
|
||||||
Then open http://127.0.0.1:8080 — the UI and API share one origin, so no CORS or browser-permission setup is needed.
|
Then open http://127.0.0.1:8080 — the UI and API share one origin, so no CORS or browser-permission setup is needed. The server starts fine with an empty reader (no card is a normal state): insert a card and it is initialized automatically (auto-equip), or press **Equip card** in the UI.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ cd otaman
|
|||||||
./start.sh # или start.bat — запускает сервер (он же раздаёт PWA)
|
./start.sh # или start.bat — запускает сервер (он же раздаёт PWA)
|
||||||
```
|
```
|
||||||
|
|
||||||
Затем откройте http://127.0.0.1:8080 — интерфейс и API на одном origin, поэтому CORS и разрешения браузера не нужны.
|
Затем откройте http://127.0.0.1:8080 — интерфейс и API на одном origin, поэтому CORS и разрешения браузера не нужны. Сервер запускается и с пустым картридером (отсутствие карты — нормальное состояние): вставьте карту, и она инициализируется автоматически (auto-equip), либо нажмите **Equip card** в интерфейсе.
|
||||||
|
|
||||||
## Сборка
|
## Сборка
|
||||||
|
|
||||||
|
|||||||
@@ -517,7 +517,7 @@ start.bat # запускает сервер (PWA + API)</pre>
|
|||||||
<li><strong>Резерв сервера</strong> — при запуске без аргументов ридера сервер сам опрашивает PC/SC-ридер при старте (3 попытки с интервалом 2 с)</li>
|
<li><strong>Резерв сервера</strong> — при запуске без аргументов ридера сервер сам опрашивает PC/SC-ридер при старте (3 попытки с интервалом 2 с)</li>
|
||||||
<li><strong>Serial-ридеры</strong> — запустите сервер вручную с <code class="font-mono text-sm">-d /dev/ttyUSB0</code> (Linux)</li>
|
<li><strong>Serial-ридеры</strong> — запустите сервер вручную с <code class="font-mono text-sm">-d /dev/ttyUSB0</code> (Linux)</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="text-sm mb-3">Если карта отсутствует, вкладка «Картридер» показывает «Карта не обнаружена. Вставьте карту и нажмите Подключить карту».</p>
|
<p class="text-sm mb-3">Если карта отсутствует, вкладка «Картридер» показывает «Карта не обнаружена. Вставьте карту и нажмите Подключить карту». Вставьте карту — сервер инициализирует её автоматически (auto-equip включён по умолчанию) — либо нажмите <strong>Equip card</strong>. Сам сервер запускается и с пустым картридером: отсутствие карты — нормальное состояние, а не ошибка.</p>
|
||||||
|
|
||||||
<h3 id="manual-install" class="text-lg font-medium mb-2">9.6 Ручная установка</h3>
|
<h3 id="manual-install" class="text-lg font-medium mb-2">9.6 Ручная установка</h3>
|
||||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3"># Создать и активировать venv
|
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3"># Создать и активировать venv
|
||||||
|
|||||||
+1
-1
@@ -517,7 +517,7 @@ start.bat # starts the server (serves PWA + API)</pre>
|
|||||||
<li><strong>Server fallback</strong> — started without reader arguments, the server itself probes for a PC/SC reader at startup (3 attempts, 2 s apart)</li>
|
<li><strong>Server fallback</strong> — started without reader arguments, the server itself probes for a PC/SC reader at startup (3 attempts, 2 s apart)</li>
|
||||||
<li><strong>Serial readers</strong> — start the server manually with <code class="font-mono text-sm">-d /dev/ttyUSB0</code> (Linux)</li>
|
<li><strong>Serial readers</strong> — start the server manually with <code class="font-mono text-sm">-d /dev/ttyUSB0</code> (Linux)</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="text-sm mb-3">If no card is present, the Card reader tab shows “No card detected”. Insert the card and click <strong>Equip card</strong> to initialize it.</p>
|
<p class="text-sm mb-3">If no card is present, the Card reader tab shows “No card detected”. Insert the card — the server auto-equips it (auto-equip is on by default) — or click <strong>Equip card</strong>. The server itself starts fine with an empty reader; the absence of a card is a normal state, not an error.</p>
|
||||||
|
|
||||||
<h3 id="manual-install" class="text-lg font-medium mb-2">9.6 Manual installation</h3>
|
<h3 id="manual-install" class="text-lg font-medium mb-2">9.6 Manual installation</h3>
|
||||||
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3"># Create and activate a venv
|
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3"># Create and activate a venv
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
<div class="max-w-7xl mx-auto px-6 py-2">
|
<div class="max-w-7xl mx-auto px-6 py-2">
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.2.16</span></h1>
|
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.2.17</span></h1>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<span id="state-indicator" class="flex items-center select-none" style="cursor:default" title="Connecting...">
|
<span id="state-indicator" class="flex items-center select-none" style="cursor:default" title="Connecting...">
|
||||||
<span id="state-indicator-dot" class="text-xs text-gray-400" title="Connecting...">●</span>
|
<span id="state-indicator-dot" class="text-xs text-gray-400" title="Connecting...">●</span>
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'otaman-v183';
|
const CACHE = 'otaman-v184';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-otaman-server"
|
name = "pysim-otaman-server"
|
||||||
version = "2.2.16"
|
version = "2.2.17"
|
||||||
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import traceback
|
|||||||
from http.server import HTTPServer
|
from http.server import HTTPServer
|
||||||
from pySim.card_handler import CardHandler
|
from pySim.card_handler import CardHandler
|
||||||
from pySim.commands import SimCardCommands
|
from pySim.commands import SimCardCommands
|
||||||
|
from pySim.exceptions import NoCardError
|
||||||
from pySim.log import PySimLogger
|
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 . import fastinit
|
||||||
from .server import PysimHandler, StderrApduTracer, _LoggingApduTracer, VERSION, _send_terminal_profile, _DefaultProactiveHandler, _handle_proactive_chain, _send_status, _init_proactive_session, _timing_on, _tlog, _set_menu_timeout, start_card_monitor, set_auto_equip, _read_iccid
|
from .server import PysimHandler, StderrApduTracer, _LoggingApduTracer, VERSION, _send_terminal_profile, _DefaultProactiveHandler, _handle_proactive_chain, _send_status, _init_proactive_session, _timing_on, _tlog, _set_menu_timeout, start_card_monitor, set_auto_equip, _read_iccid, _LineFilter
|
||||||
|
|
||||||
|
|
||||||
_server_start = 0
|
_server_start = 0
|
||||||
@@ -105,14 +106,28 @@ def main():
|
|||||||
if opts.fast_init:
|
if opts.fast_init:
|
||||||
try:
|
try:
|
||||||
rs, card = fastinit.init_card_fast(sl, opts.skip_card_init, wait=True)
|
rs, card = fastinit.init_card_fast(sl, opts.skip_card_init, wait=True)
|
||||||
|
except NoCardError:
|
||||||
|
# Normal cardless start: there was no card in the reader (the
|
||||||
|
# 3s wait timed out). The presence monitor auto-equips once a
|
||||||
|
# card appears; nothing to recover, no traceback.
|
||||||
|
sys.stderr.write('INIT: no card in the reader — server ready; insert a card or press Equip\n')
|
||||||
except Exception:
|
except Exception:
|
||||||
print("Warning: fast card initialization failed, falling back to pysim init:", file=sys.stderr)
|
print("Warning: fast card initialization failed, falling back to pysim init:", file=sys.stderr)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
try:
|
||||||
rs, card = mod.init_card(sl, opts.skip_card_init)
|
rs, card = mod.init_card(sl, opts.skip_card_init)
|
||||||
|
except NoCardError:
|
||||||
|
# The fallback retried the cardless wait; still a normal
|
||||||
|
# cardless start, not an initialization failure.
|
||||||
|
sys.stderr.write('INIT: no card in the reader — server ready; insert a card or press Equip\n')
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
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)
|
||||||
|
except NoCardError:
|
||||||
|
sys.stderr.write('INIT: no card in the reader — server ready; insert a card or press Equip\n')
|
||||||
_tlog('card_init: %.0fms' % ((time.time() - t_phase) * 1000))
|
_tlog('card_init: %.0fms' % ((time.time() - t_phase) * 1000))
|
||||||
|
if card is not None:
|
||||||
scc.cat_cla = '80' if isinstance(card, UiccCardBase) else 'a0'
|
scc.cat_cla = '80' if isinstance(card, UiccCardBase) else 'a0'
|
||||||
except Exception:
|
except Exception:
|
||||||
print("Warning: reader/card initialization failed:", file=sys.stderr)
|
print("Warning: reader/card initialization failed:", file=sys.stderr)
|
||||||
@@ -120,7 +135,20 @@ def main():
|
|||||||
ch = CardHandler(sl) if sl else None
|
ch = CardHandler(sl) if sl else None
|
||||||
t_phase = time.time()
|
t_phase = time.time()
|
||||||
try:
|
try:
|
||||||
|
if card is not None:
|
||||||
app = mod.PysimApp(verbose=opts.verbose, card=card, rs=rs, sl=sl, ch=ch)
|
app = mod.PysimApp(verbose=opts.verbose, card=card, rs=rs, sl=sl, ch=ch)
|
||||||
|
else:
|
||||||
|
# Cardless start: pySim logs 'Waiting for card...' (its own retry
|
||||||
|
# path) and pySim-shell prints 'pySim-shell not equipped!'; we
|
||||||
|
# report both cases with our own single line above. A PysimApp
|
||||||
|
# without a card would also retry the cardless wait, so install
|
||||||
|
# the filter before constructing it and drop the two internals.
|
||||||
|
saved_stdout = sys.stdout
|
||||||
|
sys.stdout = _LineFilter(saved_stdout, ('Waiting for card...', 'pySim-shell not equipped!'))
|
||||||
|
try:
|
||||||
|
app = mod.PysimApp(verbose=opts.verbose, card=None, rs=None, sl=sl, ch=ch)
|
||||||
|
finally:
|
||||||
|
sys.stdout = saved_stdout
|
||||||
except Exception:
|
except Exception:
|
||||||
print("Warning: PysimApp creation failed:", file=sys.stderr)
|
print("Warning: PysimApp creation failed:", file=sys.stderr)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@@ -153,8 +181,6 @@ def main():
|
|||||||
_tlog('terminal_profile_drain: %.0fms' % ((time.time() - t_phase) * 1000))
|
_tlog('terminal_profile_drain: %.0fms' % ((time.time() - t_phase) * 1000))
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc(file=sys.stderr)
|
traceback.print_exc(file=sys.stderr)
|
||||||
elif scc is not None:
|
|
||||||
sys.stderr.write('INIT: card not initialized — use Equip once the card is readable\n')
|
|
||||||
if app is not None and opts.apdu_trace:
|
if app is not None and opts.apdu_trace:
|
||||||
# PysimApp.__init__ routes PySimLogger through app.poutput() (app.stdout)
|
# PysimApp.__init__ routes PySimLogger through app.poutput() (app.stdout)
|
||||||
# and drops the root level to INFO. Re-route pysim's own APDU trace logging
|
# and drops the root level to INFO. Re-route pysim's own APDU trace logging
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
|||||||
from osmocom.tlv import BER_TLV_IE
|
from osmocom.tlv import BER_TLV_IE
|
||||||
|
|
||||||
|
|
||||||
VERSION = '2.2.16'
|
VERSION = '2.2.17'
|
||||||
|
|
||||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
@@ -59,6 +59,51 @@ def _tlog(msg):
|
|||||||
sys.stderr.write('TIMING [+%7.3fs] %s\n' % (time.time() - _T0, msg))
|
sys.stderr.write('TIMING [+%7.3fs] %s\n' % (time.time() - _T0, msg))
|
||||||
|
|
||||||
|
|
||||||
|
class _LineFilter:
|
||||||
|
"""Text stream that drops whole lines matching any of the given substrings
|
||||||
|
and forwards everything else to the wrapped stream. Used to mute pySim/
|
||||||
|
pySim-shell internals we report ourselves (e.g. 'Waiting for card...' or
|
||||||
|
'pySim-shell not equipped!'). Lines arrive through write(); the dropped
|
||||||
|
lines are written as one call each by pySim's print/logger and by cmd2's
|
||||||
|
Rich console, so a substring check per line is reliable here."""
|
||||||
|
|
||||||
|
def __init__(self, stream, patterns):
|
||||||
|
self._stream = stream
|
||||||
|
self._patterns = list(patterns)
|
||||||
|
self._pending = ''
|
||||||
|
|
||||||
|
def write(self, text):
|
||||||
|
text = self._pending + text
|
||||||
|
self._pending = ''
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
if not text.endswith('\n'):
|
||||||
|
# Keep a trailing partial line so a match is not missed when the
|
||||||
|
# line is completed by the next write().
|
||||||
|
nl = text.rfind('\n')
|
||||||
|
if nl < 0:
|
||||||
|
self._pending = text
|
||||||
|
return
|
||||||
|
self._pending = text[nl + 1:]
|
||||||
|
text = text[:nl + 1]
|
||||||
|
for line in text.splitlines(True):
|
||||||
|
if not any(p in line for p in self._patterns):
|
||||||
|
self._stream.write(line)
|
||||||
|
self._stream.flush()
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
if self._pending:
|
||||||
|
line, self._pending = self._pending, ''
|
||||||
|
if not any(p in line for p in self._patterns):
|
||||||
|
self._stream.write(line)
|
||||||
|
self._stream.flush()
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
# encoding/isatty/fileno: the Rich console probes these on the stream
|
||||||
|
# (cmd2's self.stdout), so they must keep working.
|
||||||
|
return getattr(self._stream, name)
|
||||||
|
|
||||||
|
|
||||||
_APDU_TIMES = []
|
_APDU_TIMES = []
|
||||||
_APDU_TIME_COLLECT = False
|
_APDU_TIME_COLLECT = False
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Tests for the cardless-startup handling in __main__.
|
||||||
|
|
||||||
|
A reader without a card is a normal state, not an initialization failure:
|
||||||
|
pySim raises NoCardError after its wait, which must be reported with a single
|
||||||
|
line and no traceback, and the pySim-internal 'Waiting for card...' /
|
||||||
|
'pySim-shell not equipped!' lines must not reach the console. Any other
|
||||||
|
failure keeps the stock-pysim fallback and its traceback.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
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 NoCardError, SwMatchError
|
||||||
|
|
||||||
|
from pysim_otaman_server import __main__ as srv_main
|
||||||
|
from pysim_otaman_server.server import _LineFilter
|
||||||
|
|
||||||
|
CARDLESS_LINE = 'INIT: no card in the reader — server ready; insert a card or press Equip'
|
||||||
|
|
||||||
|
|
||||||
|
class LineFilterTest(unittest.TestCase):
|
||||||
|
def test_drops_matching_lines_and_keeps_the_rest(self):
|
||||||
|
out = io.StringIO()
|
||||||
|
f = _LineFilter(out, ('Waiting for card...', 'pySim-shell not equipped!'))
|
||||||
|
f.write('Waiting for card...\n')
|
||||||
|
f.write('pySim-shell not equipped!\n')
|
||||||
|
f.write('INIT: keep me\n')
|
||||||
|
f.flush()
|
||||||
|
self.assertEqual(out.getvalue(), 'INIT: keep me\n')
|
||||||
|
|
||||||
|
def test_line_completed_by_the_next_write_is_still_dropped(self):
|
||||||
|
out = io.StringIO()
|
||||||
|
f = _LineFilter(out, ('Waiting for card...',))
|
||||||
|
f.write('Waiting for card...')
|
||||||
|
self.assertEqual(out.getvalue(), '')
|
||||||
|
f.write('\n')
|
||||||
|
f.write('ok\n')
|
||||||
|
self.assertEqual(out.getvalue(), 'ok\n')
|
||||||
|
|
||||||
|
def test_partial_line_is_flushed_when_it_does_not_match(self):
|
||||||
|
out = io.StringIO()
|
||||||
|
f = _LineFilter(out, ('Waiting for card...',))
|
||||||
|
f.write('partial')
|
||||||
|
f.flush()
|
||||||
|
self.assertEqual(out.getvalue(), 'partial')
|
||||||
|
|
||||||
|
def test_stream_attributes_are_proxied(self):
|
||||||
|
out = io.StringIO()
|
||||||
|
f = _LineFilter(out, ('x',))
|
||||||
|
self.assertEqual(f.encoding, out.encoding)
|
||||||
|
self.assertIs(f.isatty(), out.isatty())
|
||||||
|
|
||||||
|
|
||||||
|
class StartupNoCardTest(unittest.TestCase):
|
||||||
|
"""Exercise the init branch of main() without a reader or HTTP server."""
|
||||||
|
|
||||||
|
def _run(self, init_side_effect, argv=()):
|
||||||
|
fake_app = mock.Mock()
|
||||||
|
fake_mod = mock.Mock()
|
||||||
|
fake_mod.option_parser = _parser()
|
||||||
|
fake_mod.PysimApp.return_value = fake_app
|
||||||
|
fake_mod.init_card = mock.Mock(side_effect=init_side_effect)
|
||||||
|
err = io.StringIO()
|
||||||
|
out = io.StringIO()
|
||||||
|
with mock.patch.object(srv_main, 'load_pysim_app', return_value=fake_mod), \
|
||||||
|
mock.patch.object(srv_main, 'fastinit') as fake_fastinit, \
|
||||||
|
mock.patch.object(srv_main, 'start_card_monitor'), \
|
||||||
|
mock.patch.object(srv_main, '_read_iccid', return_value=None), \
|
||||||
|
mock.patch.object(srv_main, '_send_terminal_profile', return_value=(None, None)), \
|
||||||
|
mock.patch.object(srv_main, '_send_status', return_value=('', '9000')), \
|
||||||
|
mock.patch.object(srv_main, 'HTTPServer') as fake_http, \
|
||||||
|
mock.patch.object(srv_main, 'CardHandler'), \
|
||||||
|
mock.patch('sys.stderr', err), mock.patch('sys.stdout', out):
|
||||||
|
fake_fastinit.init_card_fast.side_effect = init_side_effect
|
||||||
|
fake_fastinit.install = mock.Mock()
|
||||||
|
fake_http.return_value.serve_forever.side_effect = KeyboardInterrupt
|
||||||
|
try:
|
||||||
|
with mock.patch('sys.argv', ['pysim-otaman-server']):
|
||||||
|
srv_main.main()
|
||||||
|
except (KeyboardInterrupt, SystemExit):
|
||||||
|
pass
|
||||||
|
return err.getvalue(), out.getvalue(), fake_fastinit, fake_mod
|
||||||
|
|
||||||
|
def test_no_card_reports_one_line_without_traceback(self):
|
||||||
|
err, out, fake_fastinit, fake_mod = self._run(NoCardError())
|
||||||
|
self.assertIn(CARDLESS_LINE, err)
|
||||||
|
self.assertNotIn('Traceback', err)
|
||||||
|
self.assertNotIn('falling back to pysim init', err)
|
||||||
|
# no second (stock) init attempt after the cardless fast init
|
||||||
|
fake_mod.init_card.assert_not_called()
|
||||||
|
|
||||||
|
def test_real_failure_still_falls_back_with_traceback(self):
|
||||||
|
def boom(*a, **k):
|
||||||
|
raise SwMatchError('6a82', '9000')
|
||||||
|
|
||||||
|
err, out, fake_fastinit, fake_mod = self._run(boom)
|
||||||
|
self.assertIn('falling back to pysim init', err)
|
||||||
|
self.assertIn('Traceback', err)
|
||||||
|
self.assertIn('SwMatchError', err)
|
||||||
|
self.assertTrue(fake_mod.init_card.called)
|
||||||
|
|
||||||
|
def test_cardless_app_construction_mutes_pysim_internals(self):
|
||||||
|
# PysimApp is constructed without a card: the filter must be active
|
||||||
|
# around the call, so lines written through sys.stdout are dropped.
|
||||||
|
seen = {}
|
||||||
|
|
||||||
|
def make_app(**kwargs):
|
||||||
|
seen['kwargs'] = kwargs
|
||||||
|
sys.stdout.write('Waiting for card...\n')
|
||||||
|
sys.stdout.write('pySim-shell not equipped!\n')
|
||||||
|
sys.stdout.write('INIT: something else\n')
|
||||||
|
return mock.Mock()
|
||||||
|
|
||||||
|
fake_mod = mock.Mock()
|
||||||
|
fake_mod.option_parser = _parser()
|
||||||
|
fake_mod.PysimApp.side_effect = make_app
|
||||||
|
fake_mod.init_card = mock.Mock()
|
||||||
|
err, out = io.StringIO(), io.StringIO()
|
||||||
|
with mock.patch.object(srv_main, 'load_pysim_app', return_value=fake_mod), \
|
||||||
|
mock.patch.object(srv_main, 'fastinit') as fake_fastinit, \
|
||||||
|
mock.patch.object(srv_main, 'start_card_monitor'), \
|
||||||
|
mock.patch.object(srv_main, 'HTTPServer') as fake_http, \
|
||||||
|
mock.patch.object(srv_main, 'CardHandler'), \
|
||||||
|
mock.patch('sys.stderr', err), mock.patch('sys.stdout', out):
|
||||||
|
fake_fastinit.init_card_fast.side_effect = NoCardError()
|
||||||
|
fake_fastinit.install = mock.Mock()
|
||||||
|
fake_http.return_value.serve_forever.side_effect = KeyboardInterrupt
|
||||||
|
try:
|
||||||
|
with mock.patch('sys.argv', ['pysim-otaman-server']):
|
||||||
|
srv_main.main()
|
||||||
|
except (KeyboardInterrupt, SystemExit):
|
||||||
|
pass
|
||||||
|
self.assertIsNone(seen['kwargs']['card'])
|
||||||
|
self.assertIsNone(seen['kwargs']['rs'])
|
||||||
|
self.assertNotIn('Waiting for card...', out.getvalue())
|
||||||
|
self.assertNotIn('pySim-shell not equipped!', out.getvalue())
|
||||||
|
self.assertIn('INIT: something else', out.getvalue())
|
||||||
|
|
||||||
|
|
||||||
|
def _parser():
|
||||||
|
import argparse
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
p.add_argument('-p', '--pcsc-dev', type=int, default=0)
|
||||||
|
p.add_argument('--pcsc-regex', default=None)
|
||||||
|
p.add_argument('--apdu-trace', action='store_true', default=False)
|
||||||
|
p.add_argument('--verbose', action='store_true', default=False)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user