v3.10.0: preset TAR table with per-TAR MSL; derive the SPI1 from the TAR
Presets drop the card-wide SPI1/SPI2 and the fixed tar/uiccTar/usimTar fields: a preset now carries `tars` - the three mandatory role entries (isd/uiccRfm/usimRfm, spec defaults pre-settled) plus free TARs with an optional description, each with its MSL (Minimum SPI1, TS 102 226 8.2.1.3.2.4). v3.9.1 presets convert on load/import (the spi1 seeds all three MSLs); duplicate TARs and empty TAR/MSL values are refused; the store schema version is 3. Operations derive a packet's SPI1 from its TAR's MSL (_spi1_for_tar): the RAM ops, the test runner (per-step overrides still win), the counter probe and each TAR-probe row; the plain send keeps an explicit SPI1 with a `warning` when it is below the MSL, and a TAR outside the preset without an explicit SPI1 is refused with a hint. The TAR probe resolves per checked TAR before sending and reports the effective spi1 per row. The secured packet form's SPI2 default is 00 (the RAM/probe operations pin their own). PWA: the Cards form edits the TAR table (role rows with MSL inputs plus Add/Remove free rows with a description), the list shows a TARs (MSL) column, the SCP80 TAR field suggests the preset's entries (datalist) and a preset apply seeds the SPI1 from the operation TAR's MSL with an amber warning below it; the preset-wide SPI1/SPI2 fields and the header marker's SPI requirement are gone (completeness = a keyset + the three role entries with their MSLs). Help EN/RU, docs/api.md and AGENTS updated; sw cache simple-v358. Live: TAR probe 000000 at its MSL 16 (registered, por_ok, counter advanced); a send without spi1 derived 16 (por_ok, counter advanced); a send with spi1 0A returned the warning and the card refused it (6200, no counter consumed); an unknown TAR without spi1 was refused with the hint.
This commit is contained in:
@@ -237,7 +237,7 @@ class CapCompatHttpTests(unittest.TestCase):
|
||||
p.start()
|
||||
try:
|
||||
status, resp = self._post('/api/cap-compat', {
|
||||
'cap_hex': _mini_cap_hex(),
|
||||
'cap_hex': _mini_cap_hex(), 'spi1': '16',
|
||||
'probe_imports': {'A0000000620101': '0.0', 'dead beef': '1.2'},
|
||||
})
|
||||
finally:
|
||||
@@ -306,7 +306,7 @@ class RamInstallNvFootprintHttpTests(unittest.TestCase):
|
||||
for p in patches:
|
||||
p.start()
|
||||
try:
|
||||
return self._post('/api/ram-install', {'cap_hex': 'AA'})
|
||||
return self._post('/api/ram-install', {'cap_hex': 'AA', 'spi1': '16'})
|
||||
finally:
|
||||
for p in reversed(patches):
|
||||
p.stop()
|
||||
@@ -444,7 +444,7 @@ class CntrLowGuardHttpTests(unittest.TestCase):
|
||||
for p in patches:
|
||||
p.start()
|
||||
try:
|
||||
status, resp = self._post('/api/ram-install', {'cap_hex': 'AA'})
|
||||
status, resp = self._post('/api/ram-install', {'cap_hex': 'AA', 'spi1': '16'})
|
||||
finally:
|
||||
for p in reversed(patches):
|
||||
p.stop()
|
||||
@@ -653,6 +653,90 @@ class PresetStoreHttpTests(unittest.TestCase):
|
||||
self.assertEqual(resp['final_cntr'], '0000000002')
|
||||
self.assertEqual(self.store.find_keyset(p['id'], 1)['cntr'], '0000000002')
|
||||
|
||||
def test_the_spi1_is_derived_from_the_msl_and_a_low_value_warns(self):
|
||||
p = self.store.add(self._preset(iccid='8970119000004600098'))
|
||||
seen = []
|
||||
|
||||
def fake_decode(spi1, spi2, *a, **k):
|
||||
seen.append(spi1)
|
||||
return None
|
||||
|
||||
patches = [
|
||||
mock.patch.object(self.srv, '_send_secured_packet', lambda *a, **k: {
|
||||
'success': True, 'sw': '9000', 'bytes': 34, 'segments': 1,
|
||||
'response_data': ''}),
|
||||
mock.patch.object(self.srv, '_decode_por', fake_decode),
|
||||
]
|
||||
self.server.app = object()
|
||||
for patch in patches:
|
||||
patch.start()
|
||||
try:
|
||||
# no spi1: derived from the ISD entry's MSL (the store default 16)
|
||||
status, resp = self._post('/api/send-ota', {
|
||||
'sp': '00', 'spi2': '01', 'kic': '15', 'kid': '15',
|
||||
'tar': '000000', 'preset_id': p['id']})
|
||||
self.assertEqual(status, 200, resp)
|
||||
self.assertNotIn('warning', resp)
|
||||
# an explicit value below the MSL wins (a hand send may ask for
|
||||
# less) but the response names the risk
|
||||
status, resp = self._post('/api/send-ota', {
|
||||
'sp': '00', 'spi1': '0A', 'spi2': '01', 'kic': '15', 'kid': '15',
|
||||
'tar': '000000', 'preset_id': p['id']})
|
||||
self.assertEqual(status, 200, resp)
|
||||
self.assertIn('MSL 16', resp['warning'])
|
||||
# an explicit value above the MSL is fine
|
||||
status, resp = self._post('/api/send-ota', {
|
||||
'sp': '00', 'spi1': '21', 'spi2': '01', 'kic': '15', 'kid': '15',
|
||||
'tar': '000000', 'preset_id': p['id']})
|
||||
self.assertEqual(status, 200, resp)
|
||||
self.assertNotIn('warning', resp)
|
||||
# a TAR the preset does not carry, without an explicit SPI1:
|
||||
# refused with a hint (guessing the MSL is not an option)
|
||||
status, resp = self._post('/api/send-ota', {
|
||||
'sp': '00', 'spi2': '01', 'kic': '15', 'kid': '15',
|
||||
'tar': 'AF4D01'})
|
||||
self.assertEqual(status, 400, resp)
|
||||
self.assertIn('MSL', resp['error'])
|
||||
finally:
|
||||
for patch in reversed(patches):
|
||||
patch.stop()
|
||||
self.assertEqual(seen, ['16', '0A', '21'])
|
||||
|
||||
def test_a_ram_operation_derives_the_spi1_from_the_msl(self):
|
||||
p = self.store.add(self._preset(iccid='8970119000004600098'))
|
||||
self.store.update(p['id'], {'tars': [
|
||||
{'role': 'isd', 'tar': '000000', 'msl': '1A', 'desc': ''},
|
||||
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '16', 'desc': ''},
|
||||
{'role': 'usimRfm', 'tar': 'B00001', 'msl': '16', 'desc': ''}]})
|
||||
captured = []
|
||||
|
||||
def fake_step(server, scc, sp, state, name, apdu, silent=False):
|
||||
captured.append(sp['spi1'])
|
||||
state['steps'].append({'name': name, 'por_status': 'por_ok', 'por_sw': '9000'})
|
||||
return True
|
||||
|
||||
patches = [
|
||||
mock.patch.object(self.srv, '_cap_parse',
|
||||
lambda cap_hex: ('F0414C46416101', 'F0414C4641610101', b'\x01\x02')),
|
||||
mock.patch.object(self.srv, '_cap_apdu_sequence',
|
||||
lambda *a, **k: ['80E60200', '80E80000', '80E60000']),
|
||||
mock.patch.object(self.srv, '_ram_detect_format', lambda *a, **k: 'compact'),
|
||||
mock.patch.object(self.srv, '_ram_send_gp_apdu', fake_step),
|
||||
mock.patch.object(self.srv, '_ram_read_ff21', lambda *a, **k: None),
|
||||
]
|
||||
for patch in patches:
|
||||
patch.start()
|
||||
try:
|
||||
status, resp = self._post('/api/ram-install', {
|
||||
'cap_hex': 'AA', 'preset_id': p['id'], 'kic': '15', 'kid': '15'})
|
||||
finally:
|
||||
for patch in reversed(patches):
|
||||
patch.stop()
|
||||
self.assertEqual(status, 200, resp)
|
||||
self.assertTrue(resp['success'], resp)
|
||||
self.assertTrue(captured)
|
||||
self.assertEqual(set(captured), {'1A'})
|
||||
|
||||
def test_a_counter_less_send_ota_body_still_answers(self):
|
||||
# a pre-built packet without a counter: no final_cntr, no persist,
|
||||
# and above all no int('') crash (v3.8.0 review fix)
|
||||
@@ -811,7 +895,7 @@ class PresetStoreHttpTests(unittest.TestCase):
|
||||
self.server.app = object() # the probe only checks truthiness
|
||||
with mock.patch.object(self.srv, '_ram_send_gp_apdu', fake_step):
|
||||
status, resp = self._post('/api/tar-probe', {
|
||||
'preset_id': p['id'], 'kic': '25', 'kid': '25',
|
||||
'preset_id': p['id'], 'spi1': '16', 'kic': '25', 'kid': '25',
|
||||
'kicKey': 'AA', 'kidKey': 'BB', 'cntr': '0000000005',
|
||||
'tars': ['000000', 'B00000', 'B00001', 'B00200']})
|
||||
self.assertEqual(status, 200, resp)
|
||||
|
||||
@@ -54,6 +54,7 @@ from pysim_simple_server.server import (
|
||||
_ram_step_result,
|
||||
_record_tr,
|
||||
_send_secured_packet,
|
||||
_spi1_for_tar,
|
||||
_spi_from_bytes,
|
||||
_split_secured_packet,
|
||||
_tr_data_only,
|
||||
@@ -2296,13 +2297,61 @@ class ExpandedRemoteTests(unittest.TestCase):
|
||||
self.assertIn('wrong length', error)
|
||||
|
||||
|
||||
class Spi1ForTarTests(unittest.TestCase):
|
||||
"""The SPI1 of a packet comes from the MSL (Minimum SPI1) of its TAR
|
||||
(TS 102 226 8.2.1.3.2.4) - the preset's TAR table is the source."""
|
||||
|
||||
PRESET = {'tars': [
|
||||
{'role': 'isd', 'tar': '000000', 'msl': '16', 'desc': ''},
|
||||
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '10', 'desc': ''},
|
||||
{'role': 'usimRfm', 'tar': 'B00001', 'msl': '16', 'desc': ''},
|
||||
{'tar': 'AF4D01', 'msl': '0A', 'desc': 'applet'},
|
||||
]}
|
||||
|
||||
def test_a_hand_send_keeps_its_explicit_spi1(self):
|
||||
self.assertEqual(_spi1_for_tar(self.PRESET, '000000', '21'), ('21', ''))
|
||||
self.assertEqual(_spi1_for_tar(self.PRESET, '000000', '1A'), ('1A', ''))
|
||||
|
||||
def test_an_explicit_spi1_below_the_msl_warns(self):
|
||||
spi1, warning = _spi1_for_tar(self.PRESET, 'B00000', '0A')
|
||||
self.assertEqual(spi1, '0A')
|
||||
self.assertIn('MSL 10', warning)
|
||||
self.assertIn('B00000', warning)
|
||||
|
||||
def test_the_comparison_is_numeric(self):
|
||||
# '9' < '10' numerically - a lexicographic compare would miss it
|
||||
_, warning = _spi1_for_tar(self.PRESET, 'B00000', '9')
|
||||
self.assertIn('MSL 10', warning)
|
||||
|
||||
def test_without_an_explicit_value_the_msl_is_used(self):
|
||||
self.assertEqual(_spi1_for_tar(self.PRESET, 'B00001'), ('16', ''))
|
||||
self.assertEqual(_spi1_for_tar(self.PRESET, 'af4d01'), ('0A', ''))
|
||||
|
||||
def test_operations_prefer_the_msl_over_a_stale_explicit_value(self):
|
||||
self.assertEqual(_spi1_for_tar(self.PRESET, 'B00001', '21',
|
||||
prefer_msl=True), ('16', ''))
|
||||
# ... but the explicit value covers a TAR the preset does not carry
|
||||
self.assertEqual(_spi1_for_tar(self.PRESET, 'B00200', '21',
|
||||
prefer_msl=True), ('21', ''))
|
||||
|
||||
def test_an_unknown_tar_without_an_explicit_value_is_refused(self):
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
_spi1_for_tar(self.PRESET, 'B00200')
|
||||
self.assertIn('B00200', str(cm.exception))
|
||||
self.assertIn('MSL', str(cm.exception))
|
||||
with self.assertRaises(ValueError):
|
||||
_spi1_for_tar({}, '000000')
|
||||
|
||||
|
||||
class CounterProbeTests(unittest.TestCase):
|
||||
"""The bounded counter synchronisation probe (v3.9.x)."""
|
||||
|
||||
@staticmethod
|
||||
def _preset(cntr='0000000C2D'):
|
||||
return {'id': 'p1', 'name': 'test card', 'spi1': '16', 'spi2': '21',
|
||||
'tar': '000000',
|
||||
return {'id': 'p1', 'name': 'test card',
|
||||
'tars': [{'role': 'isd', 'tar': '000000', 'msl': '16', 'desc': ''},
|
||||
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '16', 'desc': ''},
|
||||
{'role': 'usimRfm', 'tar': 'B00001', 'msl': '16', 'desc': ''}],
|
||||
'keysets': [{'kic': '25', 'kid': '25', 'kicKey': '00' * 16,
|
||||
'kidKey': '11' * 16, 'cntr': cntr}]}
|
||||
|
||||
@@ -2373,7 +2422,7 @@ class CounterProbeTests(unittest.TestCase):
|
||||
self.assertIn('not defined', str(cm.exception))
|
||||
# SPI1 without a counter check cannot probe
|
||||
no_check = self._preset()
|
||||
no_check['spi1'] = '06' # ciphering+CC but b5b4 = 00: no counter field
|
||||
no_check['tars'][0]['msl'] = '06' # ciphering+CC but b5b4 = 00: no counter field
|
||||
with self.assertRaises(ValueError):
|
||||
_counter_probe_params(no_check, {})
|
||||
# the ceiling must stay below the 40-bit maximum
|
||||
|
||||
+85
-5
@@ -91,15 +91,80 @@ class StoreCrudTests(unittest.TestCase):
|
||||
def test_add_applies_the_form_defaults_and_assigns_an_id(self):
|
||||
p = self.store.add(preset(iccid=DIGITS))
|
||||
self.assertTrue(p['id'])
|
||||
self.assertEqual(p['spi1'], '16')
|
||||
self.assertEqual(p['spi2'], '01')
|
||||
self.assertEqual(p['tar'], '000000')
|
||||
self.assertEqual(p['uiccTar'], 'B00000')
|
||||
self.assertEqual(p['usimTar'], 'B00001')
|
||||
self.assertEqual([t['role'] for t in p['tars']],
|
||||
['isd', 'uiccRfm', 'usimRfm'])
|
||||
self.assertEqual([t['tar'] for t in p['tars']],
|
||||
['000000', 'B00000', 'B00001'])
|
||||
self.assertEqual([t['msl'] for t in p['tars']], ['16', '16', '16'])
|
||||
self.assertNotIn('spi1', p) # the card-wide SPI1 is gone: per-TAR MSL
|
||||
self.assertNotIn('spi2', p)
|
||||
self.assertEqual(p['keysets'][0]['cntr'], '0000000001')
|
||||
self.assertEqual(presets.keyset_kvn(p['keysets'][0]), 1)
|
||||
self.assertEqual([x['id'] for x in self.store.list()], [p['id']])
|
||||
|
||||
def test_a_preset_carries_extra_tars_with_optional_descriptions(self):
|
||||
p = self.store.add(preset(tars=[
|
||||
{'role': 'isd', 'tar': 'AF4D01', 'msl': '1A'},
|
||||
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '16'},
|
||||
{'role': 'usimRfm', 'tar': 'B00001', 'msl': '16'},
|
||||
{'tar': 'AF4D02', 'msl': '0A', 'desc': 'My applet'},
|
||||
{'tar': '', 'msl': '', 'desc': ''}, # a blank editor row
|
||||
]))
|
||||
self.assertEqual([(t['role'], t['tar'], t['msl']) for t in p['tars']],
|
||||
[('isd', 'AF4D01', '1A'), ('uiccRfm', 'B00000', '16'),
|
||||
('usimRfm', 'B00001', '16'), ('', 'AF4D02', '0A')])
|
||||
self.assertEqual(p['tars'][3]['desc'], 'My applet')
|
||||
self.assertEqual(len(p['tars']), 4) # the blank row was dropped
|
||||
|
||||
def test_every_tar_needs_its_msl_and_a_unique_tar(self):
|
||||
with self.assertRaises(presets.PresetError) as ctx:
|
||||
self.store.add(preset(tars=[
|
||||
{'role': 'isd', 'tar': '000000', 'msl': '16'},
|
||||
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '16'},
|
||||
{'role': 'usimRfm', 'tar': 'B00001', 'msl': '16'},
|
||||
{'tar': 'AF4D02', 'msl': ''},
|
||||
]))
|
||||
self.assertIn('MSL', str(ctx.exception))
|
||||
with self.assertRaises(presets.PresetError) as ctx:
|
||||
self.store.add(preset(tars=[
|
||||
{'role': 'isd', 'tar': '000000', 'msl': '16'},
|
||||
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '16'},
|
||||
{'role': 'usimRfm', 'tar': '000000', 'msl': '16'},
|
||||
]))
|
||||
self.assertIn('duplicate TAR', str(ctx.exception))
|
||||
with self.assertRaises(presets.PresetError) as ctx:
|
||||
self.store.add(preset(tars=[
|
||||
{'role': 'isd', 'tar': '0000', 'msl': '16'}]))
|
||||
self.assertIn('three hex bytes', str(ctx.exception))
|
||||
|
||||
def test_tar_lookup_helpers(self):
|
||||
p = self.store.add(preset(tars=[
|
||||
{'role': 'isd', 'tar': 'AF4D01', 'msl': '1A'},
|
||||
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '16'},
|
||||
{'role': 'usimRfm', 'tar': 'B00001', 'msl': '16'},
|
||||
{'tar': 'AF4D02', 'msl': '0A', 'desc': 'My applet'},
|
||||
]))
|
||||
self.assertEqual(presets.role_tar(p, 'isd'), 'AF4D01')
|
||||
self.assertEqual(presets.role_tar(p, 'usimRfm'), 'B00001')
|
||||
self.assertEqual(presets.role_tar(p, 'nope'), '')
|
||||
self.assertEqual(presets.tar_msl(p, 'af4d01'), '1A')
|
||||
self.assertEqual(presets.tar_msl(p, 'AF4D02'), '0A')
|
||||
self.assertEqual(presets.tar_msl(p, '123456'), '')
|
||||
self.assertIsNone(presets.tar_entry(p, ''))
|
||||
self.assertEqual(presets.tar_entry(p, 'AF4D02')['desc'], 'My applet')
|
||||
|
||||
def test_update_replaces_the_tar_table(self):
|
||||
p = self.store.add(preset())
|
||||
upd = self.store.update(p['id'], {'tars': [
|
||||
{'role': 'isd', 'tar': 'AF4D01', 'msl': '1A'},
|
||||
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '16'},
|
||||
{'role': 'usimRfm', 'tar': 'B00001', 'msl': '16'}]})
|
||||
self.assertEqual(presets.role_tar(upd, 'isd'), 'AF4D01')
|
||||
self.assertEqual(presets.tar_msl(upd, 'AF4D01'), '1A')
|
||||
# a malformed tars value must not silently reset the table
|
||||
again = self.store.update(p['id'], {'tars': 'nonsense'})
|
||||
self.assertEqual(presets.role_tar(again, 'isd'), 'AF4D01')
|
||||
|
||||
def test_a_preset_carries_several_keysets(self):
|
||||
p = self.store.add(preset(keysets=[keyset(kic='15', kid='15'),
|
||||
keyset(kic='29', kid='29', cntr='0000000005')]))
|
||||
@@ -317,6 +382,21 @@ class StorePersistenceTests(unittest.TestCase):
|
||||
self.assertEqual(p['keysets'][0]['cntr'], '0000000007')
|
||||
self.assertEqual(p['keysets'][0]['kicKey'], '11')
|
||||
self.assertNotIn('cntr', p)
|
||||
# the card-wide spi1 becomes the MSL of all three role entries
|
||||
self.assertEqual([t['msl'] for t in p['tars']], ['16', '16', '16'])
|
||||
|
||||
def test_a_v391_flat_tars_preset_converts_to_the_tar_table(self):
|
||||
path = pathlib.Path(self.tmp.name) / 'card_presets.json'
|
||||
path.write_text(json.dumps({'version': 2, 'presets': [
|
||||
{'name': 'Old', 'tar': 'AF4D01', 'uiccTar': 'B00000',
|
||||
'usimTar': 'B00001', 'spi1': '1A', 'spi2': '21',
|
||||
'keysets': [keyset()]}]}), encoding='utf-8')
|
||||
store = make_store(self.tmp.name)
|
||||
p = store.list()[0]
|
||||
self.assertEqual([t['tar'] for t in p['tars']],
|
||||
['AF4D01', 'B00000', 'B00001'])
|
||||
self.assertEqual([t['msl'] for t in p['tars']], ['1A', '1A', '1A'])
|
||||
self.assertNotIn('spi2', p)
|
||||
|
||||
def test_no_temporary_files_are_left_behind(self):
|
||||
store = make_store(self.tmp.name)
|
||||
|
||||
@@ -22,6 +22,13 @@ def _resolver(name):
|
||||
return {n.upper(): c for c, n in S.PROACTIVE_TYPE_NAMES.items()}.get(name.upper())
|
||||
|
||||
|
||||
def preset_tars(isd='000000', msl='16'):
|
||||
"""The three mandatory role entries of a card preset's TAR table."""
|
||||
return [{'role': 'isd', 'tar': isd, 'msl': msl, 'desc': ''},
|
||||
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '16', 'desc': ''},
|
||||
{'role': 'usimRfm', 'tar': 'B00001', 'msl': '16', 'desc': ''}]
|
||||
|
||||
|
||||
class TestValidation(unittest.TestCase):
|
||||
def test_minimal_script_normalises(self):
|
||||
script = T.normalise_script({'name': 'demo', 'steps': [
|
||||
@@ -354,8 +361,10 @@ class TestRunnerActions(RunnerTestCase):
|
||||
def test_scp80_uses_the_preset_and_advances_the_counter(self):
|
||||
scc = FakeScc()
|
||||
server = FakeServer(scc)
|
||||
# the ISD entry carries a distinctive TAR: the assertion below must
|
||||
# fail if the runner ever falls back to a built-in default
|
||||
preset = {'kic': '15', 'kid': '15', 'kicKey': 'AA' * 16, 'kidKey': 'BB' * 16,
|
||||
'counter': '0000000A', 'tar': 'B00000', 'spi1': '16', 'spi2': '01'}
|
||||
'counter': '0000000A', 'tars': preset_tars(isd='B00000')}
|
||||
with mock.patch.object(S, '_build_secured_packet',
|
||||
return_value=('AA' * 10, {})) as build, \
|
||||
mock.patch.object(S, '_send_secured_packet',
|
||||
@@ -401,7 +410,7 @@ class TestRunnerActions(RunnerTestCase):
|
||||
def test_scp80_step_overrides_tar_and_spi_only(self):
|
||||
scc = FakeScc()
|
||||
preset = {'kic': '15', 'kid': '15', 'kicKey': 'AA' * 16, 'kidKey': 'BB' * 16,
|
||||
'counter': '00000001', 'tar': 'B00000', 'spi1': '16', 'spi2': '01'}
|
||||
'counter': '00000001', 'tars': preset_tars()}
|
||||
with mock.patch.object(S, '_build_secured_packet', return_value=('AA' * 10, {})) as build, \
|
||||
mock.patch.object(S, '_send_secured_packet',
|
||||
return_value={'success': True, 'sw': '9000',
|
||||
@@ -421,9 +430,9 @@ class TestRunnerActions(RunnerTestCase):
|
||||
]}, S._test_command_type)
|
||||
self.assertIn('incomplete', S._test_preset_error(script, {}) or '')
|
||||
full = {'kic': '15', 'kid': '15', 'kicKey': 'AA', 'kidKey': 'BB',
|
||||
'counter': '00000000', 'tar': 'B00000', 'spi1': '16', 'spi2': '01'}
|
||||
'counter': '00000000', 'tars': preset_tars()}
|
||||
self.assertIsNone(S._test_preset_error(script, full))
|
||||
no_tar = dict(full, tar='')
|
||||
no_tar = dict(full, tars=[])
|
||||
self.assertIn('TAR', S._test_preset_error(script, no_tar) or '')
|
||||
|
||||
def test_file_write_and_read_actions(self):
|
||||
|
||||
Reference in New Issue
Block a user