catrelease: the CAT item introduction-release table (release guard, stage 1)

The table behind the release-aware command/parameter filter: which proactive
command, event, data object and PLI qualifier appeared in which release,
seeded from the curated reference in the workspace
docs/uicc/ETSI-3GPP-Releases.md (3GPP TS 31.111 Annex W, ETSI TS 102 223
Annex V, ETSI TS 101 220 Annex Q; the version major is the release).

- pysim_simple_server/catrelease.py: CAT_RELEASE (events/commands/objects/pli),
  release_label() - everything below Rel-6 collapses to "pre-Rel-6" - and
  item_release(); an item absent from the table is one the histories do not
  pin down and is never filtered.
- The PWA mirrors it as CAT_RELEASE + catReleaseLabel() next to the other
  release table (ETSI_TOOLKIT_RELEASE); no behaviour change yet - stage 2
  (the card-release selector and REL-x badges) and stage 3 (the filtering of
  the event list, PLI and push builders) consume it.
- Drift guards: tests/test_catrelease.py (module <-> PWA mirror, event-list
  coverage through 0x1F minus the Void 0x1A, helper vectors) and
  frontend/tests/cat_release.test.js (the PWA side).
- sw cache simple-v360.
This commit is contained in:
2026-10-02 08:57:02 +03:00
parent c14450c3ae
commit 5dd056beb8
5 changed files with 369 additions and 1 deletions
+73
View File
@@ -0,0 +1,73 @@
"""Tests for the CAT item introduction-release table (catrelease) and its PWA mirror."""
import pathlib
import re
import unittest
from pysim_simple_server import catrelease
HTML = pathlib.Path(__file__).resolve().parents[1].joinpath('frontend', 'index.html')
def _pwa_block():
html = HTML.read_text(encoding='utf-8')
m = re.search(r"const CAT_RELEASE = \{(.*?)\n\};", html, re.S)
assert m is not None, 'CAT_RELEASE not found in index.html'
return m.group(1), html
class TestCatReleaseTable(unittest.TestCase):
def test_pwa_mirror_matches_the_module(self):
# the PWA's release guard uses the same table (drift guard, like
# ETSI_TOOLKIT_RELEASE in test_cap_memory)
block, _ = _pwa_block()
ev = re.search(r"events:\s*\{(.*?)\}", block, re.S)
self.assertIsNotNone(ev, 'events section not found')
self.assertEqual(
{int(k, 16): int(v)
for k, v in re.findall(r"(0x[0-9A-Fa-f]{2}):\s*(\d+)", ev.group(1))},
catrelease.CAT_RELEASE['events'])
for section in ('commands', 'objects', 'pli'):
m = re.search(r"%s:\s*\{(.*?)\}" % section, block, re.S)
self.assertIsNotNone(m, section)
self.assertEqual(
{k: int(v) for k, v in re.findall(r"'([^']+)':\s*(\d+)", m.group(1))},
catrelease.CAT_RELEASE[section], section)
def test_events_cover_the_pwa_event_list(self):
# every event the UI names through 0x1F except the Void 0x1A carries
# an introduction release; the reserved 0x20+ codes do not
_, html = _pwa_block()
names = re.search(r"const EVENT_NAMES = \{(.*?)\};", html, re.S)
self.assertIsNotNone(names, 'EVENT_NAMES not found in index.html')
codes = {int(k, 16) for k in re.findall(r"(0x[0-9A-Fa-f]{2}):", names.group(1))}
self.assertEqual(set(catrelease.CAT_RELEASE['events']),
{c for c in codes if c <= 0x1F} - {0x1A})
def test_release_label_collapses_below_rel6(self):
self.assertEqual(catrelease.release_label(4), 'pre-Rel-6')
self.assertEqual(catrelease.release_label(5), 'pre-Rel-6')
self.assertEqual(catrelease.release_label(6), 'Rel-6')
self.assertEqual(catrelease.release_label(18), 'Rel-18')
# unknown never gets a label - it must not be filtered
self.assertEqual(catrelease.release_label(None), '')
self.assertEqual(catrelease.release_label(0), '')
def test_item_release_lookup(self):
self.assertEqual(catrelease.item_release('events', 0x1D), 14)
self.assertEqual(catrelease.item_release('commands', 'LSI COMMAND'), 17)
self.assertIsNone(catrelease.item_release('events', 0x1A))
self.assertIsNone(catrelease.item_release('nope', 'x'))
def test_table_sanity(self):
for section, table in catrelease.CAT_RELEASE.items():
self.assertTrue(table, section)
for key, rel in table.items():
self.assertIsInstance(rel, int, (section, key))
self.assertTrue(4 <= rel <= 18, (section, key, rel))
self.assertTrue(all(0 <= c <= 0x1F for c in catrelease.CAT_RELEASE['events']))
if __name__ == '__main__':
unittest.main()