v3.23.5: the TERMINAL RESPONSE result list and its spec corrections
The step editor's result select (the expect's respond, the proactive-drain's respond/first) offered five symbolic values with no hex. It now offers the full TS 102 223 v18.3.0 8.12.0 list (00-3F), labelled '<hex> - <alias>' (or '<hex> - <spec name>'), plus a manual hex entry, and an optional additional-information byte for the general result (83 02 <result> <info>, default 00; the 8.12.2 terminal-problem values are suggested through a datalist). The script aliases were corrected to the spec: refused 03->22, not_understood 04->32, modified 06->07, no_response 22->12 - a stored script using one of them now answers with the corrected value; no_response stays accepted for compatibility but is no longer offered by the editor. The server's TR decode tables (_RESULT_NAMES_BASIC/_GENERAL, a GSM 11.14 mix displayed in the proactive log) are replaced by the spec table plus the 8.12.2 additional-information names; the dcs field parses as hex (0A used to be refused and 10 read as decimal 10); the drain summary's 22 label is fixed. The result labels are not translated (spec text); the unused RU keys were removed. Shared fixture frontend/tests/tr_results.json, drift-guarded by both suites. 855 frontend + 838 Python; sw cache simple-v418.
This commit is contained in:
+10
-2
@@ -774,9 +774,17 @@ TPDU: `da` digits with whitespace skipped and a leading `+` ignored,
|
|||||||
only) or `files` (the File List of a
|
only) or `files` (the File List of a
|
||||||
REFRESH, TS 102 223 8.18; `files` is a list of path hex strings, matched
|
REFRESH, TS 102 223 8.18; `files` is a list of path hex strings, matched
|
||||||
order-insensitively); `respond` is the TERMINAL RESPONSE in the TS 102 223
|
order-insensitively); `respond` is the TERMINAL RESPONSE in the TS 102 223
|
||||||
6.8.0 object order (`result` name or value; `text`+`dcs` for GET INKEY/GET
|
6.8.0 object order (`result` - a script alias or a two-digit hex value from
|
||||||
|
the full TS 102 223 8.12.0 list; `additional_info` - the general result's
|
||||||
|
additional-information hex byte, sent as `83 02 <result> <additional_info>`,
|
||||||
|
default `00`; `text`+`dcs` for GET INKEY/GET
|
||||||
INPUT, `dcs` `00` packed / `04` 8-bit / `08` UCS2; `item_id`; extra `raw`
|
INPUT, `dcs` `00` packed / `04` 8-bit / `08` UCS2; `item_id`; extra `raw`
|
||||||
TLVs appended after the standard objects). The terminal's own objects ride
|
TLVs appended after the standard objects). The aliases (`ok`=00,
|
||||||
|
`partial`=01, `missing`=02, `modified`=07, `cancel`=10, `back`=11,
|
||||||
|
`timeout`=12, `refused`=22, `not_understood`=32, plus the compatibility-only
|
||||||
|
`no_response`=12) follow the spec; v3.23.5 corrected `refused`,
|
||||||
|
`not_understood`, `modified` and `no_response` (they used to carry legacy
|
||||||
|
values). The terminal's own objects ride
|
||||||
along unless `raw` overrides them: a successful PROVIDE LOCAL INFORMATION
|
along unless `raw` overrides them: a successful PROVIDE LOCAL INFORMATION
|
||||||
answer carries the TR Config PLI dictionary entry (or the host clock for the
|
answer carries the TR Config PLI dictionary entry (or the host clock for the
|
||||||
date/time qualifier) and a POLL INTERVAL answer the current-interval echo.
|
date/time qualifier) and a POLL INTERVAL answer the current-interval echo.
|
||||||
|
|||||||
+18
-2
@@ -466,8 +466,24 @@ Optional - an omitted (or empty) object answers `ok` (`0x00`). The TERMINAL
|
|||||||
RESPONSE follows the TS 102 223 §6.8.0 object order (Command details, Device
|
RESPONSE follows the TS 102 223 §6.8.0 object order (Command details, Device
|
||||||
identities, **Result**, Duration, Text string, Item identifier, …):
|
identities, **Result**, Duration, Text string, Item identifier, …):
|
||||||
|
|
||||||
- `result` - name or hex (`ok`, `partial`, `missing`, `refused`,
|
- `result` - a script alias or the two-digit hex value. The editor offers
|
||||||
`not_understood`, `modified`, `cancel`, `back`, `timeout`, `no_response`).
|
the **full TS 102 223 §8.12.0 list** (`00`–`3F`), labelled
|
||||||
|
`<hex> - <alias>` where an alias exists, else `<hex> - <name>`, plus a
|
||||||
|
**manual** hex entry for any other value. The aliases:
|
||||||
|
`ok`=00, `partial`=01, `missing`=02, `modified`=07, `cancel`=10,
|
||||||
|
`back`=11, `timeout`=12, `refused`=22, `not_understood`=32 (and
|
||||||
|
`no_response`=12, kept for compatibility only).
|
||||||
|
**v3.23.5 corrected** `refused` (was `03`), `not_understood` (was `04`),
|
||||||
|
`modified` (was `06`) and `no_response` (was `22`) to their spec meanings -
|
||||||
|
a stored script using one of them now answers with the corrected value.
|
||||||
|
- `additional_info` - the general result's additional-information byte (a hex
|
||||||
|
byte, default `00`): the Result is sent in the general form
|
||||||
|
`83 02 <result> <additional_info>`. TS 102 223 §8.12.2 defines the values
|
||||||
|
for the terminal problem (`20`): `00` no specific cause, `01` screen busy,
|
||||||
|
`02` busy on call, `04` no service, `05` access control class bar,
|
||||||
|
`06` radio resource not granted, `07` not in speech call, `09` busy on
|
||||||
|
SEND DTMF, `0A` no NAA active; the network problem (`21`) is NAA-specific
|
||||||
|
(`00` = no specific cause). The editor suggests the §8.12.2 list.
|
||||||
- `text` + `dcs` - the Text string for a GET INKEY / GET INPUT answer. `dcs`
|
- `text` + `dcs` - the Text string for a GET INKEY / GET INPUT answer. `dcs`
|
||||||
uses the spec's recommended codings: `"00"` GSM default alphabet 7 bits
|
uses the spec's recommended codings: `"00"` GSM default alphabet 7 bits
|
||||||
**packed**, `"04"` GSM default alphabet 8 bits (one octet per character),
|
**packed**, `"04"` GSM default alphabet 8 bits (one octet per character),
|
||||||
|
|||||||
@@ -565,7 +565,7 @@
|
|||||||
<p class="text-sm mb-2">У вкладки <strong>четыре представления</strong>: <strong>список наборов</strong> (по умолчанию; <em>Добавить набор</em> / <em>Редактировать</em> / <em>Запустить</em> / <em>Экспорт</em> / <em>Удалить</em>), <strong>редактор набора</strong> (скрипты набора в порядке выполнения — необязательный <em>setup</em> первым и <em>teardown</em> последним, с пометками ролей; у каждого скрипта <em>Запустить</em>/<em>Редактировать</em>/<em>Переместить</em>/<em>Скопировать</em>/<em>Удалить</em>, политика <em>при ошибке</em> <em>остановить</em> (по умолчанию) / <em>продолжить</em> и <em>Требуется ADM</em>), <strong>редактор скрипта</strong> (список шагов; кнопка <em>Старт</em> выполняет только этот скрипт и показывает результат под шагами — он не сохраняется) и <strong>отчёт набора</strong> (после запуска набора: статус каждого скрипта, таблица шагов с ожидаемым/фактическим, примечание об ошибке и выдержка из журнала запуска; сводка с успешно/предупреждения/ошибки/пропущено, время выполнения, счётчики SCP80 до → после по каждому keyset, пресет и предупреждение о смене сессии карты; экспорт в <em>JSON</em> или <em>Markdown</em>). <strong>Запуск набора</strong> выполняет его скрипты по порядку в <strong>одной сессии карты, без сбросов и без продолжения с места остановки</strong>; <em>при ошибке</em> решает, остановится ли набор на этом скрипте или запустится следующий, <strong>teardown выполняется всегда</strong> при остановке или ошибке (пока карта в ридере), чтобы следующий тест начался с чистого состояния, а сброс карты останавливает набор и отмечается в отчёте. <strong>Начать со сброса карты</strong> сбрасывает карту и выполняет полную переинициализацию перед запуском набора (скрипты затем работают со свежеинициализированной картой); пресет должен по-прежнему совпадать с ICCID карты, а требуемый ADM проверяется заново для новой сессии карты. Инициализация поглощает начальные proactive-команды карты, поэтому скрипт не поймает первую команду после сброса (они остаются в журнале proactive). <strong>Требуется ADM</strong> (у набора или скрипта) проверяет ключ ADM из подобранного пресета <strong>один раз</strong> перед запуском — ошибка отклоняет запуск (проверьте вручную через значок ADM; автоматических повторов нет). Скрипты хранятся в хранилище наборов (<code class="font-mono text-sm">~/.pysim-simple-server/test_suites.json</code>, <code class="font-mono text-sm">--test-suites</code>); каждый скрипт принадлежит ровно одному набору, и скрипт можно переместить или скопировать в другой набор (экспорт набора — это пакет с набором и его скриптами; старый экспорт только скриптов импортируется в автоматически созданный набор «Imported scripts»).</p>
|
<p class="text-sm mb-2">У вкладки <strong>четыре представления</strong>: <strong>список наборов</strong> (по умолчанию; <em>Добавить набор</em> / <em>Редактировать</em> / <em>Запустить</em> / <em>Экспорт</em> / <em>Удалить</em>), <strong>редактор набора</strong> (скрипты набора в порядке выполнения — необязательный <em>setup</em> первым и <em>teardown</em> последним, с пометками ролей; у каждого скрипта <em>Запустить</em>/<em>Редактировать</em>/<em>Переместить</em>/<em>Скопировать</em>/<em>Удалить</em>, политика <em>при ошибке</em> <em>остановить</em> (по умолчанию) / <em>продолжить</em> и <em>Требуется ADM</em>), <strong>редактор скрипта</strong> (список шагов; кнопка <em>Старт</em> выполняет только этот скрипт и показывает результат под шагами — он не сохраняется) и <strong>отчёт набора</strong> (после запуска набора: статус каждого скрипта, таблица шагов с ожидаемым/фактическим, примечание об ошибке и выдержка из журнала запуска; сводка с успешно/предупреждения/ошибки/пропущено, время выполнения, счётчики SCP80 до → после по каждому keyset, пресет и предупреждение о смене сессии карты; экспорт в <em>JSON</em> или <em>Markdown</em>). <strong>Запуск набора</strong> выполняет его скрипты по порядку в <strong>одной сессии карты, без сбросов и без продолжения с места остановки</strong>; <em>при ошибке</em> решает, остановится ли набор на этом скрипте или запустится следующий, <strong>teardown выполняется всегда</strong> при остановке или ошибке (пока карта в ридере), чтобы следующий тест начался с чистого состояния, а сброс карты останавливает набор и отмечается в отчёте. <strong>Начать со сброса карты</strong> сбрасывает карту и выполняет полную переинициализацию перед запуском набора (скрипты затем работают со свежеинициализированной картой); пресет должен по-прежнему совпадать с ICCID карты, а требуемый ADM проверяется заново для новой сессии карты. Инициализация поглощает начальные proactive-команды карты, поэтому скрипт не поймает первую команду после сброса (они остаются в журнале proactive). <strong>Требуется ADM</strong> (у набора или скрипта) проверяет ключ ADM из подобранного пресета <strong>один раз</strong> перед запуском — ошибка отклоняет запуск (проверьте вручную через значок ADM; автоматических повторов нет). Скрипты хранятся в хранилище наборов (<code class="font-mono text-sm">~/.pysim-simple-server/test_suites.json</code>, <code class="font-mono text-sm">--test-suites</code>); каждый скрипт принадлежит ровно одному набору, и скрипт можно переместить или скопировать в другой набор (экспорт набора — это пакет с набором и его скриптами; старый экспорт только скриптов импортируется в автоматически созданный набор «Imported scripts»).</p>
|
||||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||||
<li><strong>Действия</strong> — <em>Envelope</em> (Event Download: сырой тип события + данные, с опциональным переопределением источника — 82 терминал / 83 сеть) или <em>Event</em> (семантические формы: <strong>та же форма, что и в диалоге отправки на вкладке «Телефон»</strong>, для всех смоделированных событий — четыре события, которые собирает сервер (Location status, Access technology change, Network rejection, Data connection status change), передаются полями, которые он проверяет и собирает во время запуска, остальные несут собранный формой hex данных события (показывается под формой, с проверкой одного ENVELOPE); источник device identities берётся из события (например, MT call → сеть) или из его поля источника), <em>Menu Selection</em> (item id 1–255 или текст пункта с точным/частичным совпадением, по умолчанию без учёта регистра — выбор <em>«Учёт регистра»</em> в редакторе шагов включает точное совпадение; текст подбирается по меню карты, которое следует за параметрами установки апплета), <em>UPDATE/READ файла</em> (путь + режим; чтение проверяет данные файла), <em>сырой APDU</em> (без авто-обработчика), <em>SCP80</em> (C-APDU или готовый защищённый пакет, опциональные переопределения номера keyset (kvn) / TAR / SPI1 / SPI2 и формат команды — компактный или обёрнутый в шаблон Command Scripting TS 102 226 §5.2.1 <em>expanded</em> (AA) / <em>expanded AE 80</em>; ключи и счётчик берутся из пресета карты, подобранного по ICCID подключённой карты — запуск называет пресет в хранилище, поэтому всегда используются его keysets, TAR и счётчик — а сервер сохраняет потреблённый счётчик в пресет по ходу запуска), <em>STATUS</em> (при <em>попытках</em> > 1 опрашивает, пока карта не объявит команду) и <em>Очистка очереди проактивных команд</em> (опрос STATUS и поглощение всех объявленных команд — результат TERMINAL RESPONSE для каждой, опционально другой результат для первой; граница попыток/интервала; опциональная проверка <em>require</em> по поглощённому. Пустой опрос проходит, финальный STATUS должен быть idle, а оставшаяся команда передаётся следующему шагу — используйте в начале, середине или конце скрипта, чтобы убедиться, что карта перестала слать команды; она же поглощает команду, оставшуюся от предыдущего шага (ответившего <code class="font-mono text-sm">91XX</code>)). У каждого действия есть проверка ответа: ожидаемые SW и данные (точно или маска с подстановкой <code class="font-mono text-sm">?</code> — <code class="font-mono text-sm">91??</code>) и проверка PoR для SCP80 (<em>любой / без PoR / por_ok</em> или своя проверка декодированного PoR — статус / SW / данные; транспорт через SEND SHORT MESSAGE работает так же).</li>
|
<li><strong>Действия</strong> — <em>Envelope</em> (Event Download: сырой тип события + данные, с опциональным переопределением источника — 82 терминал / 83 сеть) или <em>Event</em> (семантические формы: <strong>та же форма, что и в диалоге отправки на вкладке «Телефон»</strong>, для всех смоделированных событий — четыре события, которые собирает сервер (Location status, Access technology change, Network rejection, Data connection status change), передаются полями, которые он проверяет и собирает во время запуска, остальные несут собранный формой hex данных события (показывается под формой, с проверкой одного ENVELOPE); источник device identities берётся из события (например, MT call → сеть) или из его поля источника), <em>Menu Selection</em> (item id 1–255 или текст пункта с точным/частичным совпадением, по умолчанию без учёта регистра — выбор <em>«Учёт регистра»</em> в редакторе шагов включает точное совпадение; текст подбирается по меню карты, которое следует за параметрами установки апплета), <em>UPDATE/READ файла</em> (путь + режим; чтение проверяет данные файла), <em>сырой APDU</em> (без авто-обработчика), <em>SCP80</em> (C-APDU или готовый защищённый пакет, опциональные переопределения номера keyset (kvn) / TAR / SPI1 / SPI2 и формат команды — компактный или обёрнутый в шаблон Command Scripting TS 102 226 §5.2.1 <em>expanded</em> (AA) / <em>expanded AE 80</em>; ключи и счётчик берутся из пресета карты, подобранного по ICCID подключённой карты — запуск называет пресет в хранилище, поэтому всегда используются его keysets, TAR и счётчик — а сервер сохраняет потреблённый счётчик в пресет по ходу запуска), <em>STATUS</em> (при <em>попытках</em> > 1 опрашивает, пока карта не объявит команду) и <em>Очистка очереди проактивных команд</em> (опрос STATUS и поглощение всех объявленных команд — результат TERMINAL RESPONSE для каждой, опционально другой результат для первой; граница попыток/интервала; опциональная проверка <em>require</em> по поглощённому. Пустой опрос проходит, финальный STATUS должен быть idle, а оставшаяся команда передаётся следующему шагу — используйте в начале, середине или конце скрипта, чтобы убедиться, что карта перестала слать команды; она же поглощает команду, оставшуюся от предыдущего шага (ответившего <code class="font-mono text-sm">91XX</code>)). У каждого действия есть проверка ответа: ожидаемые SW и данные (точно или маска с подстановкой <code class="font-mono text-sm">?</code> — <code class="font-mono text-sm">91??</code>) и проверка PoR для SCP80 (<em>любой / без PoR / por_ok</em> или своя проверка декодированного PoR — статус / SW / данные; транспорт через SEND SHORT MESSAGE работает так же).</li>
|
||||||
<li><strong>Ожидания</strong> — карта должна объявить проактивную команду на предыдущем шаге (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); <code class="font-mono text-sm">9000</code> означает «команды нет»: шаг падает на своём уровне ошибок — Ошибка останавливает скрипт, Предупреждение продолжает (терпимая форма «поглотить команду, если она есть») — а действие STATUS с <em>попытками</em> > 1 опрашивает, когда апплет выдаёт команду при опросе. Проверяются тип команды, квалификатор и содержимое: текст / пункт / raw, проверка <em>Alpha</em> (идентификатор Alpha команды, тег <code class="font-mono text-sm">05</code>/<code class="font-mono text-sm">85</code> — отличается от текстовой строки <code class="font-mono text-sm">8D</code>), проверка <em>SMS TPDU</em> (submit-TPDU команды SEND SHORT MESSAGE: получатель / PID / DCS / UDL / данные — для собственного SMS апплета), проверка <em>PoR</em> (SEND SHORT MESSAGE несёт PoR, когда пакет запросил PoR-in-submit, бит SPI2 <code class="font-mono text-sm">20</code>: статус ответа, SW R-APDU и данные ответа; перед этим должен выполниться шаг SCP80 — SW R-APDU находится <em>внутри</em> полезной нагрузки ответа: собственный TAR апплета возвращает данные приложения, поэтому проверяйте данные, а SW оставляйте пустым; отклонённый ENVELOPE (<code class="font-mono text-sm">62XX</code>/<code class="font-mono text-sm">63XX</code>) может вовсе не нести пакет ответа — для такого шага оставляйте проверку PoR «любой» или ожидайте «без PoR») и проверка <em>Files</em> (File List команды REFRESH, например квалификатор <code class="font-mono text-sm">00</code> = NAA initialization with full file change notification). <strong>TERMINAL RESPONSE</strong> (результат, идентификатор пункта, текст, доп. TLV) является частью инструкции.</li>
|
<li><strong>Ожидания</strong> — карта должна объявить проактивную команду на предыдущем шаге (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); <code class="font-mono text-sm">9000</code> означает «команды нет»: шаг падает на своём уровне ошибок — Ошибка останавливает скрипт, Предупреждение продолжает (терпимая форма «поглотить команду, если она есть») — а действие STATUS с <em>попытками</em> > 1 опрашивает, когда апплет выдаёт команду при опросе. Проверяются тип команды, квалификатор и содержимое: текст / пункт / raw, проверка <em>Alpha</em> (идентификатор Alpha команды, тег <code class="font-mono text-sm">05</code>/<code class="font-mono text-sm">85</code> — отличается от текстовой строки <code class="font-mono text-sm">8D</code>), проверка <em>SMS TPDU</em> (submit-TPDU команды SEND SHORT MESSAGE: получатель / PID / DCS / UDL / данные — для собственного SMS апплета), проверка <em>PoR</em> (SEND SHORT MESSAGE несёт PoR, когда пакет запросил PoR-in-submit, бит SPI2 <code class="font-mono text-sm">20</code>: статус ответа, SW R-APDU и данные ответа; перед этим должен выполниться шаг SCP80 — SW R-APDU находится <em>внутри</em> полезной нагрузки ответа: собственный TAR апплета возвращает данные приложения, поэтому проверяйте данные, а SW оставляйте пустым; отклонённый ENVELOPE (<code class="font-mono text-sm">62XX</code>/<code class="font-mono text-sm">63XX</code>) может вовсе не нести пакет ответа — для такого шага оставляйте проверку PoR «любой» или ожидайте «без PoR») и проверка <em>Files</em> (File List команды REFRESH, например квалификатор <code class="font-mono text-sm">00</code> = NAA initialization with full file change notification). <strong>TERMINAL RESPONSE</strong> является частью инструкции: поле <strong>результата</strong> предлагает полный список TS 102 223 §8.12.0 (каждая строка подписана <code class="font-mono text-sm"><hex> - <алиас или имя></code>, плюс ручной ввод hex; алиасы соответствуют спецификации — <code class="font-mono text-sm">ok</code> 00, <code class="font-mono text-sm">modified</code> 07, <code class="font-mono text-sm">cancel</code> 10, <code class="font-mono text-sm">timeout</code> 12, <code class="font-mono text-sm">refused</code> 22, <code class="font-mono text-sm">not_understood</code> 32, …), поле <strong>additional information</strong> (дополнительная информация общего результата, по умолчанию <code class="font-mono text-sm">00</code>; предлагаются значения §8.12.2 для проблем терминала), идентификатор пункта, текст (+DCS) и дополнительные TLV.</li>
|
||||||
<li><strong>Уровни ошибок</strong> — несовпадение помечает шаг как <em>Ошибка</em> (скрипт останавливается) или <em>Предупреждение</em> (продолжает); уровень применяется к любой ошибке шага, включая ожидание без объявленной команды и ошибку карты, возникшую на шаге (например, <code class="font-mono text-sm">6A82</code> отсутствующего необязательного файла); каждая проверка показывается с ожидаемым и фактическим значением.</li>
|
<li><strong>Уровни ошибок</strong> — несовпадение помечает шаг как <em>Ошибка</em> (скрипт останавливается) или <em>Предупреждение</em> (продолжает); уровень применяется к любой ошибке шага, включая ожидание без объявленной команды и ошибку карты, возникшую на шаге (например, <code class="font-mono text-sm">6A82</code> отсутствующего необязательного файла); каждая проверка показывается с ожидаемым и фактическим значением.</li>
|
||||||
<li><strong>Шаблоны</strong> — Empty, <em>Обзор меню STK</em>, <em>Апплет через SCP80</em>; редактор шагов — формный (действие Event показывает форму события из симулятора), а набор экспортируется/импортируется пакетом JSON (набор и его скрипты).</li>
|
<li><strong>Шаблоны</strong> — Empty, <em>Обзор меню STK</em>, <em>Апплет через SCP80</em>; редактор шагов — формный (действие Event показывает форму события из симулятора), а набор экспортируется/импортируется пакетом JSON (набор и его скрипты).</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
+1
-1
@@ -564,7 +564,7 @@
|
|||||||
<p class="text-sm mb-2">The pill has <strong>four views</strong>: the <strong>suite list</strong> (default; <em>Add suite</em> / <em>Edit</em> / <em>Run</em> / <em>Export</em> / <em>Delete</em>), the <strong>suite editor</strong> (the suite's scripts in run order — the optional <em>setup</em> first and the <em>teardown</em> last, marked with role badges; per script <em>Run</em>/<em>Edit</em>/<em>Move</em>/<em>Copy</em>/<em>Delete</em>, the per-member <em>on failure</em> policy <em>stop</em> (default) / <em>continue</em>, and <em>ADM required</em>), the <strong>script editor</strong> (the step list; its <em>Start</em> runs that single script and shows the result under the steps — not preserved) and the <strong>suite report</strong> (after a suite run: every script's status, its step table with expected/actual, the failing note and a run-log excerpt; the summary with passed/warnings/failed/skipped, wall time, the SCP80 counters before → after per keyset, the preset and a card-session-changed warning; export as <em>JSON</em> or <em>Markdown</em>). A <strong>suite run</strong> executes its scripts in order in <strong>one card session, without resets and without resume</strong>; <em>on failure</em> decides whether the suite stops at that script or runs the next one, the <strong>teardown always runs</strong> on a stop or a failure (while the card is in the reader) so the next test starts clean, and a card reset stops the suite and is flagged in the report. <strong>Start with card reset</strong> resets the card and runs the full re-initialization before the suite starts (the scripts then run against a freshly initialized card); the preset must still match the card's ICCID afterwards and a required ADM is verified again for the new card session. The initialization consumes the card's initial proactive commands, so a script cannot catch the first command after the reset (they stay in the proactive log). <strong>ADM required</strong> (on the suite or a script) verifies the matched preset's ADM key <strong>once</strong> before the start — a failure refuses the run (verify manually with the ADM badge; nothing is retried automatically). Scripts live in the suite's store (<code class="font-mono text-sm">~/.pysim-simple-server/test_suites.json</code>, <code class="font-mono text-sm">--test-suites</code>); every script belongs to exactly one suite, and a script can be moved or copied to another suite (a bundle export carries the suite plus its scripts; the old script-only export imports into an auto-created “Imported scripts” suite).</p>
|
<p class="text-sm mb-2">The pill has <strong>four views</strong>: the <strong>suite list</strong> (default; <em>Add suite</em> / <em>Edit</em> / <em>Run</em> / <em>Export</em> / <em>Delete</em>), the <strong>suite editor</strong> (the suite's scripts in run order — the optional <em>setup</em> first and the <em>teardown</em> last, marked with role badges; per script <em>Run</em>/<em>Edit</em>/<em>Move</em>/<em>Copy</em>/<em>Delete</em>, the per-member <em>on failure</em> policy <em>stop</em> (default) / <em>continue</em>, and <em>ADM required</em>), the <strong>script editor</strong> (the step list; its <em>Start</em> runs that single script and shows the result under the steps — not preserved) and the <strong>suite report</strong> (after a suite run: every script's status, its step table with expected/actual, the failing note and a run-log excerpt; the summary with passed/warnings/failed/skipped, wall time, the SCP80 counters before → after per keyset, the preset and a card-session-changed warning; export as <em>JSON</em> or <em>Markdown</em>). A <strong>suite run</strong> executes its scripts in order in <strong>one card session, without resets and without resume</strong>; <em>on failure</em> decides whether the suite stops at that script or runs the next one, the <strong>teardown always runs</strong> on a stop or a failure (while the card is in the reader) so the next test starts clean, and a card reset stops the suite and is flagged in the report. <strong>Start with card reset</strong> resets the card and runs the full re-initialization before the suite starts (the scripts then run against a freshly initialized card); the preset must still match the card's ICCID afterwards and a required ADM is verified again for the new card session. The initialization consumes the card's initial proactive commands, so a script cannot catch the first command after the reset (they stay in the proactive log). <strong>ADM required</strong> (on the suite or a script) verifies the matched preset's ADM key <strong>once</strong> before the start — a failure refuses the run (verify manually with the ADM badge; nothing is retried automatically). Scripts live in the suite's store (<code class="font-mono text-sm">~/.pysim-simple-server/test_suites.json</code>, <code class="font-mono text-sm">--test-suites</code>); every script belongs to exactly one suite, and a script can be moved or copied to another suite (a bundle export carries the suite plus its scripts; the old script-only export imports into an auto-created “Imported scripts” suite).</p>
|
||||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||||
<li><strong>Actions</strong> — <em>Envelope</em> (Event Download: raw event type + data, with an optional source override — 82 terminal / 83 network) or <em>Event</em> (the semantic forms: the <strong>same form the Phone tab's send dialog uses</strong>, for every modelled event — the four events the server builds (Location status, Access technology change, Network rejection, Data connection status change) travel as fields it validates and builds at run time, every other event carries the form's built event-data hex (shown live under the form, with the one-ENVELOPE check); the device-identities source comes from the event (e.g. MT call → network) or its source field), <em>Menu Selection</em> (item id 1–255, or the item's text with an exact/contains match, case-insensitive by default — the step editor's <em>Match case</em> choice switches to an exact-case match; the text is resolved against the card's menu, which follows the applet's install parameters), <em>UPDATE/READ file</em> (path + mode; reads verify the file data), <em>raw APDU</em> (no auto-handler), <em>SCP80</em> (C-APDU or a pre-built secured packet, optional keyset number (kvn) / TAR / SPI1 / SPI2 overrides and a command format — compact, or the TS 102 226 §5.2.1 Command Scripting template <em>expanded</em> (AA) / <em>expanded AE 80</em> around the C-APDU; the keys and the counter come from the card preset matched by the equipped card's ICCID — the run names the stored preset, so it always uses its keysets, TARs and counter — the server persists the consumed counter into it as the run proceeds), <em>STATUS</em> (with <em>attempts</em> > 1 it polls until the card announces a command) and <em>Proactive drain</em> (poll STATUS and consume every announced command — the TERMINAL RESPONSE result for each, optionally a different result for the first; the attempt/interval bound; an optional <em>require</em> assertion over what was drained. An empty poll passes, the final STATUS must be idle, and a still-pending command is handed to the next step — use it at the start, in the middle or at the end of a script to make sure the card has stopped sending; it also consumes a command a previous step left pending (a step that answered <code class="font-mono text-sm">91XX</code>). Every action has a response check: expected SW and data (exact, or a mask with <code class="font-mono text-sm">?</code> nibble wildcards — <code class="font-mono text-sm">91??</code>) plus a PoR check for SCP80 (<em>any / no PoR / por_ok</em>, or a custom assertion of the decoded PoR — status / SW / data; the SEND SHORT MESSAGE transport works the same).</li>
|
<li><strong>Actions</strong> — <em>Envelope</em> (Event Download: raw event type + data, with an optional source override — 82 terminal / 83 network) or <em>Event</em> (the semantic forms: the <strong>same form the Phone tab's send dialog uses</strong>, for every modelled event — the four events the server builds (Location status, Access technology change, Network rejection, Data connection status change) travel as fields it validates and builds at run time, every other event carries the form's built event-data hex (shown live under the form, with the one-ENVELOPE check); the device-identities source comes from the event (e.g. MT call → network) or its source field), <em>Menu Selection</em> (item id 1–255, or the item's text with an exact/contains match, case-insensitive by default — the step editor's <em>Match case</em> choice switches to an exact-case match; the text is resolved against the card's menu, which follows the applet's install parameters), <em>UPDATE/READ file</em> (path + mode; reads verify the file data), <em>raw APDU</em> (no auto-handler), <em>SCP80</em> (C-APDU or a pre-built secured packet, optional keyset number (kvn) / TAR / SPI1 / SPI2 overrides and a command format — compact, or the TS 102 226 §5.2.1 Command Scripting template <em>expanded</em> (AA) / <em>expanded AE 80</em> around the C-APDU; the keys and the counter come from the card preset matched by the equipped card's ICCID — the run names the stored preset, so it always uses its keysets, TARs and counter — the server persists the consumed counter into it as the run proceeds), <em>STATUS</em> (with <em>attempts</em> > 1 it polls until the card announces a command) and <em>Proactive drain</em> (poll STATUS and consume every announced command — the TERMINAL RESPONSE result for each, optionally a different result for the first; the attempt/interval bound; an optional <em>require</em> assertion over what was drained. An empty poll passes, the final STATUS must be idle, and a still-pending command is handed to the next step — use it at the start, in the middle or at the end of a script to make sure the card has stopped sending; it also consumes a command a previous step left pending (a step that answered <code class="font-mono text-sm">91XX</code>). Every action has a response check: expected SW and data (exact, or a mask with <code class="font-mono text-sm">?</code> nibble wildcards — <code class="font-mono text-sm">91??</code>) plus a PoR check for SCP80 (<em>any / no PoR / por_ok</em>, or a custom assertion of the decoded PoR — status / SW / data; the SEND SHORT MESSAGE transport works the same).</li>
|
||||||
<li><strong>Expectations</strong> — the card must have announced a proactive command in the previous step (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); a <code class="font-mono text-sm">9000</code> means “no command”: the step fails at its fail level — Error stops the script, Warning continues (the tolerant “consume a command if there is one” form) — and a STATUS action with <em>attempts</em> > 1 polls when the applet delivers on poll. The command type, qualifier and contents are verified: text / item / raw checks, an <em>Alpha</em> check (the command's Alpha identifier, tag <code class="font-mono text-sm">05</code>/<code class="font-mono text-sm">85</code> — distinct from the <code class="font-mono text-sm">8D</code> text string), an <em>SMS TPDU</em> check (the submit TPDU of a SEND SHORT MESSAGE: destination / PID / DCS / UDL / user data — for an applet's own SMS), a <em>PoR</em> check (a SEND SHORT MESSAGE carries the PoR when the packet requested PoR-in-submit, SPI2 bit <code class="font-mono text-sm">20</code>: the response status, the R-APDU SW and the response data; make sure an SCP80 step ran first — the R-APDU SW lives <em>inside</em> the response payload: an applet's own TAR returns application data, so check its data and leave the SW empty; a refused ENVELOPE (<code class="font-mono text-sm">62XX</code>/<code class="font-mono text-sm">63XX</code>) may carry no response packet at all — leave the PoR check at <em>any</em> or expect <em>no PoR</em> for such a step) and a <em>Files</em> check (the File List of a REFRESH, e.g. qualifier <code class="font-mono text-sm">00</code> = NAA initialization with full file change notification). The scripted <strong>TERMINAL RESPONSE</strong> (result, item identifier, text, extra TLVs) is part of the instruction.</li>
|
<li><strong>Expectations</strong> — the card must have announced a proactive command in the previous step (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); a <code class="font-mono text-sm">9000</code> means “no command”: the step fails at its fail level — Error stops the script, Warning continues (the tolerant “consume a command if there is one” form) — and a STATUS action with <em>attempts</em> > 1 polls when the applet delivers on poll. The command type, qualifier and contents are verified: text / item / raw checks, an <em>Alpha</em> check (the command's Alpha identifier, tag <code class="font-mono text-sm">05</code>/<code class="font-mono text-sm">85</code> — distinct from the <code class="font-mono text-sm">8D</code> text string), an <em>SMS TPDU</em> check (the submit TPDU of a SEND SHORT MESSAGE: destination / PID / DCS / UDL / user data — for an applet's own SMS), a <em>PoR</em> check (a SEND SHORT MESSAGE carries the PoR when the packet requested PoR-in-submit, SPI2 bit <code class="font-mono text-sm">20</code>: the response status, the R-APDU SW and the response data; make sure an SCP80 step ran first — the R-APDU SW lives <em>inside</em> the response payload: an applet's own TAR returns application data, so check its data and leave the SW empty; a refused ENVELOPE (<code class="font-mono text-sm">62XX</code>/<code class="font-mono text-sm">63XX</code>) may carry no response packet at all — leave the PoR check at <em>any</em> or expect <em>no PoR</em> for such a step) and a <em>Files</em> check (the File List of a REFRESH, e.g. qualifier <code class="font-mono text-sm">00</code> = NAA initialization with full file change notification). The scripted <strong>TERMINAL RESPONSE</strong> is part of the instruction: the <strong>result</strong> offers the full TS 102 223 §8.12.0 list (each entry labelled <code class="font-mono text-sm"><hex> - <alias or name></code>, plus a manual hex entry; the aliases follow the spec — <code class="font-mono text-sm">ok</code> 00, <code class="font-mono text-sm">modified</code> 07, <code class="font-mono text-sm">cancel</code> 10, <code class="font-mono text-sm">timeout</code> 12, <code class="font-mono text-sm">refused</code> 22, <code class="font-mono text-sm">not_understood</code> 32, …), the <strong>additional information</strong> byte of the general result (default <code class="font-mono text-sm">00</code>; the §8.12.2 terminal-problem values are suggested), the item identifier, the text (+DCS) and extra TLVs.</li>
|
||||||
<li><strong>Fail levels</strong> — a mismatch flags the step as <em>Error</em> (terminates the script) or <em>Warning</em> (continues); every step failure honours it, including an expectation that finds no pending command and a card error raised during the step (an absent optional file's <code class="font-mono text-sm">6A82</code>); each check is reported with expected vs actual values.</li>
|
<li><strong>Fail levels</strong> — a mismatch flags the step as <em>Error</em> (terminates the script) or <em>Warning</em> (continues); every step failure honours it, including an expectation that finds no pending command and a card error raised during the step (an absent optional file's <code class="font-mono text-sm">6A82</code>); each check is reported with expected vs actual values.</li>
|
||||||
<li><strong>Templates</strong> — Empty, <em>STK menu browsing</em>, <em>Applet via SCP80</em>; the step editor is form-based (the event action renders the Simulator's event form), and a suite exports/imports as a JSON bundle (the suite plus its scripts).</li>
|
<li><strong>Templates</strong> — Empty, <em>STK menu browsing</em>, <em>Applet via SCP80</em>; the step editor is form-based (the event action renders the Simulator's event form), and a suite exports/imports as a JSON bundle (the suite plus its scripts).</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
+172
-21
@@ -1905,7 +1905,7 @@
|
|||||||
// ===== Version =====
|
// ===== Version =====
|
||||||
// Single source of truth for the PWA version: shown in the header and used
|
// Single source of truth for the PWA version: shown in the header and used
|
||||||
// by the server version check in pysimConnect().
|
// by the server version check in pysimConnect().
|
||||||
const SIMPLE_VERSION = '3.23.4';
|
const SIMPLE_VERSION = '3.23.5';
|
||||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||||
|
|
||||||
// ===== view history (the browser Back/Forward buttons) =====
|
// ===== view history (the browser Back/Forward buttons) =====
|
||||||
@@ -7960,6 +7960,146 @@ function testDeleteStep(i) {
|
|||||||
testRenderSteps();
|
testRenderSteps();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TERMINAL RESPONSE general results (TS 102 223 v18.3.0 8.12.0) and the
|
||||||
|
// terminal-problem additional information (8.12.2): the step editor offers
|
||||||
|
// the full list labelled 'hex - alias' (where a script alias exists) or
|
||||||
|
// 'hex - name', plus a manual hex entry. The values are the spec's; the
|
||||||
|
// aliases are the authoring convenience. frontend/tests/tr_results.json is
|
||||||
|
// the fixture both suites drift-guard against.
|
||||||
|
const TR_RESULTS = [
|
||||||
|
{v: '00', alias: 'ok', name: 'Command performed successfully'},
|
||||||
|
{v: '01', alias: 'partial', name: 'Command performed with partial comprehension'},
|
||||||
|
{v: '02', alias: 'missing', name: 'Command performed, with missing information'},
|
||||||
|
{v: '03', name: 'REFRESH performed with additional EFs read'},
|
||||||
|
{v: '04', name: 'Command performed successfully, but requested icon could not be displayed'},
|
||||||
|
{v: '05', name: 'Command performed, but modified by call control by NAA'},
|
||||||
|
{v: '06', name: 'Command performed successfully, limited service'},
|
||||||
|
{v: '07', alias: 'modified', name: 'Command performed with modification'},
|
||||||
|
{v: '08', name: 'REFRESH performed but indicated NAA was not active'},
|
||||||
|
{v: '09', name: 'Command performed successfully, tone not played'},
|
||||||
|
{v: '10', alias: 'cancel', name: 'Proactive UICC session terminated by the user'},
|
||||||
|
{v: '11', alias: 'back', name: 'Backward move in the proactive UICC session requested by the user'},
|
||||||
|
{v: '12', alias: 'timeout', name: 'No response from user'},
|
||||||
|
{v: '13', name: 'Help information required by the user'},
|
||||||
|
{v: '14', name: 'Reserved for GSM/3G'},
|
||||||
|
{v: '15', name: 'Reserved for 3GPP (for future usage)'},
|
||||||
|
{v: '16', name: 'Reserved for 3GPP (for future usage)'},
|
||||||
|
{v: '20', name: 'Terminal currently unable to process command'},
|
||||||
|
{v: '21', name: 'Network currently unable to process command'},
|
||||||
|
{v: '22', alias: 'refused', name: 'User did not accept the proactive command'},
|
||||||
|
{v: '23', name: 'User cleared down call before connection or network release'},
|
||||||
|
{v: '24', name: 'Action in contradiction with the current timer state'},
|
||||||
|
{v: '25', name: 'Interaction with call control by NAA, temporary problem'},
|
||||||
|
{v: '26', name: 'Launch browser generic error'},
|
||||||
|
{v: '27', name: 'MMS temporary problem'},
|
||||||
|
{v: '28', name: 'Reserved for 3GPP (BIP temporary error, T timer in the additional information)'},
|
||||||
|
{v: '29', name: 'Reserved for 3GPP (for future usage)'},
|
||||||
|
{v: '30', name: 'Command beyond terminal\'s capabilities'},
|
||||||
|
{v: '31', name: 'Command type not understood by terminal'},
|
||||||
|
{v: '32', alias: 'not_understood', name: 'Command data not understood by terminal'},
|
||||||
|
{v: '33', name: 'Command number not known by terminal'},
|
||||||
|
{v: '34', name: 'Reserved for GSM/3G'},
|
||||||
|
{v: '35', name: 'Reserved for GSM/3G'},
|
||||||
|
{v: '36', name: 'Error, required values are missing'},
|
||||||
|
{v: '37', name: 'Reserved for GSM/3G'},
|
||||||
|
{v: '38', name: 'MultipleCard commands error'},
|
||||||
|
{v: '39', name: 'Interaction with call control by NAA, permanent problem'},
|
||||||
|
{v: '3A', name: 'Bearer Independent Protocol permanent error'},
|
||||||
|
{v: '3B', name: 'Access Technology unable to process command'},
|
||||||
|
{v: '3C', name: 'Frames error'},
|
||||||
|
{v: '3D', name: 'MMS Error'},
|
||||||
|
{v: '3E', name: 'Reserved for 3GPP (for future usage)'},
|
||||||
|
{v: '3F', name: 'Reserved for 3GPP (for future usage)'},
|
||||||
|
];
|
||||||
|
|
||||||
|
const TR_RESULT_ADD_INFO = [
|
||||||
|
{v: '00', name: 'No specific cause can be given'},
|
||||||
|
{v: '01', name: 'Screen is busy'},
|
||||||
|
{v: '02', name: 'Terminal currently busy on call'},
|
||||||
|
{v: '03', name: 'Reserved for GSM/3G'},
|
||||||
|
{v: '04', name: 'No service'},
|
||||||
|
{v: '05', name: 'Access control class bar'},
|
||||||
|
{v: '06', name: 'Radio resource not granted'},
|
||||||
|
{v: '07', name: 'Not in speech call'},
|
||||||
|
{v: '08', name: 'Reserved for GSM/3G'},
|
||||||
|
{v: '09', name: 'Terminal currently busy on SEND DTMF command'},
|
||||||
|
{v: '0A', name: 'No NAA active'},
|
||||||
|
];
|
||||||
|
|
||||||
|
// The script authoring aliases (the script may also use the hex value).
|
||||||
|
// `no_response` is kept for backward compatibility (12) but is not offered
|
||||||
|
// by the editor - 12's canonical alias is `timeout`.
|
||||||
|
const TR_RESULT_ALIASES = {
|
||||||
|
ok: '00', partial: '01', missing: '02', refused: '22', not_understood: '32',
|
||||||
|
modified: '07', cancel: '10', back: '11', timeout: '12', no_response: '12',
|
||||||
|
};
|
||||||
|
|
||||||
|
// The result select's options: 'hex - alias' where an alias exists, else
|
||||||
|
// 'hex - spec name', plus a manual hex entry.
|
||||||
|
function trResultOptions() {
|
||||||
|
const opts = TR_RESULTS.map(r => ({v: r.alias || r.v, l: r.v + ' - ' + (r.alias || r.name)}));
|
||||||
|
opts.push({v: 'manual', l: t('manual input')});
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A stored result -> the select's value and the manual hex ('' unless the
|
||||||
|
// value is not in the list). Accepts the aliases, the hex and the 0x form.
|
||||||
|
function trResultFormValue(stored) {
|
||||||
|
const raw = String(stored === undefined || stored === null ? '' : stored).trim();
|
||||||
|
if (raw === '') return {value: '', manual: ''};
|
||||||
|
if (TR_RESULT_ALIASES[raw.toLowerCase()]) return {value: raw.toLowerCase(), manual: ''};
|
||||||
|
const hex = raw.replace(/^0x/i, '').toUpperCase();
|
||||||
|
if (/^[0-9A-F]{2}$/.test(hex)) {
|
||||||
|
const hit = TR_RESULTS.find(r => r.v === hex);
|
||||||
|
return hit ? {value: hit.alias || hit.v, manual: ''} : {value: 'manual', manual: hex};
|
||||||
|
}
|
||||||
|
return {value: 'ok', manual: ''};
|
||||||
|
}
|
||||||
|
|
||||||
|
// A result value is valid when it is an alias or a hex byte.
|
||||||
|
function trResultValid(v) {
|
||||||
|
const s = String(v === undefined || v === null ? '' : v).trim().toLowerCase();
|
||||||
|
return !!TR_RESULT_ALIASES[s] || /^(0x)?[0-9a-f]{2}$/.test(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The collected result value of a result select: the selected option's
|
||||||
|
// alias/hex, or the manual hex entry ('' when empty/invalid -
|
||||||
|
// testStepFormError reports it). `fallback` is the empty-select default
|
||||||
|
// (the drain's optional `first` keeps the empty value).
|
||||||
|
function trResultCollect(selId, hexId, fallback) {
|
||||||
|
const v = ((document.getElementById(selId) || {}).value || '');
|
||||||
|
if (v !== 'manual') return v || fallback || '';
|
||||||
|
return ((document.getElementById(hexId) || {}).value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide a result select's manual hex entry.
|
||||||
|
function testTrResultChanged(sel, wrapId) {
|
||||||
|
const el = document.getElementById(wrapId);
|
||||||
|
if (el) el.classList.toggle('hidden', sel.value !== 'manual');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The result row's HTML: the select, its manual hex entry and the optional
|
||||||
|
// additional-information field (a hex byte; the terminal-problem codings are
|
||||||
|
// suggested through the datalist).
|
||||||
|
function trResultRowHtml(selId, stored, addInfo, opts) {
|
||||||
|
const v = trResultFormValue(stored);
|
||||||
|
const wrapId = selId + '-hexwrap';
|
||||||
|
const options = (opts && opts.empty ? [{v: '', l: t(opts.empty)}] : []).concat(trResultOptions());
|
||||||
|
let html = '<div class="flex gap-2">'
|
||||||
|
+ testFormSelect(selId, v.value, options, "testTrResultChanged(this, '" + wrapId + "')")
|
||||||
|
+ '<div id="' + wrapId + '" class="w-24' + (v.value === 'manual' ? '' : ' hidden') + '">'
|
||||||
|
+ testFormInput(selId + '-hex', v.manual, '00', 'maxlength="2"') + '</div></div>';
|
||||||
|
const add = addInfo === undefined || addInfo === null || addInfo === '' ? ''
|
||||||
|
: (typeof addInfo === 'number' ? addInfo.toString(16).toUpperCase().padStart(2, '0') : String(addInfo).toUpperCase());
|
||||||
|
html += '<div class="mt-1 flex gap-2 items-center"><label class="text-xs text-gray-500 dark:text-slate-400 shrink-0">'
|
||||||
|
+ esc(t('Additional information (optional hex byte)')) + '</label>'
|
||||||
|
+ '<div class="w-24">' + testFormInput(selId + '-addinfo', add, '00', 'list="tr-addinfo-list" maxlength="2"') + '</div></div>';
|
||||||
|
html += '<datalist id="tr-addinfo-list">'
|
||||||
|
+ TR_RESULT_ADD_INFO.map(a => '<option value="' + esc(a.v) + '">' + esc(a.name) + '</option>').join('')
|
||||||
|
+ '</datalist>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
function testFormRow(label, inner) {
|
function testFormRow(label, inner) {
|
||||||
return '<div><label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300">' + esc(t(label)) + '</label>' + inner + '</div>';
|
return '<div><label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300">' + esc(t(label)) + '</label>' + inner + '</div>';
|
||||||
}
|
}
|
||||||
@@ -8073,15 +8213,11 @@ function testStepRender() {
|
|||||||
} else if (step.kind === 'proactive-drain') {
|
} else if (step.kind === 'proactive-drain') {
|
||||||
const pr = p.respond || {};
|
const pr = p.respond || {};
|
||||||
html += testFormRow('TERMINAL RESPONSE result (each command)',
|
html += testFormRow('TERMINAL RESPONSE result (each command)',
|
||||||
testFormSelect('test-f-presult', pr.result === undefined ? 'ok' : String(pr.result), [
|
trResultRowHtml('test-f-presult', pr.result === undefined ? 'ok' : pr.result, pr.additional_info));
|
||||||
{v: 'ok', l: t('ok')}, {v: 'cancel', l: t('cancel')}, {v: 'back', l: t('back')},
|
|
||||||
{v: 'timeout', l: t('timeout')}, {v: 'no_response', l: t('no response')}]));
|
|
||||||
const pf = p.first || {};
|
const pf = p.first || {};
|
||||||
html += testFormRow('First command only (optional)',
|
html += testFormRow('First command only (optional)',
|
||||||
testFormSelect('test-f-pfirst', pf.result === undefined ? '' : String(pf.result), [
|
trResultRowHtml('test-f-pfirst', pf.result === undefined ? '' : pf.result, pf.additional_info,
|
||||||
{v: '', l: t('same as above')}, {v: 'ok', l: t('ok')}, {v: 'cancel', l: t('cancel')},
|
{empty: 'same as above'}));
|
||||||
{v: 'back', l: t('back')}, {v: 'timeout', l: t('timeout')},
|
|
||||||
{v: 'no_response', l: t('no response')}]));
|
|
||||||
html += testFormRow('Attempts (STATUS rounds)', testFormInput('test-f-pattempts', p.attempts === undefined ? 3 : p.attempts, '1..1000'));
|
html += testFormRow('Attempts (STATUS rounds)', testFormInput('test-f-pattempts', p.attempts === undefined ? 3 : p.attempts, '1..1000'));
|
||||||
html += testFormRow('Interval (ms, between rounds)', testFormInput('test-f-pinterval', p.interval_ms === undefined ? 200 : p.interval_ms, '0..10000'));
|
html += testFormRow('Interval (ms, between rounds)', testFormInput('test-f-pinterval', p.interval_ms === undefined ? 200 : p.interval_ms, '0..10000'));
|
||||||
const rq = p.require || {};
|
const rq = p.require || {};
|
||||||
@@ -8124,9 +8260,8 @@ function testStepRender() {
|
|||||||
+ '<button onclick="testAddCheck()" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600">' + esc(t('Add check')) + '</button></div>'
|
+ '<button onclick="testAddCheck()" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600">' + esc(t('Add check')) + '</button></div>'
|
||||||
+ '<div id="test-checks"></div></div>';
|
+ '<div id="test-checks"></div></div>';
|
||||||
const r = step.respond || {};
|
const r = step.respond || {};
|
||||||
html += testFormRow('TERMINAL RESPONSE result', testFormSelect('test-f-result', r.result === undefined ? 'ok' : String(r.result), [
|
html += testFormRow('TERMINAL RESPONSE result',
|
||||||
{v: 'ok', l: t('ok')}, {v: 'cancel', l: t('cancel')}, {v: 'back', l: t('back')},
|
trResultRowHtml('test-f-result', r.result === undefined ? 'ok' : r.result, r.additional_info));
|
||||||
{v: 'timeout', l: t('timeout')}, {v: 'no_response', l: t('no response')}]));
|
|
||||||
html += testFormRow('Item identifier (SELECT ITEM / SET UP MENU)', testFormInput('test-f-ritem', r.item_id, '1..255'));
|
html += testFormRow('Item identifier (SELECT ITEM / SET UP MENU)', testFormInput('test-f-ritem', r.item_id, '1..255'));
|
||||||
html += testFormRow('Text (GET INKEY / GET INPUT)', '<div class="flex gap-2">'
|
html += testFormRow('Text (GET INKEY / GET INPUT)', '<div class="flex gap-2">'
|
||||||
+ testFormInput('test-f-rtext', r.text, 'text')
|
+ testFormInput('test-f-rtext', r.text, 'text')
|
||||||
@@ -8406,9 +8541,13 @@ function testStepCollect() {
|
|||||||
}
|
}
|
||||||
else if (kind === 'status') { p.attempts = num('test-f-attempts', 1); p.interval_ms = num('test-f-interval', 200); }
|
else if (kind === 'status') { p.attempts = num('test-f-attempts', 1); p.interval_ms = num('test-f-interval', 200); }
|
||||||
else if (kind === 'proactive-drain') {
|
else if (kind === 'proactive-drain') {
|
||||||
p.respond = {result: val('test-f-presult') || 'ok'};
|
p.respond = {result: trResultCollect('test-f-presult', 'test-f-presult-hex', 'ok')};
|
||||||
const first = val('test-f-pfirst');
|
const padd = hexNoMask('test-f-presult-addinfo');
|
||||||
|
if (padd) p.respond.additional_info = padd;
|
||||||
|
const first = trResultCollect('test-f-pfirst', 'test-f-pfirst-hex', '');
|
||||||
if (first) p.first = {result: first};
|
if (first) p.first = {result: first};
|
||||||
|
const fadd = hexNoMask('test-f-pfirst-addinfo');
|
||||||
|
if (fadd && p.first) p.first.additional_info = fadd;
|
||||||
p.attempts = num('test-f-pattempts', 3);
|
p.attempts = num('test-f-pattempts', 3);
|
||||||
p.interval_ms = num('test-f-pinterval', 200);
|
p.interval_ms = num('test-f-pinterval', 200);
|
||||||
const requireCmd = val('test-f-prequire');
|
const requireCmd = val('test-f-prequire');
|
||||||
@@ -8457,7 +8596,9 @@ function testStepCollect() {
|
|||||||
if (c.kind === 'files') return !!(c.files && c.files.length);
|
if (c.kind === 'files') return !!(c.files && c.files.length);
|
||||||
return !!(c.value || '').trim();
|
return !!(c.value || '').trim();
|
||||||
});
|
});
|
||||||
const r = {result: val('test-f-result') || 'ok'};
|
const r = {result: trResultCollect('test-f-result', 'test-f-result-hex', 'ok')};
|
||||||
|
const radd = hexNoMask('test-f-result-addinfo');
|
||||||
|
if (radd) r.additional_info = radd;
|
||||||
const ri = num('test-f-ritem', 0);
|
const ri = num('test-f-ritem', 0);
|
||||||
if (ri) r.item_id = ri;
|
if (ri) r.item_id = ri;
|
||||||
const text = val('test-f-rtext');
|
const text = val('test-f-rtext');
|
||||||
@@ -8502,9 +8643,19 @@ function testStepFormError() {
|
|||||||
}
|
}
|
||||||
if ((step.kind === 'file-write' || step.kind === 'file-read') && !p.path) return t('file path is empty');
|
if ((step.kind === 'file-write' || step.kind === 'file-read') && !p.path) return t('file path is empty');
|
||||||
if (step.kind === 'status' && !(p.attempts >= 1)) return t('attempts must be at least 1');
|
if (step.kind === 'status' && !(p.attempts >= 1)) return t('attempts must be at least 1');
|
||||||
if (step.kind === 'proactive-drain' && !(p.attempts >= 1)) return t('attempts must be at least 1');
|
if (step.kind === 'proactive-drain') {
|
||||||
|
if (!(p.attempts >= 1)) return t('attempts must be at least 1');
|
||||||
|
if (!trResultValid((p.respond || {}).result)) return t('TERMINAL RESPONSE result must be two hex digits');
|
||||||
|
const fsel = val('test-f-pfirst');
|
||||||
|
if (fsel === 'manual' && !/^[0-9A-F]{2}$/.test(hexNoMask('test-f-pfirst-hex'))) {
|
||||||
|
return t('TERMINAL RESPONSE result must be two hex digits');
|
||||||
|
}
|
||||||
|
if (fsel && fsel !== 'manual' && !trResultValid(fsel)) return t('TERMINAL RESPONSE result must be two hex digits');
|
||||||
|
}
|
||||||
} else if (!step.command) {
|
} else if (!step.command) {
|
||||||
return t('command is required');
|
return t('command is required');
|
||||||
|
} else if (!trResultValid((step.respond || {}).result)) {
|
||||||
|
return t('TERMINAL RESPONSE result must be two hex digits');
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -16699,7 +16850,9 @@ async function pysimProactiveLogRender() {
|
|||||||
if (e.tr_hex !== undefined) {
|
if (e.tr_hex !== undefined) {
|
||||||
if (e.tr_result !== undefined) {
|
if (e.tr_result !== undefined) {
|
||||||
html += '<div class="flex gap-1"><span class="text-gray-500 dark:text-slate-400 shrink-0">' + t('Result') + ':</span>'
|
html += '<div class="flex gap-1"><span class="text-gray-500 dark:text-slate-400 shrink-0">' + t('Result') + ':</span>'
|
||||||
+ '<span class="break-all">' + (e.tr_result_name ? esc(e.tr_result_name) + ' (' + esc(e.tr_result) + ')' : esc(e.tr_result)) + '</span></div>';
|
+ '<span class="break-all">' + (e.tr_result_name
|
||||||
|
? esc(e.tr_result_name) + (e.tr_result_info_name ? ' \u00b7 ' + esc(e.tr_result_info_name) : '') + ' (' + esc(e.tr_result) + ')'
|
||||||
|
: esc(e.tr_result)) + '</span></div>';
|
||||||
}
|
}
|
||||||
if (e.tr_hex) {
|
if (e.tr_hex) {
|
||||||
html += pysimLogFields(e.tr_decoded);
|
html += pysimLogFields(e.tr_decoded);
|
||||||
@@ -22872,11 +23025,9 @@ const LANG_RU = {
|
|||||||
'Text': 'Текст',
|
'Text': 'Текст',
|
||||||
'id': 'id',
|
'id': 'id',
|
||||||
'text': 'текст',
|
'text': 'текст',
|
||||||
'ok': 'ок',
|
'manual input': 'ручной ввод',
|
||||||
'cancel': 'отмена',
|
'Additional information (optional hex byte)': 'Дополнительная информация (необязательный hex-байт)',
|
||||||
'back': 'назад',
|
'TERMINAL RESPONSE result must be two hex digits': 'Результат TERMINAL RESPONSE — ровно два hex-символа',
|
||||||
'timeout': 'тайм-аут',
|
|
||||||
'no response': 'нет ответа',
|
|
||||||
'PoR = por_ok': 'PoR = por_ok',
|
'PoR = por_ok': 'PoR = por_ok',
|
||||||
'event data is required': 'нужны данные события',
|
'event data is required': 'нужны данные события',
|
||||||
'event data must be hex': 'данные события должны быть в hex',
|
'event data must be hex': 'данные события должны быть в hex',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v417';
|
const CACHE = 'simple-v418';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -57,6 +57,16 @@ globalThis.testSuites = () => _testSuites || [];
|
|||||||
eval(extractFunc(html, 'testFormRow'));
|
eval(extractFunc(html, 'testFormRow'));
|
||||||
eval(extractFunc(html, 'testFormInput'));
|
eval(extractFunc(html, 'testFormInput'));
|
||||||
eval(extractFunc(html, 'testFormSelect'));
|
eval(extractFunc(html, 'testFormSelect'));
|
||||||
|
// the TERMINAL RESPONSE result tables + the shared result-row helpers
|
||||||
|
eval(extractBlock('const TR_RESULTS = [', 'const TR_RESULT_ADD_INFO = [').replace(/^const /gm, 'var '));
|
||||||
|
eval(extractBlock('const TR_RESULT_ADD_INFO = [', 'const TR_RESULT_ALIASES = {').replace(/^const /gm, 'var '));
|
||||||
|
eval(extractBlock('const TR_RESULT_ALIASES = {', 'function trResultOptions').replace(/^const /gm, 'var '));
|
||||||
|
eval(extractFunc(html, 'trResultOptions'));
|
||||||
|
eval(extractFunc(html, 'trResultFormValue'));
|
||||||
|
eval(extractFunc(html, 'trResultValid'));
|
||||||
|
eval(extractFunc(html, 'trResultCollect'));
|
||||||
|
eval(extractFunc(html, 'testTrResultChanged'));
|
||||||
|
eval(extractFunc(html, 'trResultRowHtml'));
|
||||||
eval(extractFunc(html, 'testStepRender'));
|
eval(extractFunc(html, 'testStepRender'));
|
||||||
eval(extractFunc(html, 'testRenderChecks'));
|
eval(extractFunc(html, 'testRenderChecks'));
|
||||||
eval(extractFunc(html, 'testStepCollect'));
|
eval(extractFunc(html, 'testStepCollect'));
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"comment": "TERMINAL RESPONSE general results (TS 102 223 v18.3.0 8.12.0) and the terminal-problem additional information (8.12.2). Mirrored by pysim_simple_server/testscript.py (TR_RESULTS/TR_RESULT_ADD_INFO/RESULT_NAMES) and frontend/index.html (the same names); both suites drift-guard against this fixture.",
|
||||||
|
"results": [
|
||||||
|
{"v": "00", "name": "Command performed successfully", "alias": "ok"},
|
||||||
|
{"v": "01", "name": "Command performed with partial comprehension", "alias": "partial"},
|
||||||
|
{"v": "02", "name": "Command performed, with missing information", "alias": "missing"},
|
||||||
|
{"v": "03", "name": "REFRESH performed with additional EFs read"},
|
||||||
|
{"v": "04", "name": "Command performed successfully, but requested icon could not be displayed"},
|
||||||
|
{"v": "05", "name": "Command performed, but modified by call control by NAA"},
|
||||||
|
{"v": "06", "name": "Command performed successfully, limited service"},
|
||||||
|
{"v": "07", "name": "Command performed with modification", "alias": "modified"},
|
||||||
|
{"v": "08", "name": "REFRESH performed but indicated NAA was not active"},
|
||||||
|
{"v": "09", "name": "Command performed successfully, tone not played"},
|
||||||
|
{"v": "10", "name": "Proactive UICC session terminated by the user", "alias": "cancel"},
|
||||||
|
{"v": "11", "name": "Backward move in the proactive UICC session requested by the user", "alias": "back"},
|
||||||
|
{"v": "12", "name": "No response from user", "alias": "timeout"},
|
||||||
|
{"v": "13", "name": "Help information required by the user"},
|
||||||
|
{"v": "14", "name": "Reserved for GSM/3G"},
|
||||||
|
{"v": "15", "name": "Reserved for 3GPP (for future usage)"},
|
||||||
|
{"v": "16", "name": "Reserved for 3GPP (for future usage)"},
|
||||||
|
{"v": "20", "name": "Terminal currently unable to process command"},
|
||||||
|
{"v": "21", "name": "Network currently unable to process command"},
|
||||||
|
{"v": "22", "name": "User did not accept the proactive command", "alias": "refused"},
|
||||||
|
{"v": "23", "name": "User cleared down call before connection or network release"},
|
||||||
|
{"v": "24", "name": "Action in contradiction with the current timer state"},
|
||||||
|
{"v": "25", "name": "Interaction with call control by NAA, temporary problem"},
|
||||||
|
{"v": "26", "name": "Launch browser generic error"},
|
||||||
|
{"v": "27", "name": "MMS temporary problem"},
|
||||||
|
{"v": "28", "name": "Reserved for 3GPP (BIP temporary error, T timer in the additional information)"},
|
||||||
|
{"v": "29", "name": "Reserved for 3GPP (for future usage)"},
|
||||||
|
{"v": "30", "name": "Command beyond terminal's capabilities"},
|
||||||
|
{"v": "31", "name": "Command type not understood by terminal"},
|
||||||
|
{"v": "32", "name": "Command data not understood by terminal", "alias": "not_understood"},
|
||||||
|
{"v": "33", "name": "Command number not known by terminal"},
|
||||||
|
{"v": "34", "name": "Reserved for GSM/3G"},
|
||||||
|
{"v": "35", "name": "Reserved for GSM/3G"},
|
||||||
|
{"v": "36", "name": "Error, required values are missing"},
|
||||||
|
{"v": "37", "name": "Reserved for GSM/3G"},
|
||||||
|
{"v": "38", "name": "MultipleCard commands error"},
|
||||||
|
{"v": "39", "name": "Interaction with call control by NAA, permanent problem"},
|
||||||
|
{"v": "3A", "name": "Bearer Independent Protocol permanent error"},
|
||||||
|
{"v": "3B", "name": "Access Technology unable to process command"},
|
||||||
|
{"v": "3C", "name": "Frames error"},
|
||||||
|
{"v": "3D", "name": "MMS Error"},
|
||||||
|
{"v": "3E", "name": "Reserved for 3GPP (for future usage)"},
|
||||||
|
{"v": "3F", "name": "Reserved for 3GPP (for future usage)"}
|
||||||
|
],
|
||||||
|
"aliases": {
|
||||||
|
"ok": "00",
|
||||||
|
"partial": "01",
|
||||||
|
"missing": "02",
|
||||||
|
"refused": "22",
|
||||||
|
"not_understood": "32",
|
||||||
|
"modified": "07",
|
||||||
|
"cancel": "10",
|
||||||
|
"back": "11",
|
||||||
|
"timeout": "12",
|
||||||
|
"no_response": "12"
|
||||||
|
},
|
||||||
|
"additional_info": [
|
||||||
|
{"v": "00", "name": "No specific cause can be given"},
|
||||||
|
{"v": "01", "name": "Screen is busy"},
|
||||||
|
{"v": "02", "name": "Terminal currently busy on call"},
|
||||||
|
{"v": "03", "name": "Reserved for GSM/3G"},
|
||||||
|
{"v": "04", "name": "No service"},
|
||||||
|
{"v": "05", "name": "Access control class bar"},
|
||||||
|
{"v": "06", "name": "Radio resource not granted"},
|
||||||
|
{"v": "07", "name": "Not in speech call"},
|
||||||
|
{"v": "08", "name": "Reserved for GSM/3G"},
|
||||||
|
{"v": "09", "name": "Terminal currently busy on SEND DTMF command"},
|
||||||
|
{"v": "0A", "name": "No NAA active"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
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');
|
||||||
|
const fixture = JSON.parse(fs.readFileSync(path.join(__dirname, 'tr_results.json'), '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 tables leak out of sloppy-mode eval.
|
||||||
|
eval(extractBlock('const TR_RESULTS = [', 'const TR_RESULT_ADD_INFO = [').replace(/^const /gm, 'var '));
|
||||||
|
eval(extractBlock('const TR_RESULT_ADD_INFO = [', 'const TR_RESULT_ALIASES = {').replace(/^const /gm, 'var '));
|
||||||
|
eval(extractBlock('const TR_RESULT_ALIASES = {', 'function trResultOptions').replace(/^const /gm, 'var '));
|
||||||
|
eval(extractFunc(html, 'esc'));
|
||||||
|
eval(extractFunc(html, 'trResultOptions'));
|
||||||
|
eval(extractFunc(html, 'trResultFormValue'));
|
||||||
|
eval(extractFunc(html, 'trResultValid'));
|
||||||
|
eval(extractFunc(html, 'trResultCollect'));
|
||||||
|
eval(extractFunc(html, 'testTrResultChanged'));
|
||||||
|
eval(extractFunc(html, 'testFormInput'));
|
||||||
|
eval(extractFunc(html, 'testFormSelect'));
|
||||||
|
eval(extractFunc(html, 'trResultRowHtml'));
|
||||||
|
|
||||||
|
globalThis.t = s => s;
|
||||||
|
|
||||||
|
const fixtureResults = fixture.results.map(r => {
|
||||||
|
const o = { v: r.v, name: r.name };
|
||||||
|
if (r.alias) o.alias = r.alias;
|
||||||
|
return o;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the PWA result tables match the shared fixture', () => {
|
||||||
|
assert.deepStrictEqual(TR_RESULTS, fixtureResults);
|
||||||
|
assert.deepStrictEqual(TR_RESULT_ADD_INFO, fixture.additional_info);
|
||||||
|
assert.deepStrictEqual(TR_RESULT_ALIASES, fixture.aliases);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the corrected aliases carry the TS 102 223 8.12.0 values', () => {
|
||||||
|
assert.strictEqual(TR_RESULT_ALIASES.refused, '22');
|
||||||
|
assert.strictEqual(TR_RESULT_ALIASES.not_understood, '32');
|
||||||
|
assert.strictEqual(TR_RESULT_ALIASES.modified, '07');
|
||||||
|
assert.strictEqual(TR_RESULT_ALIASES.no_response, '12');
|
||||||
|
assert.strictEqual(TR_RESULT_ALIASES.ok, '00');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('trResultOptions labels every entry with its hex value', () => {
|
||||||
|
const opts = trResultOptions();
|
||||||
|
// the full list plus the manual entry
|
||||||
|
assert.strictEqual(opts.length, TR_RESULTS.length + 1);
|
||||||
|
assert.deepStrictEqual(opts[0], { v: 'ok', l: '00 - ok' });
|
||||||
|
const byValue = {};
|
||||||
|
opts.forEach(o => { byValue[o.v] = o.l; });
|
||||||
|
assert.strictEqual(byValue.modified, '07 - modified');
|
||||||
|
assert.strictEqual(byValue['09'], '09 - Command performed successfully, tone not played');
|
||||||
|
assert.strictEqual(byValue['20'], '20 - Terminal currently unable to process command');
|
||||||
|
assert.strictEqual(byValue['3F'], '3F - Reserved for 3GPP (for future usage)');
|
||||||
|
assert.deepStrictEqual(opts[opts.length - 1], { v: 'manual', l: 'manual input' });
|
||||||
|
// the compat-only alias is not offered (12's canonical alias is timeout)
|
||||||
|
assert.ok(!('no_response' in byValue), 'no_response must not be an option');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('trResultFormValue maps the stored alias/hex/manual values', () => {
|
||||||
|
assert.deepStrictEqual(trResultFormValue('ok'), { value: 'ok', manual: '' });
|
||||||
|
assert.deepStrictEqual(trResultFormValue('OK'), { value: 'ok', manual: '' });
|
||||||
|
assert.deepStrictEqual(trResultFormValue('07'), { value: 'modified', manual: '' });
|
||||||
|
assert.deepStrictEqual(trResultFormValue('0x09'), { value: '09', manual: '' });
|
||||||
|
assert.deepStrictEqual(trResultFormValue('1a'), { value: 'manual', manual: '1A' });
|
||||||
|
assert.deepStrictEqual(trResultFormValue(''), { value: '', manual: '' });
|
||||||
|
assert.deepStrictEqual(trResultFormValue(undefined), { value: '', manual: '' });
|
||||||
|
assert.deepStrictEqual(trResultFormValue('bogus'), { value: 'ok', manual: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('trResultValid accepts the aliases and hex bytes only', () => {
|
||||||
|
['ok', 'refused', '07', '0x0a', 'FF'].forEach(v => assert.ok(trResultValid(v), v));
|
||||||
|
['manual', '', 'f', 'zzz'].forEach(v => assert.ok(!trResultValid(v), v));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the step editor offers the full list for all three result fields', () => {
|
||||||
|
const form = extractFunc(html, 'testStepRender');
|
||||||
|
assert.ok(form.includes("trResultRowHtml('test-f-result'"), 'the expectation result');
|
||||||
|
assert.ok(form.includes("trResultRowHtml('test-f-presult'"), 'the drain result');
|
||||||
|
assert.ok(form.includes("trResultRowHtml('test-f-pfirst'"), 'the drain first');
|
||||||
|
assert.ok(form.includes("{empty: 'same as above'}"), 'the first keeps its empty option');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('trResultRowHtml renders the manual entry and the additional info', () => {
|
||||||
|
const out = trResultRowHtml('test-f-result', '07', '04');
|
||||||
|
assert.ok(out.includes('07 - modified'), out);
|
||||||
|
assert.ok(out.includes('value="04"'), out);
|
||||||
|
assert.ok(out.includes('id="tr-addinfo-list"'), out);
|
||||||
|
assert.ok(out.includes('No specific cause can be given'), out);
|
||||||
|
assert.ok(out.includes('id="test-f-result-hexwrap" class="w-24 hidden"'), out);
|
||||||
|
const manual = trResultRowHtml('test-f-result', '1A', '');
|
||||||
|
assert.ok(manual.includes('id="test-f-result-hexwrap" class="w-24"'), manual);
|
||||||
|
assert.ok(manual.includes('value="1A"'), manual);
|
||||||
|
assert.ok(manual.includes('<option value="manual" selected>'), manual);
|
||||||
|
// a server-normalised (integer) additional info shows as hex
|
||||||
|
const intAdd = trResultRowHtml('test-f-result', '07', 10);
|
||||||
|
assert.ok(intAdd.includes('value="0A"'), intAdd);
|
||||||
|
});
|
||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.23.4"
|
version = "3.23.5"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
from osmocom.utils import rpad
|
||||||
|
|
||||||
|
|
||||||
VERSION = '3.23.4'
|
VERSION = '3.23.5'
|
||||||
|
|
||||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
@@ -4209,52 +4209,22 @@ def _build_tr(scc, cmd_num, cmd_type, dev_src, dev_dst, cmd_qual, dt=None):
|
|||||||
return base
|
return base
|
||||||
|
|
||||||
|
|
||||||
_RESULT_NAMES_BASIC = {
|
def _tr_result_name(b):
|
||||||
0x00: 'Command performed successfully',
|
"""The general result's name (TS 102 223 v18.3.0 8.12.0). The result byte
|
||||||
0x01: 'Command performed with partial comprehension',
|
has the same coding in the basic (03) and general (83) forms; only the
|
||||||
0x02: 'Command performed, with missing information',
|
general form carries additional information, which is named separately
|
||||||
0x03: 'REFUSED BY THE ME',
|
(8.12.2 for the terminal problem)."""
|
||||||
0x04: 'Command not understood by the ME',
|
return testscript.TR_RESULTS.get('%02X' % (b & 0xFF))
|
||||||
0x05: 'Command not permitted by the user',
|
|
||||||
0x06: 'Command performed with modification',
|
|
||||||
0x20: 'Proactive SIM session terminated by the user',
|
|
||||||
0x21: 'Backward move in the proactive SIM session requested by the user',
|
|
||||||
0x22: 'No response from user',
|
|
||||||
0x23: 'Help information required by the user',
|
|
||||||
0x24: 'Action in contradiction with the current timer state',
|
|
||||||
0x25: 'Interaction with call control by NAA, temporary problem',
|
|
||||||
0x26: 'Launch browser generic error',
|
|
||||||
}
|
|
||||||
|
|
||||||
_RESULT_NAMES_GENERAL = {
|
|
||||||
0x10: 'Command performed with additional information',
|
|
||||||
0x20: 'ME currently unable to process command',
|
|
||||||
0x21: 'Network currently unable to process command',
|
|
||||||
0x22: 'User did not accept the proactive command',
|
|
||||||
0x23: 'User cleared down call before connection or network release',
|
|
||||||
0x24: 'Action in contradiction with the current enforcement state',
|
|
||||||
0x25: 'Action in contradiction with the current timer state',
|
|
||||||
0x26: 'ME currently unable to process command',
|
|
||||||
0x27: 'User did not accept the proactive command',
|
|
||||||
0x28: 'User cleared down call before connection or network release',
|
|
||||||
0x29: 'Action in contradiction with the current enforcement state',
|
|
||||||
0x2A: 'Action in contradiction with the current timer state',
|
|
||||||
0x30: 'Command performed but partial understanding',
|
|
||||||
0x31: 'Command performed, with missing information',
|
|
||||||
0x32: 'REFUSED BY THE ME',
|
|
||||||
0x33: 'Command not understood by the ME',
|
|
||||||
0x34: 'Command not permitted by the user',
|
|
||||||
0x35: 'Command performed with modification',
|
|
||||||
}
|
|
||||||
|
|
||||||
def _tr_result_name(b, is_general):
|
def _tr_result_add_info_name(result, info):
|
||||||
table = _RESULT_NAMES_GENERAL if is_general else _RESULT_NAMES_BASIC
|
"""The general result's additional information name: 8.12.2 defines it for
|
||||||
if b in table:
|
the terminal problem (result 20); for the network problem (21) the coding
|
||||||
return table[b]
|
is NAA-specific and only '00' is defined; elsewhere it is not defined."""
|
||||||
if 0x40 <= b <= 0x4F or 0x70 <= b <= 0x7F:
|
if result == 0x20:
|
||||||
return 'Command performed with modification'
|
return testscript.TR_RESULT_ADD_INFO.get('%02X' % (info & 0xFF))
|
||||||
if 0x60 <= b <= 0x6F:
|
if result == 0x21 and (info & 0xFF) == 0x00:
|
||||||
return 'Command performed with limited understanding'
|
return testscript.TR_RESULT_ADD_INFO.get('00')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -4286,9 +4256,13 @@ def _record_tr(entry, tr_tlv, tr_sw=None):
|
|||||||
if result is not None:
|
if result is not None:
|
||||||
tag, val = result
|
tag, val = result
|
||||||
entry['tr_result'] = val.hex()
|
entry['tr_result'] = val.hex()
|
||||||
name = _tr_result_name(val[0], tag == 0x83)
|
name = _tr_result_name(val[0])
|
||||||
if name:
|
if name:
|
||||||
entry['tr_result_name'] = name
|
entry['tr_result_name'] = name
|
||||||
|
if len(val) > 1:
|
||||||
|
info_name = _tr_result_add_info_name(val[0], val[1])
|
||||||
|
if info_name:
|
||||||
|
entry['tr_result_info_name'] = info_name
|
||||||
entry['tr_decoded'] = _decode_tr(entry.get('type_hex'), entry.get('qualifier'), entry['tr_hex'])
|
entry['tr_decoded'] = _decode_tr(entry.get('type_hex'), entry.get('qualifier'), entry['tr_hex'])
|
||||||
except Exception:
|
except Exception:
|
||||||
entry['tr_decoded'] = []
|
entry['tr_decoded'] = []
|
||||||
@@ -7095,9 +7069,12 @@ def _test_drain_pending(scc, fetch_len, result=0x00, max_commands=3):
|
|||||||
return {'note': 'drain failed: %s' % e}
|
return {'note': 'drain failed: %s' % e}
|
||||||
if not answered:
|
if not answered:
|
||||||
return {'note': 'nothing to drain'}
|
return {'note': 'nothing to drain'}
|
||||||
result_name = {0x00: 'ok', 0x10: 'cancel', 0x11: 'back',
|
# the drain's sent-TR summary: the short alias where one exists, else the
|
||||||
0x12: 'timeout', 0x22: 'no response'}.get(result,
|
# spec name (TS 102 223 8.12.0) - 0x22 is "user did not accept" (the old
|
||||||
'0x%02X' % result)
|
# label "no response" belongs to 0x12)
|
||||||
|
_DRAIN_ALIAS = {0x00: 'ok', 0x10: 'cancel', 0x11: 'back', 0x12: 'timeout',
|
||||||
|
0x22: 'refused'}
|
||||||
|
result_name = _DRAIN_ALIAS.get(result) or _tr_result_name(result) or ('0x%02X' % result)
|
||||||
return {'sent': '; '.join('TR %s %s' % (result_name, a['tr']) for a in answered),
|
return {'sent': '; '.join('TR %s %s' % (result_name, a['tr']) for a in answered),
|
||||||
'sw': answered[-1]['sw'], 'data': answered[0]['raw'],
|
'sw': answered[-1]['sw'], 'data': answered[0]['raw'],
|
||||||
'note': '; '.join('pending %s (0x%02X) answered with %s'
|
'note': '; '.join('pending %s (0x%02X) answered with %s'
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from pysim_simple_server import events
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'ScriptError', 'ACTION_KINDS', 'FAIL_LEVELS', 'POR_CHECKS', 'RESULT_NAMES',
|
'ScriptError', 'ACTION_KINDS', 'FAIL_LEVELS', 'POR_CHECKS', 'RESULT_NAMES',
|
||||||
|
'TR_RESULTS', 'TR_RESULT_ADD_INFO',
|
||||||
'normalise_script', 'normalise_step', 'normalise_respond',
|
'normalise_script', 'normalise_step', 'normalise_respond',
|
||||||
'match_value', 'match_text', 'match_item', 'combine_levels', 'build_tr',
|
'match_value', 'match_text', 'match_item', 'combine_levels', 'build_tr',
|
||||||
'pack_gsm7',
|
'pack_gsm7',
|
||||||
@@ -37,11 +38,79 @@ ACTION_KINDS = ('envelope', 'event', 'menu-select', 'file-write', 'file-read',
|
|||||||
FAIL_LEVELS = ('error', 'warning')
|
FAIL_LEVELS = ('error', 'warning')
|
||||||
POR_CHECKS = ('none', 'ok', 'any')
|
POR_CHECKS = ('none', 'ok', 'any')
|
||||||
|
|
||||||
# TERMINAL RESPONSE result values commonly used by scripts (TS 102 223 8.12).
|
# TERMINAL RESPONSE general results (TS 102 223 v18.3.0 8.12.0) and the
|
||||||
|
# terminal-problem additional information (8.12.2), pinned by
|
||||||
|
# frontend/tests/tr_results.json (both suites drift-guard against it).
|
||||||
|
TR_RESULTS = {
|
||||||
|
'00': 'Command performed successfully',
|
||||||
|
'01': 'Command performed with partial comprehension',
|
||||||
|
'02': 'Command performed, with missing information',
|
||||||
|
'03': 'REFRESH performed with additional EFs read',
|
||||||
|
'04': 'Command performed successfully, but requested icon could not be displayed',
|
||||||
|
'05': 'Command performed, but modified by call control by NAA',
|
||||||
|
'06': 'Command performed successfully, limited service',
|
||||||
|
'07': 'Command performed with modification',
|
||||||
|
'08': 'REFRESH performed but indicated NAA was not active',
|
||||||
|
'09': 'Command performed successfully, tone not played',
|
||||||
|
'10': 'Proactive UICC session terminated by the user',
|
||||||
|
'11': 'Backward move in the proactive UICC session requested by the user',
|
||||||
|
'12': 'No response from user',
|
||||||
|
'13': 'Help information required by the user',
|
||||||
|
'14': 'Reserved for GSM/3G',
|
||||||
|
'15': 'Reserved for 3GPP (for future usage)',
|
||||||
|
'16': 'Reserved for 3GPP (for future usage)',
|
||||||
|
'20': 'Terminal currently unable to process command',
|
||||||
|
'21': 'Network currently unable to process command',
|
||||||
|
'22': 'User did not accept the proactive command',
|
||||||
|
'23': 'User cleared down call before connection or network release',
|
||||||
|
'24': 'Action in contradiction with the current timer state',
|
||||||
|
'25': 'Interaction with call control by NAA, temporary problem',
|
||||||
|
'26': 'Launch browser generic error',
|
||||||
|
'27': 'MMS temporary problem',
|
||||||
|
'28': 'Reserved for 3GPP (BIP temporary error, T timer in the additional information)',
|
||||||
|
'29': 'Reserved for 3GPP (for future usage)',
|
||||||
|
'30': "Command beyond terminal's capabilities",
|
||||||
|
'31': 'Command type not understood by terminal',
|
||||||
|
'32': 'Command data not understood by terminal',
|
||||||
|
'33': 'Command number not known by terminal',
|
||||||
|
'34': 'Reserved for GSM/3G',
|
||||||
|
'35': 'Reserved for GSM/3G',
|
||||||
|
'36': 'Error, required values are missing',
|
||||||
|
'37': 'Reserved for GSM/3G',
|
||||||
|
'38': 'MultipleCard commands error',
|
||||||
|
'39': 'Interaction with call control by NAA, permanent problem',
|
||||||
|
'3A': 'Bearer Independent Protocol permanent error',
|
||||||
|
'3B': 'Access Technology unable to process command',
|
||||||
|
'3C': 'Frames error',
|
||||||
|
'3D': 'MMS Error',
|
||||||
|
'3E': 'Reserved for 3GPP (for future usage)',
|
||||||
|
'3F': 'Reserved for 3GPP (for future usage)',
|
||||||
|
}
|
||||||
|
|
||||||
|
TR_RESULT_ADD_INFO = {
|
||||||
|
'00': 'No specific cause can be given',
|
||||||
|
'01': 'Screen is busy',
|
||||||
|
'02': 'Terminal currently busy on call',
|
||||||
|
'03': 'Reserved for GSM/3G',
|
||||||
|
'04': 'No service',
|
||||||
|
'05': 'Access control class bar',
|
||||||
|
'06': 'Radio resource not granted',
|
||||||
|
'07': 'Not in speech call',
|
||||||
|
'08': 'Reserved for GSM/3G',
|
||||||
|
'09': 'Terminal currently busy on SEND DTMF command',
|
||||||
|
'0A': 'No NAA active',
|
||||||
|
}
|
||||||
|
|
||||||
|
# The script authoring aliases: a convenience for the common values (the
|
||||||
|
# script may also use the two-digit hex value directly). `no_response` is
|
||||||
|
# kept as an alias of 12 for backward compatibility but is not offered by the
|
||||||
|
# editor (12's canonical alias is `timeout`); the v3.23.5 review corrected
|
||||||
|
# `refused` (03 -> 22), `not_understood` (04 -> 32), `modified` (06 -> 07)
|
||||||
|
# and `no_response` (22 -> 12) to their TS 102 223 8.12.0 meanings.
|
||||||
RESULT_NAMES = {
|
RESULT_NAMES = {
|
||||||
'ok': 0x00, 'partial': 0x01, 'missing': 0x02, 'refused': 0x03,
|
'ok': 0x00, 'partial': 0x01, 'missing': 0x02, 'refused': 0x22,
|
||||||
'not_understood': 0x04, 'modified': 0x06,
|
'not_understood': 0x32, 'modified': 0x07,
|
||||||
'cancel': 0x10, 'back': 0x11, 'timeout': 0x12, 'no_response': 0x22,
|
'cancel': 0x10, 'back': 0x11, 'timeout': 0x12, 'no_response': 0x12,
|
||||||
}
|
}
|
||||||
|
|
||||||
_HEX_MASK_RE = re.compile(r'^[0-9A-F?]+$')
|
_HEX_MASK_RE = re.compile(r'^[0-9A-F?]+$')
|
||||||
@@ -89,6 +158,16 @@ def _int(value, what, lo, hi):
|
|||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def _hex_byte(value, what):
|
||||||
|
"""A byte given as an int or as a hex string ('0A' / '0a' / '0x0A')."""
|
||||||
|
if isinstance(value, str):
|
||||||
|
text = value.strip().lower().replace('0x', '')
|
||||||
|
if not re.fullmatch(r'[0-9a-f]{1,2}', text):
|
||||||
|
raise ScriptError('%s must be a hex byte' % what)
|
||||||
|
return int(text, 16)
|
||||||
|
return _int(value, what, 0, 255)
|
||||||
|
|
||||||
|
|
||||||
def _data_hex(value, what, allow_empty=False):
|
def _data_hex(value, what, allow_empty=False):
|
||||||
if value in (None, ''):
|
if value in (None, ''):
|
||||||
if allow_empty:
|
if allow_empty:
|
||||||
@@ -562,12 +641,17 @@ def normalise_respond(respond, cmd_type=None):
|
|||||||
else:
|
else:
|
||||||
res = _int(res, 'respond result', 0, 255)
|
res = _int(res, 'respond result', 0, 255)
|
||||||
out = {'result': res}
|
out = {'result': res}
|
||||||
|
if respond.get('additional_info') is not None:
|
||||||
|
out['additional_info'] = _hex_byte(respond['additional_info'],
|
||||||
|
'respond additional_info')
|
||||||
if respond.get('item_id') is not None:
|
if respond.get('item_id') is not None:
|
||||||
out['item_id'] = _int(respond['item_id'], 'respond item_id', 1, 255)
|
out['item_id'] = _int(respond['item_id'], 'respond item_id', 1, 255)
|
||||||
if respond.get('text') is not None:
|
if respond.get('text') is not None:
|
||||||
out['text'] = str(respond['text'])
|
out['text'] = str(respond['text'])
|
||||||
dcs = respond.get('dcs')
|
dcs = respond.get('dcs')
|
||||||
out['dcs'] = _int(dcs, 'respond dcs', 0, 255) if dcs is not None else 0x00
|
# the field is a hex byte (the PWA's input strips to hex): parse it as
|
||||||
|
# hex, not decimal - '0A' used to be refused and '10' read as 0x0A
|
||||||
|
out['dcs'] = _hex_byte(dcs, 'respond dcs') if dcs is not None else 0x00
|
||||||
# The same coding the TERMINAL RESPONSE builder uses: a text the
|
# The same coding the TERMINAL RESPONSE builder uses: a text the
|
||||||
# selected dcs cannot code (a non-GSM character with 00/04) must fail
|
# selected dcs cannot code (a non-GSM character with 00/04) must fail
|
||||||
# the script at load, not mid-run after the command was fetched
|
# the script at load, not mid-run after the command was fetched
|
||||||
@@ -627,7 +711,8 @@ def build_tr(cmd_num, cmd_type, dev_dst, dev_src, respond, answer_tlvs=None):
|
|||||||
out = bytearray([0x81, 0x03, cmd_num & 0xFF, cmd_type & 0xFF, 0x00])
|
out = bytearray([0x81, 0x03, cmd_num & 0xFF, cmd_type & 0xFF, 0x00])
|
||||||
out += bytes([0x82, 0x02, dev_dst & 0xFF, dev_src & 0xFF])
|
out += bytes([0x82, 0x02, dev_dst & 0xFF, dev_src & 0xFF])
|
||||||
result = int(respond.get('result', 0))
|
result = int(respond.get('result', 0))
|
||||||
out += bytes([0x83, 0x02, result & 0xFF, 0x00])
|
add_info = int(respond.get('additional_info', 0) or 0)
|
||||||
|
out += bytes([0x83, 0x02, result & 0xFF, add_info & 0xFF])
|
||||||
if answer_tlvs:
|
if answer_tlvs:
|
||||||
out += bytes.fromhex(answer_tlvs)
|
out += bytes.fromhex(answer_tlvs)
|
||||||
if respond.get('text') is not None:
|
if respond.get('text') is not None:
|
||||||
|
|||||||
@@ -1044,13 +1044,21 @@ class TestProactiveDecode(unittest.TestCase):
|
|||||||
entry = {'type_hex': '26', 'qualifier': '00'}
|
entry = {'type_hex': '26', 'qualifier': '00'}
|
||||||
_record_tr(entry, bytes.fromhex('8103032600820281839305521001100083022001'))
|
_record_tr(entry, bytes.fromhex('8103032600820281839305521001100083022001'))
|
||||||
self.assertEqual(entry['tr_result'], '2001')
|
self.assertEqual(entry['tr_result'], '2001')
|
||||||
self.assertEqual(entry['tr_result_name'], 'ME currently unable to process command')
|
self.assertEqual(entry['tr_result_name'], 'Terminal currently unable to process command')
|
||||||
|
# the general result's additional information (8.12.2 for 0x20)
|
||||||
|
self.assertEqual(entry['tr_result_info_name'], 'Screen is busy')
|
||||||
self.assertEqual(entry['tr_hex'], '93055210011000')
|
self.assertEqual(entry['tr_hex'], '93055210011000')
|
||||||
|
|
||||||
def test_record_tr_unknown_result(self):
|
def test_record_tr_unknown_result(self):
|
||||||
|
# 0x07 is defined (v3.23.5 corrected the table: 'Command performed
|
||||||
|
# with modification'); a reserved value (0x40) carries no name
|
||||||
entry = {'type_hex': '26', 'qualifier': '00'}
|
entry = {'type_hex': '26', 'qualifier': '00'}
|
||||||
_record_tr(entry, bytes.fromhex('810303260082028181030107'))
|
_record_tr(entry, bytes.fromhex('810303260082028181030107'))
|
||||||
self.assertEqual(entry['tr_result'], '07')
|
self.assertEqual(entry['tr_result'], '07')
|
||||||
|
self.assertEqual(entry['tr_result_name'], 'Command performed with modification')
|
||||||
|
entry = {'type_hex': '26', 'qualifier': '00'}
|
||||||
|
_record_tr(entry, bytes.fromhex('810303260082028181030140'))
|
||||||
|
self.assertEqual(entry['tr_result'], '40')
|
||||||
self.assertNotIn('tr_result_name', entry)
|
self.assertNotIn('tr_result_name', entry)
|
||||||
|
|
||||||
def test_record_tr_no_result_tlv(self):
|
def test_record_tr_no_result_tlv(self):
|
||||||
@@ -1257,7 +1265,9 @@ class TestExpandedRemoteResponse(unittest.TestCase):
|
|||||||
# Result TLV: 03 01 6A (error_code 0x6A)
|
# Result TLV: 03 01 6A (error_code 0x6A)
|
||||||
_record_tr(entry, bytes.fromhex('81030326008202818303016A'))
|
_record_tr(entry, bytes.fromhex('81030326008202818303016A'))
|
||||||
self.assertEqual(entry['tr_result'], '6a')
|
self.assertEqual(entry['tr_result'], '6a')
|
||||||
self.assertEqual(entry['tr_result_name'], 'Command performed with limited understanding')
|
# 0x6A is not a defined general result (TS 102 223 8.12.0) - the old
|
||||||
|
# 'Command performed with limited understanding' fallback was invented
|
||||||
|
self.assertNotIn('tr_result_name', entry)
|
||||||
|
|
||||||
def test_expanded_response_with_chaining(self):
|
def test_expanded_response_with_chaining(self):
|
||||||
entry = {'type_hex': '26', 'qualifier': '00'}
|
entry = {'type_hex': '26', 'qualifier': '00'}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""TERMINAL RESPONSE results (TS 102 223 v18.3.0 8.12.0/8.12.2) against the
|
||||||
|
SHARED fixture `frontend/tests/tr_results.json`: the Node suite asserts the
|
||||||
|
same file against the PWA's own tables, this suite asserts it against
|
||||||
|
`testscript.py`, so neither implementation can drift alone. Also pins the
|
||||||
|
corrected script aliases (the v3.23.5 review), the hex parsing of the
|
||||||
|
`dcs`/`additional_info` fields and the scripted TR's general result."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
PROJECTS = pathlib.Path(__file__).resolve().parents[2]
|
||||||
|
PY_SIM = PROJECTS / 'pysim'
|
||||||
|
if str(PY_SIM) not in sys.path:
|
||||||
|
sys.path.insert(0, str(PY_SIM))
|
||||||
|
|
||||||
|
import pysim_simple_server.server as S
|
||||||
|
import pysim_simple_server.testscript as T
|
||||||
|
|
||||||
|
FIXTURE = (pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
/ 'frontend' / 'tests' / 'tr_results.json')
|
||||||
|
|
||||||
|
|
||||||
|
class TrResults(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.data = json.loads(FIXTURE.read_text(encoding='utf-8'))
|
||||||
|
|
||||||
|
def test_the_shared_result_table(self):
|
||||||
|
want = {r['v']: r['name'] for r in self.data['results']}
|
||||||
|
self.assertEqual(T.TR_RESULTS, want)
|
||||||
|
# the proactive log's decode uses the same table (a spec name, not the
|
||||||
|
# old GSM 11.14 mix)
|
||||||
|
self.assertEqual(S._tr_result_name(0x03), want['03'])
|
||||||
|
self.assertEqual(S._tr_result_name(0x20), want['20'])
|
||||||
|
|
||||||
|
def test_the_shared_additional_info_table(self):
|
||||||
|
want = {a['v']: a['name'] for a in self.data['additional_info']}
|
||||||
|
self.assertEqual(T.TR_RESULT_ADD_INFO, want)
|
||||||
|
|
||||||
|
def test_the_shared_aliases(self):
|
||||||
|
want = {k: int(v, 16) for k, v in self.data['aliases'].items()}
|
||||||
|
self.assertEqual(T.RESULT_NAMES, want)
|
||||||
|
|
||||||
|
def test_the_corrected_aliases(self):
|
||||||
|
# the v3.23.5 review: these four carried legacy (GSM 11.14) values
|
||||||
|
self.assertEqual(T.RESULT_NAMES['refused'], 0x22)
|
||||||
|
self.assertEqual(T.RESULT_NAMES['not_understood'], 0x32)
|
||||||
|
self.assertEqual(T.RESULT_NAMES['modified'], 0x07)
|
||||||
|
self.assertEqual(T.RESULT_NAMES['no_response'], 0x12)
|
||||||
|
# the common ones were already right
|
||||||
|
self.assertEqual(T.RESULT_NAMES['ok'], 0x00)
|
||||||
|
self.assertEqual(T.RESULT_NAMES['cancel'], 0x10)
|
||||||
|
self.assertEqual(T.RESULT_NAMES['timeout'], 0x12)
|
||||||
|
|
||||||
|
def test_the_tr_decode_names(self):
|
||||||
|
self.assertEqual(S._tr_result_name(0x22), 'User did not accept the proactive command')
|
||||||
|
self.assertIsNone(S._tr_result_name(0x99))
|
||||||
|
self.assertEqual(S._tr_result_add_info_name(0x20, 0x04), 'No service')
|
||||||
|
self.assertEqual(S._tr_result_add_info_name(0x21, 0x00), 'No specific cause can be given')
|
||||||
|
self.assertIsNone(S._tr_result_add_info_name(0x10, 0x04))
|
||||||
|
|
||||||
|
def test_the_respond_normalises_the_additional_info(self):
|
||||||
|
out = T.normalise_respond({'result': 'refused', 'additional_info': '0A'})
|
||||||
|
self.assertEqual(out['result'], 0x22)
|
||||||
|
self.assertEqual(out['additional_info'], 0x0A)
|
||||||
|
out = T.normalise_respond({'result': '00', 'additional_info': 4})
|
||||||
|
self.assertEqual(out['additional_info'], 4)
|
||||||
|
out = T.normalise_respond({'result': 'ok'})
|
||||||
|
self.assertNotIn('additional_info', out)
|
||||||
|
with self.assertRaises(T.ScriptError):
|
||||||
|
T.normalise_respond({'result': 'ok', 'additional_info': 'zz'})
|
||||||
|
|
||||||
|
def test_the_dcs_field_parses_as_hex(self):
|
||||||
|
# '0A' used to be refused and '10' read as decimal 10 (= 0x0A)
|
||||||
|
self.assertEqual(T.normalise_respond({'result': 'ok', 'text': 'A', 'dcs': '0A'})['dcs'], 0x0A)
|
||||||
|
self.assertEqual(T.normalise_respond({'result': 'ok', 'text': 'A', 'dcs': '10'})['dcs'], 0x10)
|
||||||
|
|
||||||
|
def test_the_scripted_tr_carries_the_additional_info(self):
|
||||||
|
tr = T.build_tr(1, 0x21, 0x81, 0x82, {'result': 0x20, 'additional_info': 0x04})
|
||||||
|
self.assertIn(bytes([0x83, 0x02, 0x20, 0x04]), tr)
|
||||||
|
tr = T.build_tr(1, 0x21, 0x81, 0x82, {'result': 0x00})
|
||||||
|
self.assertIn(bytes([0x83, 0x02, 0x00, 0x00]), tr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user