152878062f
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.
100 lines
4.4 KiB
Python
100 lines
4.4 KiB
Python
"""Server-side event-data builders (the semantic test-script `event` action).
|
|
|
|
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):
|
|
"""Byte parity with the PWA builders from the shared fixture."""
|
|
|
|
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_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 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()
|
|
self.assertRegex(out, r'^9D0100AA01001C01002607[0-9A-F]{14}9B0100$')
|
|
|
|
def test_plmn_encoding(self):
|
|
self.assertEqual(events.plmn_hex('250', '01'), '52F010')
|
|
self.assertEqual(events.plmn_hex('310', '410'), '130014')
|
|
self.assertEqual(events.plmn_hex('262', '02'), '62F220')
|
|
self.assertEqual(events.plmn_hex('310', '260'), '130062')
|
|
|
|
|
|
class EventValidationTests(unittest.TestCase):
|
|
def test_resolve_event_accepts_names_and_hex(self):
|
|
self.assertEqual(events.resolve_event('location status'), 0x03)
|
|
self.assertEqual(events.resolve_event('Location_Status'), 0x03)
|
|
self.assertEqual(events.resolve_event('0B'), 0x0B)
|
|
self.assertEqual(events.resolve_event('0x12'), 0x12)
|
|
self.assertEqual(events.resolve_event(0x1D), 0x1D)
|
|
self.assertIsNone(events.resolve_event('nope'))
|
|
self.assertIsNone(events.resolve_event(300))
|
|
self.assertIsNone(events.resolve_event(True))
|
|
self.assertIsNone(events.resolve_event(''))
|
|
|
|
def test_field_validation(self):
|
|
with self.assertRaises(events.EventError):
|
|
events.build(0x03, {'status': '9'})
|
|
with self.assertRaises(events.EventError):
|
|
events.build(0x03, {'status': '0', 'mcc': '25'})
|
|
with self.assertRaises(events.EventError):
|
|
events.build(0x0B, {'tech': '300'})
|
|
with self.assertRaises(events.EventError):
|
|
events.build(0x12, {'reg_type': '0x12'})
|
|
with self.assertRaises(events.EventError):
|
|
events.build(0x12, {'reg_type': '9', 'tac': 'ABC'})
|
|
with self.assertRaises(events.EventError):
|
|
events.build(0x12, {'reg_type': '9', 'ext_info_type': '1', 'ext_info': 'A'})
|
|
with self.assertRaises(events.EventError):
|
|
events.build(0x1D, {'status': '0', 'apn': 'x' * 101})
|
|
with self.assertRaises(events.EventError):
|
|
events.build(0x1D, {'status': '0', 'ti': 'ZZ'})
|
|
# an event without a semantic builder is refused with a hint
|
|
with self.assertRaises(events.EventError) as ctx:
|
|
events.normalise(0x05, {})
|
|
self.assertIn('envelope', str(ctx.exception))
|
|
|
|
def test_normalise_returns_the_validated_fields(self):
|
|
fields = {'status': '1', 'mcc': '250'}
|
|
self.assertEqual(events.normalise(0x03, fields), fields)
|
|
self.assertEqual(events.normalise(0x0B, None), {})
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|