9fb46b786f
A script's `event` value may be a code (a hex byte or a number) or one of the server's name keywords (location_status, access_tech, network_rejection, data_connection, ... - events.py's EVENT_NAMES); the API accepts both. The PWA parsed every value as a hex string: the step list showed 'EVENT 0xaccess_tech' (the unparsed fallback), the editor fell back to event 03 (Location status) with the location field set, and saving then rewrote the step to that event - silently corrupting name-authored scripts (the two Alfa2G trigger scripts use access_tech/data_connection). A mirrored EVENT_ALIASES map plus an eventCode() resolver (number / hex byte / name -> code) now feed the step summary, the editor's event block and the event check; the summary's fallback no longer prefixes a non-numeric value with 0x, and the editor keeps its canonical hex form on save (the same code). Drift guard: the aliases live in frontend/tests/event_vectors.json, asserted by the Node test (the PWA's map + resolver) and the Python test (events.EVENT_NAMES + resolve_event). Tests: 856 frontend + 839 Python; sw cache simple-v419.
110 lines
5.0 KiB
Python
110 lines
5.0 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_shared_event_name_aliases(self):
|
|
# the script's `event` value may be one of these name keywords; the
|
|
# PWA mirrors the map (frontend/tests/event_forms.test.js)
|
|
want = {name: int(hexv, 16) for name, hexv in VECTORS['aliases'].items()}
|
|
self.assertEqual(events.EVENT_NAMES, want)
|
|
for name, code in want.items():
|
|
self.assertEqual(events.resolve_event(name), code, name)
|
|
self.assertEqual(events.resolve_event(hex(code)[2:].upper().zfill(2)), code)
|
|
self.assertIsNone(events.resolve_event('nonsense'))
|
|
|
|
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()
|