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:
@@ -9016,6 +9016,103 @@ const ETSI_TOOLKIT_RELEASE = {
|
||||
'1.12': 'REL-15/16/17', '1.13': 'REL-17', '1.14': 'REL-17/18',
|
||||
};
|
||||
|
||||
// CAT/USAT item introduction releases (the release guard): which proactive
|
||||
// command, event, data object and PLI qualifier appeared in which release.
|
||||
// Curated from the change histories of the three pinned specs (3GPP TS 31.111
|
||||
// Annex W, ETSI TS 102 223 Annex V, ETSI TS 101 220 Annex Q); the reference
|
||||
// with the per-row sources lives in the workspace at
|
||||
// docs/uicc/ETSI-3GPP-Releases.md, the Python mirror in
|
||||
// pysim_simple_server/catrelease.py (drift-guarded by tests/test_catrelease.py).
|
||||
// Release 4 is the specification baseline; an absent item means the histories
|
||||
// do not pin it down - never filter on it. Releases below 6 display as
|
||||
// "pre-Rel-6" (catReleaseLabel).
|
||||
const CAT_RELEASE = {
|
||||
events: {
|
||||
0x00: 4, 0x01: 4, 0x02: 4, 0x03: 4, 0x04: 4, 0x05: 4, 0x06: 4, 0x07: 4,
|
||||
0x08: 4, 0x09: 4, 0x0A: 4, 0x0B: 4, 0x0C: 4, 0x0D: 4, 0x0E: 4, 0x0F: 4,
|
||||
0x10: 6, 0x11: 7, 0x12: 8, 0x13: 9, 0x14: 12, 0x15: 9, 0x16: 9, 0x17: 10,
|
||||
0x18: 10, 0x19: 10, 0x1B: 11, 0x1C: 12, 0x1D: 14, 0x1E: 17, 0x1F: 16,
|
||||
},
|
||||
commands: {
|
||||
'REFRESH': 4, 'MORE TIME': 4,
|
||||
'POLL INTERVAL': 4, 'POLLING OFF': 4,
|
||||
'SET UP EVENT LIST': 4, 'SET UP CALL': 4,
|
||||
'SEND SS': 4, 'SEND USSD': 4,
|
||||
'SEND SHORT MESSAGE': 4, 'SEND DTMF': 4,
|
||||
'LAUNCH BROWSER': 4, 'PLAY TONE': 4,
|
||||
'DISPLAY TEXT': 4, 'GET INKEY': 4,
|
||||
'GET INPUT': 4, 'SELECT ITEM': 4,
|
||||
'SET UP MENU': 4, 'PROVIDE LOCAL INFORMATION': 4,
|
||||
'TIMER MANAGEMENT': 4, 'SET UP IDLE MODE TEXT': 4,
|
||||
'RUN AT COMMAND': 4, 'LANGUAGE NOTIFICATION': 4,
|
||||
'OPEN CHANNEL': 4, 'CLOSE CHANNEL': 4,
|
||||
'SEND DATA': 4, 'RECEIVE DATA': 4,
|
||||
'GET CHANNEL STATUS': 4, 'SERVICE SEARCH': 4,
|
||||
'GET SERVICE INFORMATION': 4, 'DECLARE SERVICE': 4,
|
||||
'SET FRAMES': 6, 'GET FRAMES STATUS': 6,
|
||||
'RETRIEVE MULTIMEDIA MESSAGE': 7, 'SUBMIT MULTIMEDIA MESSAGE': 7,
|
||||
'DISPLAY MULTIMEDIA MESSAGE': 7, 'GEOGRAPHICAL LOCATION REQUEST': 8,
|
||||
'ACTIVATE': 13, 'CONTACTLESS STATE CHANGED': 9,
|
||||
'COMMAND CONTAINER': 10, 'ENCAPSULATED SESSION CONTROL': 11,
|
||||
'LSI COMMAND': 17,
|
||||
},
|
||||
objects: {
|
||||
'06/86 Address': 4,
|
||||
'08/88 Subaddress': 4,
|
||||
'1C/9C Transaction identifier': 4,
|
||||
'1A/9A Cause': 4,
|
||||
'34/B4 Browser termination cause': 4,
|
||||
'35/B5 Bearer description': 4,
|
||||
'37/B7 Channel data length': 4,
|
||||
'38/B8 Channel status': 4,
|
||||
'3E/BE Other address (local / destination)': 4,
|
||||
'40/C0 Display parameters': 4,
|
||||
'64/E4 Browsing status': 4,
|
||||
'65/E5 Network search mode': 4,
|
||||
'67/E7 Frames Information': 6,
|
||||
'4B/CB (I-)WLAN Access Status': 7,
|
||||
'55/D5 CSG cell selection status': 9,
|
||||
'56/D6 CSG ID': 9,
|
||||
'57/D7 HNB name': 9,
|
||||
'31/B1 IMS URI': 10,
|
||||
'77/F7 IARI': 10,
|
||||
'77/F7 IMPU list': 11,
|
||||
'78/F8 IMS status code': 11,
|
||||
'7E/FE Media Type': 12,
|
||||
'09/89 PLMN ID': 4,
|
||||
'3F/BF Access technology': 4,
|
||||
'B4 Supported Radio Access Technologies': 13,
|
||||
'9D Data connection status': 14,
|
||||
'AA Data connection type': 14,
|
||||
'AE (E/5G)SM cause': 14,
|
||||
'C7 Network Access Name': 14,
|
||||
'0B/8B PDP/PDN/PDU type': 14,
|
||||
'26/A6 Date-Time and Time zone': 4,
|
||||
'13/93 Location Information': 4,
|
||||
'55/D5 CAG cell selection status': 17,
|
||||
'56/D6 CAG information list': 17,
|
||||
'57/D7 CAG Human-readable network name list': 17,
|
||||
'55/D5 Slices status': 16,
|
||||
'56/D6 Slices information (served)': 16,
|
||||
'78/F8 Allowed slices information': 16,
|
||||
'77/F7 Allowed slices with S-NSSAI mapping': 18,
|
||||
'D7 Rejected slices with S-NSSAI mapping': 18,
|
||||
'B1 Rejected slices information': 18,
|
||||
},
|
||||
pli: {
|
||||
'00': 4, '01': 4, '02': 4, '03': 4, '04': 4, '06': 4, '07': 4, '08': 6,
|
||||
'09': 4, '0A': 4, '0D': 8, '0E': 12, '0F': 10, '10': 10, '11': 9, '12': 10,
|
||||
'13': 10, '14': 13, '15': 16, '16': 17, '17': 18, '1A': 13,
|
||||
},
|
||||
};
|
||||
|
||||
// The display label of an introduction release: everything below Rel-6
|
||||
// collapses to "pre-Rel-6"; 0/undefined yields ''.
|
||||
function catReleaseLabel(rel) {
|
||||
if (!rel) return '';
|
||||
return rel < 6 ? 'pre-Rel-6' : 'Rel-' + rel;
|
||||
}
|
||||
|
||||
// Normalise an AID (strip spacing, uppercase) and reject malformed input;
|
||||
// '' when it cannot be an AID (5-16 bytes).
|
||||
function jcAidNorm(aid) {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v359';
|
||||
const CACHE = 'simple-v360';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||
|
||||
function extractBlock(startMarker, endMarker) {
|
||||
const start = html.indexOf(startMarker);
|
||||
const end = html.indexOf(endMarker, start);
|
||||
if (start < 0 || end < 0) throw new Error('block not found: ' + startMarker);
|
||||
return html.slice(start, end);
|
||||
}
|
||||
|
||||
function extractFunc(src, name) {
|
||||
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||
const m = re.exec(src);
|
||||
if (!m) throw new Error('function ' + name + ' not found');
|
||||
let i = m.index + m[0].length - 1;
|
||||
let depth = 0;
|
||||
for (; i < src.length; i++) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
return src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
// Rewrite top-level const -> var so the maps leak out of sloppy-mode eval.
|
||||
eval(extractBlock('const CAT_RELEASE = {', '// The display label of an introduction release')
|
||||
.replace(/^const /gm, 'var '));
|
||||
eval(extractFunc(html, 'catReleaseLabel'));
|
||||
eval(extractBlock('const EVENT_NAMES = {', 'const REJECTION_CAUSES = [').replace(/^const /gm, 'var '));
|
||||
|
||||
test('CAT_RELEASE carries the introduction releases of the event list', () => {
|
||||
assert.strictEqual(CAT_RELEASE.events[0x10], 6);
|
||||
assert.strictEqual(CAT_RELEASE.events[0x12], 8);
|
||||
assert.strictEqual(CAT_RELEASE.events[0x1D], 14);
|
||||
assert.strictEqual(CAT_RELEASE.events[0x1E], 17);
|
||||
assert.strictEqual(CAT_RELEASE.events[0x1F], 16);
|
||||
// the Void 0x1A is not an event and the reserved 0x20+ codes are not
|
||||
// introduced by any release
|
||||
assert.strictEqual(CAT_RELEASE.events[0x1A], undefined);
|
||||
assert.strictEqual(CAT_RELEASE.events[0x20], undefined);
|
||||
});
|
||||
|
||||
test('every named event through 0x1F has an introduction release', () => {
|
||||
const named = Object.keys(EVENT_NAMES).map(Number).filter(c => c <= 0x1F && c !== 0x1A);
|
||||
for (const code of named) {
|
||||
assert.strictEqual(typeof CAT_RELEASE.events[code], 'number', '0x' + code.toString(16));
|
||||
}
|
||||
assert.strictEqual(Object.keys(CAT_RELEASE.events).length, named.length);
|
||||
});
|
||||
|
||||
test('CAT_RELEASE carries commands, objects and PLI qualifiers', () => {
|
||||
assert.strictEqual(CAT_RELEASE.commands['OPEN CHANNEL'], 4);
|
||||
assert.strictEqual(CAT_RELEASE.commands['SET FRAMES'], 6);
|
||||
assert.strictEqual(CAT_RELEASE.commands['GEOGRAPHICAL LOCATION REQUEST'], 8);
|
||||
assert.strictEqual(CAT_RELEASE.commands['ACTIVATE'], 13);
|
||||
assert.strictEqual(CAT_RELEASE.commands['LSI COMMAND'], 17);
|
||||
assert.strictEqual(CAT_RELEASE.objects['67/E7 Frames Information'], 6);
|
||||
assert.strictEqual(CAT_RELEASE.objects['9D Data connection status'], 14);
|
||||
assert.strictEqual(CAT_RELEASE.objects['55/D5 CAG cell selection status'], 17);
|
||||
assert.strictEqual(CAT_RELEASE.pli['08'], 6);
|
||||
assert.strictEqual(CAT_RELEASE.pli['14'], 13);
|
||||
assert.strictEqual(CAT_RELEASE.pli['15'], 16);
|
||||
assert.strictEqual(CAT_RELEASE.pli['16'], 17);
|
||||
});
|
||||
|
||||
test('catReleaseLabel collapses everything below Rel-6', () => {
|
||||
assert.strictEqual(catReleaseLabel(0), '');
|
||||
assert.strictEqual(catReleaseLabel(undefined), '');
|
||||
assert.strictEqual(catReleaseLabel(4), 'pre-Rel-6');
|
||||
assert.strictEqual(catReleaseLabel(5), 'pre-Rel-6');
|
||||
assert.strictEqual(catReleaseLabel(6), 'Rel-6');
|
||||
assert.strictEqual(catReleaseLabel(18), 'Rel-18');
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
"""CAT/USAT item introduction releases (Stage 1 of the release guard).
|
||||
|
||||
Which proactive command, event, data object and PROVIDE LOCAL INFORMATION
|
||||
qualifier appeared in which release, curated from the change histories of the
|
||||
three pinned specifications:
|
||||
|
||||
* 3GPP TS 31.111 (USAT) V18.12.0, Annex W;
|
||||
* ETSI TS 102 223 (CAT) V18.3.0, Annex V;
|
||||
* ETSI TS 101 220 (TLVs/TARs) V18.3.0, Annex Q.
|
||||
|
||||
The version major equals the release (V7.x = Rel-7 ... V18.x = Rel-18). The
|
||||
human-readable reference with the per-row sources lives in the workspace at
|
||||
``docs/uicc/ETSI-3GPP-Releases.md``. Release 4 is the specification baseline
|
||||
(an item the histories never name); an item absent from this table is one the
|
||||
histories do not pin down - never filter on it.
|
||||
|
||||
The PWA mirrors this table (``CAT_RELEASE`` in frontend/index.html); the drift
|
||||
guard is tests/test_catrelease.py. Releases below 6 display as "pre-Rel-6"
|
||||
(``release_label``).
|
||||
"""
|
||||
|
||||
# Keyed by: events - the event code (TS 102 223 8.25 / TS 31.111 7.5);
|
||||
# commands - the proactive command name; objects - 'tag name' as in the
|
||||
# reference document; pli - the PROVIDE LOCAL INFORMATION qualifier byte.
|
||||
CAT_RELEASE = {
|
||||
'events': {
|
||||
0x00: 4, 0x01: 4, 0x02: 4, 0x03: 4, 0x04: 4, 0x05: 4, 0x06: 4, 0x07: 4,
|
||||
0x08: 4, 0x09: 4, 0x0A: 4, 0x0B: 4, 0x0C: 4, 0x0D: 4, 0x0E: 4, 0x0F: 4,
|
||||
0x10: 6, 0x11: 7, 0x12: 8, 0x13: 9, 0x14: 12, 0x15: 9, 0x16: 9, 0x17: 10,
|
||||
0x18: 10, 0x19: 10, 0x1B: 11, 0x1C: 12, 0x1D: 14, 0x1E: 17, 0x1F: 16,
|
||||
},
|
||||
'commands': {
|
||||
'REFRESH': 4, 'MORE TIME': 4,
|
||||
'POLL INTERVAL': 4, 'POLLING OFF': 4,
|
||||
'SET UP EVENT LIST': 4, 'SET UP CALL': 4,
|
||||
'SEND SS': 4, 'SEND USSD': 4,
|
||||
'SEND SHORT MESSAGE': 4, 'SEND DTMF': 4,
|
||||
'LAUNCH BROWSER': 4, 'PLAY TONE': 4,
|
||||
'DISPLAY TEXT': 4, 'GET INKEY': 4,
|
||||
'GET INPUT': 4, 'SELECT ITEM': 4,
|
||||
'SET UP MENU': 4, 'PROVIDE LOCAL INFORMATION': 4,
|
||||
'TIMER MANAGEMENT': 4, 'SET UP IDLE MODE TEXT': 4,
|
||||
'RUN AT COMMAND': 4, 'LANGUAGE NOTIFICATION': 4,
|
||||
'OPEN CHANNEL': 4, 'CLOSE CHANNEL': 4,
|
||||
'SEND DATA': 4, 'RECEIVE DATA': 4,
|
||||
'GET CHANNEL STATUS': 4, 'SERVICE SEARCH': 4,
|
||||
'GET SERVICE INFORMATION': 4, 'DECLARE SERVICE': 4,
|
||||
'SET FRAMES': 6, 'GET FRAMES STATUS': 6,
|
||||
'RETRIEVE MULTIMEDIA MESSAGE': 7, 'SUBMIT MULTIMEDIA MESSAGE': 7,
|
||||
'DISPLAY MULTIMEDIA MESSAGE': 7, 'GEOGRAPHICAL LOCATION REQUEST': 8,
|
||||
'ACTIVATE': 13, 'CONTACTLESS STATE CHANGED': 9,
|
||||
'COMMAND CONTAINER': 10, 'ENCAPSULATED SESSION CONTROL': 11,
|
||||
'LSI COMMAND': 17,
|
||||
},
|
||||
'objects': {
|
||||
'06/86 Address': 4,
|
||||
'08/88 Subaddress': 4,
|
||||
'1C/9C Transaction identifier': 4,
|
||||
'1A/9A Cause': 4,
|
||||
'34/B4 Browser termination cause': 4,
|
||||
'35/B5 Bearer description': 4,
|
||||
'37/B7 Channel data length': 4,
|
||||
'38/B8 Channel status': 4,
|
||||
'3E/BE Other address (local / destination)': 4,
|
||||
'40/C0 Display parameters': 4,
|
||||
'64/E4 Browsing status': 4,
|
||||
'65/E5 Network search mode': 4,
|
||||
'67/E7 Frames Information': 6,
|
||||
'4B/CB (I-)WLAN Access Status': 7,
|
||||
'55/D5 CSG cell selection status': 9,
|
||||
'56/D6 CSG ID': 9,
|
||||
'57/D7 HNB name': 9,
|
||||
'31/B1 IMS URI': 10,
|
||||
'77/F7 IARI': 10,
|
||||
'77/F7 IMPU list': 11,
|
||||
'78/F8 IMS status code': 11,
|
||||
'7E/FE Media Type': 12,
|
||||
'09/89 PLMN ID': 4,
|
||||
'3F/BF Access technology': 4,
|
||||
'B4 Supported Radio Access Technologies': 13,
|
||||
'9D Data connection status': 14,
|
||||
'AA Data connection type': 14,
|
||||
'AE (E/5G)SM cause': 14,
|
||||
'C7 Network Access Name': 14,
|
||||
'0B/8B PDP/PDN/PDU type': 14,
|
||||
'26/A6 Date-Time and Time zone': 4,
|
||||
'13/93 Location Information': 4,
|
||||
'55/D5 CAG cell selection status': 17,
|
||||
'56/D6 CAG information list': 17,
|
||||
'57/D7 CAG Human-readable network name list': 17,
|
||||
'55/D5 Slices status': 16,
|
||||
'56/D6 Slices information (served)': 16,
|
||||
'78/F8 Allowed slices information': 16,
|
||||
'77/F7 Allowed slices with S-NSSAI mapping': 18,
|
||||
'D7 Rejected slices with S-NSSAI mapping': 18,
|
||||
'B1 Rejected slices information': 18,
|
||||
},
|
||||
'pli': {
|
||||
'00': 4, '01': 4, '02': 4, '03': 4, '04': 4, '06': 4, '07': 4, '08': 6,
|
||||
'09': 4, '0A': 4, '0D': 8, '0E': 12, '0F': 10, '10': 10, '11': 9, '12': 10,
|
||||
'13': 10, '14': 13, '15': 16, '16': 17, '17': 18, '1A': 13,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def release_label(rel):
|
||||
"""The display label of an introduction release.
|
||||
|
||||
Everything below Rel-6 collapses to "pre-Rel-6" (the UI convention);
|
||||
0/None yields '' (unknown - the item is never filtered).
|
||||
"""
|
||||
if not rel:
|
||||
return ''
|
||||
return 'pre-Rel-6' if rel < 6 else 'Rel-%d' % rel
|
||||
|
||||
|
||||
def item_release(kind, key, default=None):
|
||||
"""The introduction release of an item, or `default` when unknown."""
|
||||
return CAT_RELEASE.get(kind, {}).get(key, default)
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user