v3.18.2: review fixes - sticky event-JSON error, store keep-on-load, write queue
The four findings from the review of the 3.15-3.18 batch: - The event step's `_eventFieldsError` flag is cleared at the start of each collect: fixing malformed JSON used to leave the save refused until the editor was closed and reopened. Regression test covers invalid -> fixed. - The script store keeps an entry that no longer validates on load (a tightened engine rule or a hand edit): it is served, fails at run start with the validator's message and can be fixed in the editor, instead of being dropped from memory and erased by the next save. Mutation paths still refuse it. (The presets store already had this behaviour.) - Writebacks are serialized per script (`_testWriteChains`): two quick saves of an unsaved draft no longer create two server-side scripts - the window could be minutes while a RAM operation holds the card lock. Regression test asserts one create + one update. - Deleting a script cancels its pending debounced save and marks it deleted, so a queued writeback never resurrects it; a create already in flight is undone with a delete. Regression test covers the pending-save case. - The event vectors moved to a shared fixture (frontend/tests/event_vectors.json) asserted by BOTH suites - a one-sided builder change now fails the other suite instead of drifting silently. Tests: 816 frontend + 716 Python; sw cache simple-v383.
This commit is contained in:
+26
-67
@@ -1,89 +1,48 @@
|
||||
"""Server-side event-data builders (the semantic test-script `event` action).
|
||||
|
||||
The byte vectors are generated from the PWA's own `EVENT_FORMS` builders (the
|
||||
Node generator run over `frontend/index.html`) and pinned here, so the two
|
||||
implementations cannot drift apart: every vector below is asserted by the
|
||||
frontend suite too (or was produced by the very code it asserts).
|
||||
The byte vectors live in the SHARED fixture `frontend/tests/event_vectors.json`:
|
||||
the Node suite asserts the same file against the PWA's own `EVENT_FORMS`
|
||||
builders, this suite asserts it against `events.py`, so neither implementation
|
||||
can drift alone (a change on one side fails the other until the fixture and the
|
||||
code agree).
|
||||
"""
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from pysim_simple_server import events
|
||||
|
||||
VECTORS = json.loads((pathlib.Path(__file__).resolve().parents[1]
|
||||
/ 'frontend' / 'tests' / 'event_vectors.json')
|
||||
.read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
def st(year=2026, mon=9, day=30, hour=21, minute=55, sec=0):
|
||||
return time.struct_time((year, mon, day, hour, minute, sec, 0, 0, -1))
|
||||
|
||||
|
||||
class EventVectorTests(unittest.TestCase):
|
||||
def test_location_status(self):
|
||||
cases = [
|
||||
({'status': '0', 'mcc': '250', 'mnc': '01', 'lac': '00FF', 'cell': '0001'},
|
||||
'9B0100930752F01000FF0001'),
|
||||
({'status': '1', 'mcc': '250'}, '9B0101'),
|
||||
({'status': '0'}, '9B0100'),
|
||||
({'status': '0', 'mcc': '310', 'mnc': '410', 'lac': 'ABCD', 'cell': '1234'},
|
||||
'9B01009307130014ABCD1234'),
|
||||
]
|
||||
for fields, want in cases:
|
||||
self.assertEqual(events.build(0x03, fields).upper(), want)
|
||||
"""Byte parity with the PWA builders from the shared fixture."""
|
||||
|
||||
def test_access_tech(self):
|
||||
for fields, want in [({'tech': '0'}, 'BF0100'), ({'tech': '8'}, 'BF0108'),
|
||||
({'tech': '12'}, 'BF010C')]:
|
||||
self.assertEqual(events.build(0x0B, fields).upper(), want)
|
||||
def test_the_shared_event_vectors(self):
|
||||
self.assertTrue(VECTORS['events'], 'the fixture must not be empty')
|
||||
for v in VECTORS['events']:
|
||||
code = int(v['event'], 16)
|
||||
self.assertEqual(events.build(code, v['fields']).upper(), v['hex'],
|
||||
'event 0x%s %s' % (v['event'], v['fields']))
|
||||
|
||||
def test_network_rejection(self):
|
||||
cases = [
|
||||
# location updating -> LAI, defaults filled
|
||||
({'reg_type': '0'}, 'F40100BF0100F50102930552F0100000'),
|
||||
# GPRS -> RAI
|
||||
({'reg_type': '3', 'mcc': '262', 'mnc': '02', 'lac': '00FF',
|
||||
'rac': '05', 'access_tech': '3', 'cause': '15'},
|
||||
'F40103BF0103F5010FF30662F22000FF05'),
|
||||
# EPS -> TAI (6-hex TAC) + the extended EMM cause
|
||||
({'reg_type': '9', 'mcc': '310', 'mnc': '260', 'tac': 'A1B2C3',
|
||||
'access_tech': '8', 'cause': '7', 'ext_cause': '22'},
|
||||
'F40109BF0108F501077D06130062A1B2C3570116'),
|
||||
# 5GS -> TAI (4-hex TAC)
|
||||
({'reg_type': '17', 'tac': '0001', 'access_tech': '10', 'cause': '2'},
|
||||
'F40111BF010AF501027D0552F0100001'),
|
||||
# the extended information object (CAG ID)
|
||||
({'reg_type': '9', 'access_tech': '8', 'cause': '2',
|
||||
'ext_info_type': '1', 'ext_info': '00000001'},
|
||||
'F40109BF0108F501027D0552F0100001F2050100000001'),
|
||||
]
|
||||
for fields, want in cases:
|
||||
self.assertEqual(events.build(0x12, fields).upper(), want)
|
||||
|
||||
def test_data_connection(self):
|
||||
cases = [
|
||||
({'status': '0', 'type': '0', 'ti': '00', 'loc_status': '0'},
|
||||
'9D0100AA01001C01009B0100'),
|
||||
({'status': '1', 'type': '2', 'cause': '26', 'ti': '85', 'mcc': '250',
|
||||
'mnc': '01', 'lac': '00FF', 'cell': '0001', 'tech': '8',
|
||||
'loc_status': '1', 'apn': 'internet', 'pdp_type': '3'},
|
||||
'9D0101AA0102AE011A1C0185130752F01000FF0001BF01089B0101'
|
||||
'C708696E7465726E65740B0103'),
|
||||
({'status': '2', 'type': '1', 'ti': '', 'mcc': '250', 'loc_status': '2'},
|
||||
'9D0102AA01011C0100130752F010000000019B0102'),
|
||||
({'status': '0', 'type': '2', 'ti': 'AA', 'mcc': '262', 'mnc': '01',
|
||||
'lac': '0001', 'cell': '0002', 'tech': '7', 'loc_status': '1',
|
||||
'apn': 'web.operator.example', 'pdp_type': '4'},
|
||||
'9D0100AA01021C01AA130762F21000010002BF01079B0101'
|
||||
'C7147765622E6F70657261746F722E6578616D706C650B0104'),
|
||||
]
|
||||
for fields, want in cases:
|
||||
self.assertEqual(events.build(0x1D, fields).upper(), want)
|
||||
def test_the_shared_scts_vectors(self):
|
||||
self.assertTrue(VECTORS['scts'], 'the fixture must not be empty')
|
||||
for v in VECTORS['scts']:
|
||||
stamp = time.struct_time((v['year'], v['month'], v['day'], v['hour'],
|
||||
v['min'], v['sec'], 0, 0, -1))
|
||||
self.assertEqual(events.scts_bytes(stamp, v['tz_east_min']).hex().upper(),
|
||||
v['hex'])
|
||||
|
||||
def test_the_host_clock_date_time_object(self):
|
||||
# the PWA encoder's vector: UTC+3, 2026-09-30 21:55:00
|
||||
self.assertEqual(events.scts_bytes(st(), 180).hex().upper(), '62900312550021')
|
||||
# UTC-2:30: 10 quarters with the sign bit
|
||||
self.assertEqual(events.scts_bytes(st(2026, 1, 5, 8, 7, 9), -150).hex().upper(),
|
||||
'62105080709009')
|
||||
# the data-connection builder embeds it as the 8.39 object 26 07 ...
|
||||
# the data-connection builder embeds the 8.39 object 26 07 ...
|
||||
out = events.build(0x1D, {'status': '0', 'type': '0', 'ti': '00',
|
||||
'loc_status': '0', 'datetime': 'now'},
|
||||
now=st()).upper()
|
||||
|
||||
@@ -113,6 +113,32 @@ class StorePersistenceTests(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_an_entry_that_fails_the_validator_is_kept_and_survives_a_save(self):
|
||||
# a tightened engine rule (or a hand edit) must not silently destroy a
|
||||
# stored script on load - it stays, fails at run/update time, and the
|
||||
# next save keeps it on disk
|
||||
store = make_store(self.tmp.name, validator=lambda raw: raw)
|
||||
store.add(script('one'))
|
||||
|
||||
def strict(raw):
|
||||
if raw['name'] == 'one':
|
||||
raise ValueError('rule tightened in a later version')
|
||||
return raw
|
||||
|
||||
reopened = make_store(self.tmp.name, validator=strict)
|
||||
self.assertEqual([s['name'] for s in reopened.list()], ['one'])
|
||||
reopened.add(script('two'))
|
||||
again = make_store(self.tmp.name, validator=strict)
|
||||
self.assertEqual(sorted(s['name'] for s in again.list()), ['one', 'two'])
|
||||
# the kept entry is refused by the mutation paths while it stays invalid
|
||||
# (here the validator rejects the name 'one'), and renaming it - the
|
||||
# editor's fix - succeeds
|
||||
one = next(s for s in reopened.list() if s['name'] == 'one')
|
||||
with self.assertRaises(test_scripts.TestScriptError):
|
||||
reopened.update(one['id'], {'name': 'one'})
|
||||
fixed = reopened.update(one['id'], {'name': 'renamed'})
|
||||
self.assertEqual(fixed['name'], 'renamed')
|
||||
|
||||
def test_round_trip_keeps_ids_and_timestamps(self):
|
||||
store = make_store(self.tmp.name)
|
||||
s = store.add(script('one'))
|
||||
|
||||
Reference in New Issue
Block a user